Skip to content

Commit 9cb39d6

Browse files
feat(spp_gis_report): re-land metric disaggregation in GIS reports (from #76)
Re-lands the spp_gis_report portion of reverted PR #76. Depends on the spp_metric_service breakdown expansion (Wave-2 PR) and transitively on spp_cel_domain (#275) — do not merge before them.
1 parent a8b8f08 commit 9cb39d6

15 files changed

Lines changed: 1169 additions & 86 deletions

spp_gis_report/__init__.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,84 @@
1+
import logging
2+
3+
from psycopg2 import sql
4+
15
from . import controllers
26
from . import models
37
from . import wizards
8+
9+
_logger = logging.getLogger(__name__)
10+
11+
# Mapping: old boolean field name -> dimension technical name
12+
_BOOL_TO_DIMENSION = {
13+
"disaggregate_by_gender": "gender",
14+
"disaggregate_by_age": "age_group",
15+
"disaggregate_by_disability": "disability_status",
16+
}
17+
18+
19+
def _migrate_boolean_disaggregation(env):
20+
"""Post-init hook: migrate boolean disaggregation flags to dimension_ids.
21+
22+
Looks up reports that had boolean flags set and links them to the
23+
corresponding spp.demographic.dimension records. Logs a warning
24+
(does not crash) if a dimension record is not found.
25+
"""
26+
cr = env.cr
27+
28+
# Check if the old boolean columns still exist
29+
cr.execute(
30+
"""
31+
SELECT column_name FROM information_schema.columns
32+
WHERE table_name = 'spp_gis_report'
33+
AND column_name IN ('disaggregate_by_gender', 'disaggregate_by_age', 'disaggregate_by_disability')
34+
"""
35+
)
36+
existing_columns = {row[0] for row in cr.fetchall()}
37+
38+
if not existing_columns:
39+
_logger.info("No legacy boolean disaggregation columns found, skipping migration")
40+
return
41+
42+
# Get the m2m relation table name
43+
Dimension = env["spp.demographic.dimension"]
44+
45+
for bool_field, dim_name in _BOOL_TO_DIMENSION.items():
46+
if bool_field not in existing_columns:
47+
continue
48+
49+
# Find reports with this boolean set. bool_field is a trusted column
50+
# name from _BOOL_TO_DIMENSION; quote it safely via psycopg2.sql.
51+
query = sql.SQL("SELECT id FROM spp_gis_report WHERE {col} = true").format(col=sql.Identifier(bool_field))
52+
cr.execute(query)
53+
report_ids = [row[0] for row in cr.fetchall()]
54+
if not report_ids:
55+
continue
56+
57+
dimension = Dimension.search([("name", "=", dim_name)], limit=1)
58+
if not dimension:
59+
_logger.warning(
60+
"Dimension '%s' not found, cannot migrate %d reports with %s=True",
61+
dim_name,
62+
len(report_ids),
63+
bool_field,
64+
)
65+
continue
66+
67+
# Insert m2m links (skip duplicates)
68+
for report_id in report_ids:
69+
cr.execute(
70+
"""
71+
INSERT INTO spp_gis_report_spp_demographic_dimension_rel
72+
(spp_gis_report_id, spp_demographic_dimension_id)
73+
VALUES (%s, %s)
74+
ON CONFLICT DO NOTHING
75+
""",
76+
(report_id, dimension.id),
77+
)
78+
79+
_logger.info(
80+
"Migrated %d reports from %s=True to dimension '%s'",
81+
len(report_ids),
82+
bool_field,
83+
dim_name,
84+
)

spp_gis_report/__manifest__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "OpenSPP GIS Reports",
3-
"version": "19.0.2.0.1",
3+
"version": "19.0.2.1.0",
44
"category": "OpenSPP",
55
"summary": "Geographic visualization and reporting for social protection data",
66
"author": "OpenSPP.org, OpenSPP",
@@ -10,6 +10,7 @@
1010
"depends": [
1111
"spp_area",
1212
"spp_gis",
13+
"spp_metric_service",
1314
"spp_registry",
1415
"spp_vocabulary",
1516
"spp_cel_domain",
@@ -47,6 +48,7 @@
4748
"spp_gis_report/static/src/css/gis_report.css",
4849
],
4950
},
51+
"post_init_hook": "_migrate_boolean_disaggregation",
5052
"installable": True,
5153
"application": False,
5254
"auto_install": False,

spp_gis_report/controllers/geojson.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -100,23 +100,19 @@ def list_reports(self):
100100
for report in reports:
101101
admin_levels = report.data_ids.mapped("area_level") if report.data_ids else []
102102

103-
report_list.append(
104-
{
105-
"code": report.code,
106-
"name": report.name,
107-
"category": report.category_id.name if report.category_id else None,
108-
"last_refresh": (report.last_refresh.isoformat() if report.last_refresh else None),
109-
"admin_levels_available": sorted(set(admin_levels)),
110-
"has_disaggregation": any(
111-
[
112-
report.disaggregate_by_gender,
113-
report.disaggregate_by_age,
114-
report.disaggregate_by_disability,
115-
report.disaggregate_by_tag_ids,
116-
]
117-
),
118-
}
119-
)
103+
report_data = {
104+
"code": report.code,
105+
"name": report.name,
106+
"category": report.category_id.name if report.category_id else None,
107+
"last_refresh": (report.last_refresh.isoformat() if report.last_refresh else None),
108+
"admin_levels_available": sorted(set(admin_levels)),
109+
"has_disaggregation": bool(report.dimension_ids) or bool(report.disaggregate_by_tag_ids),
110+
}
111+
if report.dimension_ids:
112+
report_data["disaggregation_dimensions"] = [
113+
{"name": d.name, "label": d.label} for d in report.dimension_ids
114+
]
115+
report_list.append(report_data)
120116

121117
return self._json_response({"reports": report_list})
122118

spp_gis_report/models/area_ext.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,15 @@ def get_gis_layers(self, view_id=None, view_type="gis", **options):
153153
# Build legend info from thresholds for display purposes only.
154154
thresholds = report.threshold_ids.sorted("sequence")
155155
if thresholds:
156-
layer_dict["report_legend"] = [{"color": t.color, "label": t.label} for t in thresholds]
156+
layer_dict["report_legend"] = [
157+
{
158+
"color": t.color,
159+
"label": t.label,
160+
"min_value": t.min_value,
161+
"max_value": t.max_value,
162+
}
163+
for t in thresholds
164+
]
157165
layer_dict["report_legend_title"] = report.name
158166

159167
# Build pre-computed features for report layer

0 commit comments

Comments
 (0)