Skip to content

feat: add statistics dashboard module (spp_statistics_dashboard)#77

Draft
jeremi wants to merge 18 commits into19.0from
feat/spp-dashboard
Draft

feat: add statistics dashboard module (spp_statistics_dashboard)#77
jeremi wants to merge 18 commits into19.0from
feat/spp-dashboard

Conversation

@jeremi
Copy link
Member

@jeremi jeremi commented Mar 6, 2026

Summary

  • New spp_statistics_dashboard module: materialized dashboard for published statistics with kanban KPI cards, list, pivot, and graph views
  • Background refresh via queue_job (manual trigger + daily cron) computes statistics across three dimensions: system-wide, per-area, and per-program
  • k-anonymity suppression applied to small-cell values
  • Search panel sidebar for category and program filtering, area level filters (Level 0-3) for admin-level drill-down
  • Three-tier security: Dashboard Viewer (read-only) and Dashboard Manager (can trigger refresh)

Also includes

  • fix(spp_cel_domain): Odoo ORM returns False for unset non-boolean fields (Datetime, Date, etc.), but CEL null maps to Python None. _safe_getattr now normalizes False to None for non-boolean Odoo fields, fixing null comparisons like m.disabled != null
  • fix(spp_mis_demo_v2): Removed disabled_members CEL variable/statistic that referenced Odoo's archival disabled timestamp field instead of a disability indicator

Test plan

  • spp_statistics_dashboard: 48 tests (unit, integration, access control, views)
  • spp_cel_domain: 571 tests (includes 2 new null normalization tests)
  • spp_mis_demo_v2: 254 tests (updated for removed disabled_members)
  • Manual: install module, refresh stats, verify kanban/list/pivot/graph views
  • Manual: verify program and area level filters work in search
  • Manual: verify suppressed values display correctly for small populations

jeremi added 15 commits March 6, 2026 10:40
Materializes published statistics into spp.dashboard.data snapshot table
and renders via kanban/list/pivot/graph views with area and program
filtering. Includes queue_job background refresh, k-anonymity suppression,
three-tier security, and daily cron.
Replace _sql_constraints with models.Constraint (Odoo 19 API) and
remove invalid numbercall field from ir.cron definition.
Remove invalid groups_id field from ir.actions.server and add title
attributes to FontAwesome icons in kanban view template.
Use draft_name instead of name when creating spp.area records (name
is computed). Fix view load tests to assert on 'arch' key. Use correct
area_level values (computed from parent hierarchy).
… names

Replace models.Constraint with COALESCE-based unique index in init()
to handle NULL area_id/program_id correctly. Fix groups_id -> group_ids
for Odoo 19 user model.
Use has_group() for group membership checks instead of assertIn on
group_ids. Avoid unique constraint collision in manager_can_create test.
Narrow except clause in _refresh_statistic to catch only expected
errors (ValueError, TypeError, KeyError, AttributeError) instead of
broad Exception. Fix ACL entry IDs to follow access_{model}_{group}
naming convention.
Three issues found during live testing with demo data:

1. System-wide scope used scope_type "area" with area_id=False, which
   the area resolver treats as empty (returns 0 registrants). Changed
   to scope_type "cel" with expression "true" to match all registrants.

2. Double suppression: aggregation service already applies suppression
   and returns display strings (e.g., "<5"). _upsert_data then called
   stat.apply_suppression() again. Removed the redundant call.

3. Refresh menu not accessible when dashboard is empty (chicken-and-egg).
   Added "Refresh Statistics" as a dedicated submenu item, always
   accessible to managers.

Also removed program_id iteration from refresh since the scope resolver
does not support program filtering (was creating duplicate identical rows).
The CEL scope type fails because the scope resolver's env.get() check
on the AbstractModel executor returns a falsy empty recordset, causing
it to log "CEL executor not available" and return 0 registrants. This
made all system-wide values show as suppressed (<5) regardless of
actual population size.

Fix: for system-wide scope, query all registrant IDs directly and use
the "explicit" scope type which doesn't depend on the CEL executor.

Add integration tests that call through the real aggregation pipeline
(no mocks) to catch scope resolution and suppression issues.
The disabled_members variable used `m.disabled != null` as its aggregate
filter, but `res_partner.disabled` is Odoo's archival timestamp field
(when a record was deactivated), not a disability indicator. The CEL
evaluator returns False for NULL Datetime fields instead of None, so
`False != None` evaluated to True for every member, producing incorrect
counts (431 "disabled" members when none actually had disabilities).

Removes both the CEL variable (cel_var_disabled_members) and the
statistic (stat_disabled_members) from demo data, and updates tests.
Odoo ORM returns False for unset non-boolean fields (Datetime, Date,
Char, Many2one, etc.), but CEL null maps to Python None. This caused
expressions like `m.disabled != null` to evaluate as `False != None`
which is True, matching every record even when the field is actually
NULL in the database.

_safe_getattr now detects Odoo records (via _fields attribute) and
converts False to None for non-boolean fields, so CEL null comparisons
behave correctly. Boolean fields retain their False value.
More descriptive module name that clearly indicates this is the
dashboard layer for the statistics subsystem. Updates all XML ID
references, system parameter prefixes, and test refs.
The dashboard now computes statistics for each active program in
addition to system-wide and per-area dimensions. Program scope is
built by querying enrolled members via get_beneficiaries() and passing
their IDs as an explicit scope to the aggregation service.

Refactors _refresh_statistic into _refresh_scope for clearer
separation of the iteration logic from the per-scope computation.
- Add Program to search panel sidebar (multi-select with counters)
- Add area level filters (Level 0-3) for quick admin-level filtering
- Add "Per Program" scope filter
- Add "Area Level" group-by option
- Show program_name column by default in list view
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new feature: a comprehensive statistics dashboard module (spp_statistics_dashboard). This module provides users with interactive visualizations and filtered views of key statistics, enabling better monitoring and analysis. It also includes crucial fixes to the Common Expression Language (CEL) domain to ensure accurate data interpretation and cleans up outdated demo data, enhancing the overall reliability and usability of the system's statistical capabilities.

Highlights

  • New Statistics Dashboard Module: Introduced a materialized dashboard module (spp_statistics_dashboard) for published statistics, featuring kanban KPI cards, list, pivot, and graph views to provide comprehensive data visualization.
  • Automated Background Refresh: Implemented a background refresh mechanism for dashboard statistics via queue_job, with both manual and daily cron triggers, computing data across system-wide, per-area, and per-program dimensions.
  • Data Privacy with k-anonymity: Applied k-anonymity suppression to small-cell values within the dashboard to protect sensitive data and ensure privacy.
  • Enhanced Filtering Capabilities: Added a search panel sidebar for category and program filtering, along with area level filters (Level 0-3) for administrative drill-down within the dashboard views.
  • Three-Tier Security Model: Established a role-based security model with Dashboard Viewer (read-only) and Dashboard Manager (can trigger refresh) roles to control access to the statistics dashboard.
  • CEL Null Normalization Fix: Corrected an issue in spp_cel_domain where Odoo ORM's False for unset non-boolean fields was not correctly mapped to CEL's null, ensuring accurate null comparisons in expressions.
  • Demo Data Cleanup: Removed the disabled_members CEL variable and statistic from spp_mis_demo_v2 demo data, as it referenced an inappropriate Odoo archival field, improving data relevance.
Changelog
  • spp_cel_domain/services/cel_parser.py
    • Modified _safe_getattr to normalize Odoo ORM's False value for unset non-boolean fields (like Datetime, Date, Char, Many2one) to Python None, ensuring correct CEL null comparisons.
  • spp_cel_domain/tests/test_cel_parser.py
    • Added two new test cases to verify the correct normalization of Odoo's False to None for non-boolean fields and to confirm that legitimate boolean False values are not treated as null.
  • spp_mis_demo_v2/data/demo_statistics.xml
    • Removed the cel_var_disabled_members variable and the stat_disabled_members statistic, which were incorrectly referencing Odoo's archival disabled timestamp field.
  • spp_mis_demo_v2/tests/test_demo_statistics.py
    • Updated test assertions to reflect the removal of the disabled_members statistic, adjusting expected counts and category assignments.
  • spp_statistics_dashboard/init.py
    • Added the module initialization file.
  • spp_statistics_dashboard/manifest.py
    • Defined the new module's metadata, dependencies (including spp_statistic, spp_aggregation, spp_area, spp_programs, spp_security, queue_job), and data files.
  • spp_statistics_dashboard/data/ir_cron.xml
    • Introduced a daily cron job to automatically refresh dashboard statistics.
  • spp_statistics_dashboard/models/init.py
    • Added the models initialization file for the dashboard module.
  • spp_statistics_dashboard/models/dashboard_data.py
    • Implemented the spp.dashboard.data model, including database indexes, fields for statistic values, area, program, and category, and methods for refreshing data across various scopes (system-wide, per-area, per-program) with k-anonymity suppression and value formatting.
  • spp_statistics_dashboard/readme/DESCRIPTION.md
    • Provided a detailed description of the new dashboard module, outlining its key capabilities, models, configuration, UI location, security, extension points, and dependencies.
  • spp_statistics_dashboard/security/groups.xml
    • Defined group_dashboard_read and group_dashboard_manage technical groups, and user-facing group_dashboard_viewer and group_dashboard_manager groups with appropriate implied access.
  • spp_statistics_dashboard/security/ir.model.access.csv
    • Configured read, write, create, and unlink access rights for the spp.dashboard.data model based on the new security groups.
  • spp_statistics_dashboard/security/privileges.xml
    • Defined a new privilege, "Statistics Dashboard," under the "OpenSPP" category.
  • spp_statistics_dashboard/tests/init.py
    • Added the test initialization file for the dashboard module.
  • spp_statistics_dashboard/tests/test_dashboard_access.py
    • Implemented unit tests to verify access control for dashboard data based on viewer, manager, and no-access user roles.
  • spp_statistics_dashboard/tests/test_dashboard_data.py
    • Implemented unit tests for the spp.dashboard.data model, covering creation, unique constraints, cascade deletions, value formatting, and related field storage. Also tested view definitions.
  • spp_statistics_dashboard/tests/test_dashboard_integration.py
    • Implemented integration tests to verify the end-to-end dashboard refresh process, including scope resolution and aggregation service interaction.
  • spp_statistics_dashboard/tests/test_dashboard_refresh.py
    • Implemented unit tests for the dashboard refresh logic, covering data creation, upserts, suppressed values, error isolation, stale data cleanup, and scope building.
  • spp_statistics_dashboard/views/dashboard_data_views.xml
    • Defined the UI views for spp.dashboard.data, including kanban cards, a detailed list view, a comprehensive search view with filters and a search panel, pivot and graph views, and a server action to trigger manual data refresh.
  • spp_statistics_dashboard/views/menus.xml
    • Added menu items for the "Statistics Dashboard" under a new root menu, including options to view the dashboard and manually refresh statistics.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new spp_statistics_dashboard module, which provides a materialized dashboard for statistics. The implementation is very thorough, including background refresh jobs, k-anonymity suppression, comprehensive views, and well-defined security roles. The PR also includes a fix for spp_cel_domain to correctly handle Odoo's False values for unset fields in CEL expressions. The code quality is high, with excellent test coverage across unit, integration, and access control scenarios. I have one suggestion regarding a potential scalability issue in how system-wide scope is handled, which could be problematic on very large datasets.

# System-wide scope: query all registrant IDs directly and use
# explicit scope. We can't use CEL scope because the scope resolver's
# env.get() check on the AbstractModel executor returns falsy.
all_ids = self.env["res.partner"].sudo().search([("is_registrant", "=", True)]).ids

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Loading all registrant IDs into memory with search(...).ids could lead to high memory consumption and performance issues on systems with a very large number of registrants (e.g., millions). This presents a potential scalability bottleneck.

While the comment explains this is a workaround, it would be ideal to investigate if the underlying issue with the CEL scope resolver can be fixed to allow for a domain-based aggregation, which would be more memory-efficient. If that's not feasible in the short term, this implementation carries a scalability risk that should be acknowledged.

@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.91%. Comparing base (624e3cd) to head (3623bed).
⚠️ Report is 17 commits behind head on 19.0.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0      #77      +/-   ##
==========================================
+ Coverage   58.09%   64.91%   +6.82%     
==========================================
  Files         188      384     +196     
  Lines       10797    20487    +9690     
==========================================
+ Hits         6272    13300    +7028     
- Misses       4525     7187    +2662     
Flag Coverage Δ
spp_aggregation 80.10% <100.00%> (?)
spp_api_v2 79.96% <ø> (?)
spp_api_v2_change_request 60.22% <ø> (?)
spp_api_v2_cycles 71.12% <ø> (?)
spp_api_v2_data 64.41% <ø> (?)
spp_api_v2_entitlements 70.19% <ø> (?)
spp_api_v2_gis 71.52% <ø> (-0.72%) ⬇️
spp_api_v2_products 66.27% <ø> (?)
spp_api_v2_service_points 70.94% <ø> (?)
spp_api_v2_simulation 69.61% <ø> (?)
spp_api_v2_vocabulary 57.26% <ø> (?)
spp_approval 50.29% <ø> (?)
spp_base_common 90.26% <ø> (ø)
spp_programs 45.43% <ø> (ø)
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_aggregation/models/service_scope_resolver.py 70.30% <100.00%> (ø)

... and 196 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

jeremi added 3 commits March 6, 2026 23:38
…alls

Both sudo() usages are intentional:
- ir.config_parameter.sudo(): standard Odoo pattern for system params
- res.partner.sudo(): needed for queue_job cron context, reads IDs only
Adds an all_registrants scope type to the scope resolver that searches
for all registrants server-side. This avoids loading millions of IDs
into the caller's memory, which was a scalability concern when the
dashboard used explicit scope with all registrant IDs.

The dashboard's system-wide scope now uses {"scope_type": "all_registrants"}
instead of querying all IDs and passing them via explicit_partner_ids.
…nor issues

- Use scope builders from spp_aggregation.services instead of inline dicts
- Bulk-load existing rows to eliminate N+1 search per upsert
- Hoist label/config lookup out of per-scope loop
- Pass program_ids from action_refresh_all to avoid repeated queries
- Remove dead `is not None` branch and redundant `or 0`
- Fix misleading docstrings on all_registrants scope
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant