Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions spp_gis_report/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,13 @@ Dependencies
Changelog
=========

19.0.2.1.0
~~~~~~~~~~

- feat: metric disaggregation in GIS reports (breakdown dimensions,
report helpers, wizard support) (re-land from #76; uses the
spp_metric_service breakdown API).

19.0.2.0.1
~~~~~~~~~~

Expand Down
3 changes: 2 additions & 1 deletion spp_gis_report/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "OpenSPP GIS Reports",
"version": "19.0.2.0.1",
"version": "19.0.2.1.0",
"category": "OpenSPP",
"summary": "Geographic visualization and reporting for social protection data",
"author": "OpenSPP.org, OpenSPP",
Expand All @@ -10,6 +10,7 @@
"depends": [
"spp_area",
"spp_gis",
"spp_metric_service",
"spp_registry",
"spp_vocabulary",
"spp_cel_domain",
Expand Down
30 changes: 13 additions & 17 deletions spp_gis_report/controllers/geojson.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,23 +100,19 @@ def list_reports(self):
for report in reports:
admin_levels = report.data_ids.mapped("area_level") if report.data_ids else []

report_list.append(
{
"code": report.code,
"name": report.name,
"category": report.category_id.name if report.category_id else None,
"last_refresh": (report.last_refresh.isoformat() if report.last_refresh else None),
"admin_levels_available": sorted(set(admin_levels)),
"has_disaggregation": any(
[
report.disaggregate_by_gender,
report.disaggregate_by_age,
report.disaggregate_by_disability,
report.disaggregate_by_tag_ids,
]
),
}
)
report_data = {
"code": report.code,
"name": report.name,
"category": report.category_id.name if report.category_id else None,
"last_refresh": (report.last_refresh.isoformat() if report.last_refresh else None),
"admin_levels_available": sorted(set(admin_levels)),
"has_disaggregation": bool(report.dimension_ids) or bool(report.disaggregate_by_tag_ids),
}
if report.dimension_ids:
report_data["disaggregation_dimensions"] = [
{"name": d.name, "label": d.label} for d in report.dimension_ids
]
report_list.append(report_data)

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

Expand Down
94 changes: 94 additions & 0 deletions spp_gis_report/migrations/19.0.2.1.0/post-migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Migrate legacy boolean disaggregation flags to dimension_ids.

Replaces the post_init_hook version, which only ran on fresh installs (where
there is nothing to migrate) and hardcoded the m2m relation table name. As a
post-migration script this runs on upgrade, and the relation/column names are
derived from the field so they cannot drift from Odoo's canonical naming.
"""

import logging

from psycopg2 import sql

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)

# Mapping: old boolean field name -> dimension technical name
_BOOL_TO_DIMENSION = {
"disaggregate_by_gender": "gender",
"disaggregate_by_age": "age_group",
"disaggregate_by_disability": "disability_status",
}


def migrate(cr, version):
"""Link reports that had legacy boolean flags set to the matching dimensions.

``version`` is the previously-installed module version, or a falsy value on
a fresh install (nothing to migrate). Logs a warning (does not crash) if a
dimension record is not found.
"""
if not version:
# Fresh install: the legacy boolean columns never existed.
return

# Only columns that still physically exist in the table can be read. After
# removing the boolean fields, Odoo leaves the obsolete columns in place, so
# they are still readable here on the first upgrade.
cr.execute(
"""
SELECT column_name FROM information_schema.columns
WHERE table_name = 'spp_gis_report'
AND column_name IN ('disaggregate_by_gender', 'disaggregate_by_age', 'disaggregate_by_disability')
"""
)
existing_columns = {row[0] for row in cr.fetchall()}
if not existing_columns:
_logger.info("No legacy boolean disaggregation columns found, skipping migration")
return

env = api.Environment(cr, SUPERUSER_ID, {})
Dimension = env["spp.demographic.dimension"]

# Derive the m2m relation/column names from the field so this migration
# cannot drift from Odoo's canonical (alphabetically sorted) table naming.
m2m = env["spp.gis.report"]._fields["dimension_ids"]
insert_stmt = sql.SQL("INSERT INTO {rel} ({col_report}, {col_dim}) VALUES (%s, %s) ON CONFLICT DO NOTHING").format(
rel=sql.Identifier(m2m.relation),
col_report=sql.Identifier(m2m.column1),
col_dim=sql.Identifier(m2m.column2),
)

for bool_field, dim_name in _BOOL_TO_DIMENSION.items():
if bool_field not in existing_columns:
continue

# bool_field is a trusted column name from _BOOL_TO_DIMENSION; quote it
# safely via psycopg2.sql.
query = sql.SQL("SELECT id FROM spp_gis_report WHERE {col} = true").format(col=sql.Identifier(bool_field))
cr.execute(query)
report_ids = [row[0] for row in cr.fetchall()]
if not report_ids:
continue

dimension = Dimension.search([("name", "=", dim_name)], limit=1)
if not dimension:
_logger.warning(
"Dimension '%s' not found, cannot migrate %d reports with %s=True",
dim_name,
len(report_ids),
bool_field,
)
continue

for report_id in report_ids:
cr.execute(insert_stmt, (report_id, dimension.id))

_logger.info(
"Migrated %d reports from %s=True to dimension '%s'",
len(report_ids),
bool_field,
dim_name,
)
10 changes: 9 additions & 1 deletion spp_gis_report/models/area_ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,15 @@ def get_gis_layers(self, view_id=None, view_type="gis", **options):
# Build legend info from thresholds for display purposes only.
thresholds = report.threshold_ids.sorted("sequence")
if thresholds:
layer_dict["report_legend"] = [{"color": t.color, "label": t.label} for t in thresholds]
layer_dict["report_legend"] = [
{
"color": t.color,
"label": t.label,
"min_value": t.min_value,
"max_value": t.max_value,
}
for t in thresholds
]
layer_dict["report_legend_title"] = report.name

# Build pre-computed features for report layer
Expand Down
Loading