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

  1. What You Will Trace
  2. The Files Involved
  3. Step 1 — Understand the Data Source
  4. Step 2 — Verify the Metadata
  5. Step 3 — Read the Definition
  6. Step 4 — Seed the Dashboard
  7. Step 5 — Design It in the Browser
  8. Step 6 — Verify the Result
  9. Extension Exercises
  10. Related Guides

What You Will Trace

The repository contains a working pair:

  • Data source: FrameworktestdocumentSales, a tabular report over Frameworktestdocumentline.
  • Dashboard: procurement_overview, seeded from frameworkapp/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:

python
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

python
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

text
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:

text
/dashboards/manage/report-meta/?report=frameworkapp.frameworktestdocumentsales

Expected shape (abridged):

json
{
  "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:

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

python
{
    "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.enabled enables the tab's date defaults. No explicit default_period is set, so the dashboard/site default applies — by default, the last 30 days.
  • total_quantity uses the Sum computation field as a true aggregate.
  • total_value uses the same pattern with format: "currency" and decimals: 2.
  • quantity_by_item is a bar chart: category_field: "item_code", value_field: "sum__item_quantity", limit: 10, sort: "desc".
  • sales_detail is a table over all four columns with page_size: 10.

Tab 2 — Breakdown

  • value_share is a donut chart over item_code by sum__item_price, limited to 8 slices.
  • preview is a non-paginated preview of three columns with row_limit: 5.

Layout map

text
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:

bash
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_overview would 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:

text
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:

bash
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

  1. Open /dashboards/manage/.
  2. Open Procurement overview.
  3. Select the Overview tab.
  4. Open total_quantity. - Data shows the report and kpi type. - Display shows sum__item_quantity and sum. - Layout shows x=0, y=0, w=3, h=2.
  5. Change the aggregation to avg and save. The draft changes; the live dashboard does not.
  6. Preview the tab. The KPI now shows the average across grouped report rows.
  7. Revert to sum and 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.py and 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 quantity shows a number, not an error.
  • [ ] Total value shows a currency-formatted number.
  • [ ] Quantity by item renders a bar chart with up to 10 categories.
  • [ ] Item detail renders 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.view sees 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

python
{
    "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

python
{
    "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

python
{
    "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:

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