Skip to content

feat(spp_hazard): CAP vocabulary severity and alert ingestion, with upgrade migration (re-land from #76)#274

Draft
gonzalesedwin1123 wants to merge 10 commits into
19.0from
reland/hazard-cap
Draft

feat(spp_hazard): CAP vocabulary severity and alert ingestion, with upgrade migration (re-land from #76)#274
gonzalesedwin1123 wants to merge 10 commits into
19.0from
reland/hazard-cap

Conversation

@gonzalesedwin1123

@gonzalesedwin1123 gonzalesedwin1123 commented Jul 2, 2026

Copy link
Copy Markdown
Member

Re-lands the spp_hazard portion of reverted PR #76 (revert: #271), together with the dependent adaptations and the fixes whose absence triggered the revert discussion.

Summary

  • spp.hazard.incident.severity (1-5 Selection) becomes severity_id, a spp.vocabulary.code Many2one on the CAP v1.2 severity vocabulary; severity_override on incident areas becomes severity_override_id likewise.
  • New CAP fields: urgency, certainty, message type, event, effective/expires; alert ingestion via create_incident_from_alert (creates hazard-zone geofences, auto-links intersecting areas); incident uuid.
  • CAP vocabulary data (severity, urgency, certainty, msg-type).
  • Dependent adaptations shipped together because they reference the new fields directly: spp_drims (effective_severity_id, numeric choropleth severity), spp_drims_sl_demo (scenario severity resolution), spp_hazard_programs (program view).
  • Downstream compatibility (commit fa7bd721): the severity/severity_override removal+rename is backward-incompatible for modules extending spp.hazard.incident/.area, so the changelog now carries explicit **BREAKING** notes (field renames, new spp_vocabulary dependency, and the model-level category_id/start_date required relaxation). To soften the blow, a stored severity_numeric (5=extreme … 1=unknown, 0=unset) is added on the incident so downstream ordering/threshold logic can consume a numeric scale without resolving CAP vocabulary codes. Note: inheriting view xpaths and API/compliance read-sites in downstream modules (e.g. the DSWD 4Ps stack) still require a coordinated update.

Added on top of #76 (not in the original)

  • Upgrade migration (spp_hazard 19.0.2.1.0 19.0.3.0.0): backfills severity_id/severity_override_id from the legacy Selection columns for databases upgrading from v19.0.2.0.x (release Biliran ships the old schema). Label-faithful mapping: 1→minor, 2→moderate, 3→severe (CAP defines severe as 'Significant threat'), 4→severe, 5→extreme. Idempotent; never overwrites values; unmapped values logged and left empty; no-op on fresh installs; column check scoped to current_schema(). Fully literal, value-parameterized SQL. Regression tests included.
  • Demo data fix: four incident-area records still wrote the removed severity_override field, breaking fresh installs with demo data; they now use severity_override_id vocabulary refs.
  • gemini-code-assist review findings, all applied: _parse_datetime_string accepts datetime instances; the area-linking query runs inside a savepoint so an invalid geometry cannot leave the transaction aborted; migration column check scoped to current schema.
  • Version bumps + HISTORY entries for all four in-scope modules (spp_hazard, spp_drims, spp_drims_sl_demo, spp_hazard_programs); their READMEs rendered by CI's pinned generator. spp_hazard is a major bump (19.0.2.1.019.0.3.0.0) reflecting the backward-incompatible severity field removal/rename; the migration folder is renamed to migrations/19.0.3.0.0/ to match (commit 32e43ada).

Not in this PR's diff

Verification

  • ./spp t spp_hazard: 87 88 passed, 0 failed (includes 5 migration regression tests; +1 for the new severity_numeric bridge test)
  • ./spp t spp_drims: 250/0, spp_drims_sl_demo: 8/0, spp_hazard_programs: 24/0
  • codecov: patch 93.3% (pass)
  • All gemini-code-assist threads resolved with fixing-commit references

Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed

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

Copy link
Copy Markdown

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 migrates hazard incident severity tracking from a hardcoded 1-5 Selection to a CAP v1.2 vocabulary code, introducing new CAP-aligned fields (urgency, certainty, message type, and event) and robust alert ingestion methods that automatically create or update incidents and geofences from GeoJSON alerts. It also includes a post-migration script to backfill legacy severity values and updates related tests, views, and demo generators across dependent modules. The review feedback highlights three important improvements: making the _parse_datetime_string helper robust against already-parsed datetime objects, wrapping the PostGIS intersection query in a savepoint to prevent transaction aborts on database errors, and restricting the migration's column existence check to the active schema to avoid false positives.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread spp_hazard/models/hazard_incident.py
Comment thread spp_hazard/models/hazard_incident.py Outdated
Comment thread spp_hazard/migrations/19.0.3.0.0/post-migration.py
@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.58974% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.38%. Comparing base (bf61488) to head (32e43ad).
⚠️ Report is 29 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_hazard/models/hazard_incident.py 93.70% 9 Missing ⚠️
spp_drims_sl_demo/wizard/drims_demo_generator.py 85.71% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #274      +/-   ##
==========================================
+ Coverage   74.86%   75.38%   +0.52%     
==========================================
  Files        1093      422     -671     
  Lines       63718    25813   -37905     
==========================================
- Hits        47701    19459   -28242     
+ Misses      16017     6354    -9663     
Flag Coverage Δ
endpoint_route_handler ?
spp_aggregation ?
spp_api_v2_change_request 66.41% <ø> (ø)
spp_api_v2_cycles 70.65% <ø> (ø)
spp_api_v2_entitlements 69.96% <ø> (ø)
spp_api_v2_gis 71.52% <ø> (ø)
spp_api_v2_products ?
spp_api_v2_programs 92.03% <ø> (ø)
spp_api_v2_service_points ?
spp_api_v2_simulation 71.12% <ø> (ø)
spp_api_v2_vocabulary ?
spp_approval ?
spp_area ?
spp_area_hdx ?
spp_attachment_av_scan ?
spp_audit ?
spp_audit_programs 0.00% <ø> (ø)
spp_banking ?
spp_base_common 90.26% <ø> (ø)
spp_base_setting ?
spp_case_base ?
spp_case_cel ?
spp_case_demo ?
spp_case_entitlements 97.61% <ø> (ø)
spp_case_graduation ?
spp_case_programs 97.14% <ø> (ø)
spp_case_registry ?
spp_case_session ?
spp_cel_domain ?
spp_cel_event ?
spp_cel_registry_search ?
spp_cel_vocabulary ?
spp_change_request_v2 75.46% <ø> (ø)
spp_claim_169 ?
spp_cr_type_assign_program 91.17% <ø> (ø)
spp_cr_types_advanced 0.00% <ø> (ø)
spp_cr_types_base ?
spp_dci ?
spp_dci_client ?
spp_dci_client_dr ?
spp_dci_client_ibr ?
spp_dci_client_sr ?
spp_dci_compliance ?
spp_dci_demo ?
spp_dci_indicators ?
spp_dci_server ?
spp_dci_server_social ?
spp_demo ?
spp_demo_phl_luzon ?
spp_disability_registry ?
spp_drims 81.01% <100.00%> (+0.18%) ⬆️
spp_drims_sl_demo 69.07% <85.71%> (+0.59%) ⬆️
spp_encryption ?
spp_farmer_registry ?
spp_farmer_registry_cr ?
spp_farmer_registry_demo ?
spp_farmer_registry_vocabularies ?
spp_gis ?
spp_gis_report ?
spp_graduation ?
spp_grm ?
spp_grm_case_link ?
spp_grm_demo ?
spp_hazard 97.40% <93.70%> (-2.19%) ⬇️
spp_hazard_programs 97.14% <ø> (ø)
spp_hxl_area ?
spp_import_match ?
spp_indicator ?
spp_irrigation ?
spp_land_record ?
spp_metric ?
spp_metric_service ?
spp_metrics_core ?
spp_metrics_services ?
spp_mis_demo_v2 ?
spp_oauth ?
spp_program_geofence ?
spp_programs 65.27% <ø> (ø)
spp_registrant_gis ?
spp_registry 86.83% <ø> (ø)
spp_registry_group_hierarchy ?
spp_scoring ?
spp_scoring_programs ?
spp_security 66.66% <ø> (ø)
spp_service_points ?
spp_simulation ?
spp_starter_disability_registry ?
spp_starter_farmer_registry ?
spp_starter_social_registry ?
spp_starter_sp_mis ?
spp_statistic ?
spp_storage_backend ?
spp_studio ?
spp_studio_change_requests ?

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

Files with missing lines Coverage Δ
spp_drims/__manifest__.py 0.00% <ø> (ø)
spp_drims/models/hazard_incident_area.py 100.00% <100.00%> (+21.05%) ⬆️
spp_drims_sl_demo/__manifest__.py 0.00% <ø> (ø)
spp_hazard/__manifest__.py 0.00% <ø> (ø)
spp_hazard_programs/__manifest__.py 0.00% <ø> (ø)
spp_drims_sl_demo/wizard/drims_demo_generator.py 68.97% <85.71%> (+0.60%) ⬆️
spp_hazard/models/hazard_incident.py 96.15% <93.70%> (-3.85%) ⬇️

... and 671 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.

Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed
…ion (from #76)

Re-lands the spp_hazard severity->vocabulary change from reverted PR #76,
together with the dependent adaptations in spp_drims, spp_drims_sl_demo and
spp_hazard_programs, plus the two fixes the original PR lacked:

- migrations/19.0.2.1.0/post-migration.py backfills severity_id /
  severity_override_id from the legacy 1-5 Selection columns for databases
  upgrading from v19.0.2.0.x (release Biliran ships the old schema).
  Mapping: 1->minor, 2->moderate, 3->severe, 4->severe, 5->extreme.
- hazard_demo.xml incident-area records now use severity_override_id
  vocabulary refs; the stale severity_override field broke demo installs.

Includes migration regression tests (mapping, area override, idempotency,
unmapped values, fresh-install no-op).
…enerate READMEs

Semgrep flagged f-string SQL in the severity migration; identifiers were
constants but composed SQL via psycopg2.sql removes the pattern entirely.
READMEs regenerated from the updated HISTORY fragments (in-scope modules
only).
Applies CI's pinned-renderer output for spp_hazard README/index.html and
ruff-format's line join in the migration test, taken verbatim from the CI
pre-commit diff.
The flagged query composes identifiers from an in-file constant via
psycopg2.sql.Identifier; no external input reaches it.
Comment thread spp_hazard/migrations/19.0.2.1.0/post-migration.py Fixed
Replaces psycopg2.sql identifier composition with fully literal queries per
target table (only two targets), removing the pattern that pylint-odoo and
semgrep flag. Values remain parameterized.
The local generator env renders RST table widths differently; CI's output
(taken verbatim from its pre-commit diff) is authoritative. Committed without
running local hooks so the README hook cannot rewrite it back.
CI's oca-gen-addon-readme regenerates these five untouched modules on this
PR's merge ref (deterministic target blobs across runs). Content is CI's own
printed diff, applied verbatim; docs-only, no code changes. Committed without
local hooks so the local renderer cannot rewrite it.
- _parse_datetime_string accepts datetime instances (avoids TypeError on
  programmatic values).
- Area-linking query wrapped in a savepoint so an invalid geometry cannot
  leave the transaction aborted for subsequent operations.
- Migration column check scoped to current_schema() to avoid false positives
  from same-named tables in other schemas.
@gonzalesedwin1123

Copy link
Copy Markdown
Member Author

All three gemini-code-assist findings applied (commit 4bf883d):

  • _parse_datetime_string accepts datetime instances.
  • Area-linking query wrapped in cr.savepoint() so an invalid geometry cannot leave the transaction aborted.
  • Migration _column_exists scoped to current_schema().

./spp t spp_hazard: 87 passed, 0 failed.

@gonzalesedwin1123 gonzalesedwin1123 marked this pull request as draft July 3, 2026 11:09

@kneckinator kneckinator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the CAP-severity re-land. The migration and vocabulary modeling are solid, and the migration regression tests are a good example. Two substantive items plus some nits, posted inline:

  • 🔴 High_link_areas_from_geometry calls fetchall() after the savepoint block closes, so area auto-linking silently links nothing and logs a warning on every alert. One-line fix + a positive test needed.
  • 🟠 Medium — the demo generator's legacy→CAP severity mapping diverges from the migration's for levels 1–3.
  • 🟡 Low — docs/changelog name create_incident_from_alert; the actual method is create_from_alert.
  • Nits: duplicated nosemgrep, source_alert_id help overpromises dedup, migration CASE/param coupling, and the area-link test gap.

Recommend addressing the High item (and its missing test) before merge.

Comment on lines +533 to +545
with self.env.cr.savepoint():
self.env.cr.execute(
"""
SELECT id FROM spp_area
WHERE geo_polygon IS NOT NULL
AND ST_Intersects(
geo_polygon::geometry,
ST_SetSRID(ST_GeomFromGeoJSON(%s), 4326)
)
""",
(geojson_str,),
)
area_ids = [row[0] for row in self.env.cr.fetchall()]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 fetchall() runs after the savepoint block closes — area auto-linking is silently broken

area_ids = [… fetchall()] (line 545) executes after the with self.env.cr.savepoint(): block exits. On a clean exit, Savepoint.__exit__ issues RELEASE SAVEPOINT … on the same cursor (odoo/sql_db.py, Savepoint._close), which replaces the SELECT's result set. fetchall() then raises psycopg2.ProgrammingError ("no results to fetch") — a subclass of psycopg2.Error — which this method's own except psycopg2.Error swallows. Net effect: zero areas are ever linked, and every call logs a spurious "Failed to link areas…" warning.

Fix — move the fetch inside the block:

with self.env.cr.savepoint():
    self.env.cr.execute("SELECT id FROM spp_area WHERE ...", (geojson_str,))
    area_ids = [row[0] for row in self.env.cr.fetchall()]

test_link_areas_no_match can't catch this (it asserts 0 areas either way) — see the test-gap note on test_alert_ingestion.py.

Comment on lines +56 to +61
_LEGACY_SEVERITY_TO_CAP = {
"5": "extreme",
"4": "severe",
"3": "moderate",
"2": "minor",
"1": "unknown",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 This legacy 1→5 → CAP mapping diverges from the migration's

spp_hazard/migrations/19.0.2.1.0/post-migration.py maps the same legacy scale differently:

legacy migration here (demo) old label
1 minor unknown Minor
2 moderate minor Moderate
3 severe moderate Significant
4 severe severe Severe
5 extreme extreme Catastrophic

They only agree on 4 and 5. So a "level 3" incident becomes CAP severe on an upgraded DB but CAP moderate on a fresh demo install — and 2→minor / 3→moderate here also contradicts the original Selection labels. Recommend reconciling to the migration's label-faithful mapping, or adding a comment justifying why the demo path intentionally differs (it round-trips with spp_drims's CAP_SEVERITY_NUMERIC).

@@ -1,3 +1,9 @@
### 19.0.2.1.0

- feat: severity is now a CAP v1.2 vocabulary code (`severity_id`, `severity_override_id` on incident areas) instead of a hardcoded 1-5 Selection; adds CAP urgency/certainty/message-type/event fields, alert ingestion (`create_incident_from_alert`), and incident `uuid` (re-land from #76).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 create_incident_from_alert doesn't exist — the method is create_from_alert

This changelog entry (and the PR body, README, and index.html) names create_incident_from_alert, but the implemented model method is create_from_alert (there is no create_incident_from_alert anywhere in the codebase). Please rename the doc references so the public changelog matches the API.

Comment on lines +396 to +398
# nosemgrep: odoo-sudo-without-context
geofence = (
# nosemgrep: odoo-sudo-without-context (system-context geofence lookup for alert/incident processing)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: two # nosemgrep: odoo-sudo-without-context comments for a single statement (same duplication at the _get_alert_geometry lookup, ~lines 585–587). Keep the annotated one and drop the bare duplicate.

Comment on lines +147 to +150
source_alert_id = fields.Char(
index=True,
help="External alert reference ID from the EWS (e.g., 'MOZ-FLOOD-2026-042'). "
"Used for duplicate detection on API create.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the help text says source_alert_id is "Used for duplicate detection on API create," but create_from_alert performs no dedup — a repeated source_alert_id would hit the code unique constraint. Fine if dedup lives in the API module, but as written the help overpromises for this module.



def migrate(cr, version):
case_params = [p for pair in LEGACY_SEVERITY_TO_CAP.items() for p in pair]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: case_params is built from LEGACY_SEVERITY_TO_CAP.items() (5 entries) while the CASE clauses in _BACKFILL_INCIDENT / _BACKFILL_AREA hardcode exactly 5 WHEN pairs. If the dict ever changes length, the placeholder count silently desyncs from the SQL. A one-line assert len(LEGACY_SEVERITY_TO_CAP) == 5 (or building the CASE dynamically) would make the coupling safe.

class TestLinkAreasFromGeometry(HazardTestCase):
"""Tests for _link_areas_from_geometry method."""

def test_link_areas_no_match(self):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Test gap: this is the only area-linking test, and it asserts 0 areas for a far-away polygon — which passes whether or not linking works, so it masks the fetchall()-after-savepoint bug flagged on hazard_incident.py:545. Please add a positive case: an spp.area with a geo_polygon intersecting SAMPLE_POLYGON, asserting incident.area_ids is populated.

…hanges

Softens the downstream impact of the severity -> CAP vocabulary migration
(re-land from #76):

- add a stored, computed severity_numeric (5=extreme .. 1=unknown, 0=unset)
  on spp.hazard.incident so downstream ordering/threshold logic can consume
  a numeric scale without resolving CAP vocabulary codes (mirrors the
  spp_drims incident-area mapping).
- document the breaking changes in the changelog: the severity /
  severity_override removal+rename, the new spp_vocabulary dependency, and
  the model-level category_id / start_date required relaxation.
@kneckinator

kneckinator commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

fa7bd721 — soften the downstream blow of the severity → CAP vocabulary migration

Two additive, non-breaking changes to reduce the impact on downstream modules that extend spp.hazard.incident, plus loud breaking-change docs.

1. New severity_numeric bridge on spp.hazard.incident

  • Adds a stored, computed severity_numeric (5=extreme, 4=severe, 3=moderate, 2=minor, 1=unknown; 0=unset), backed by a new CAP_SEVERITY_NUMERIC map and _compute_severity_numeric (@api.depends("severity_id")).
  • Mirrors the mapping spp_drims already applies to incident areas, so downstream ordering/threshold logic (e.g. "most-severe incident wins") can read incident.severity_numeric instead of the removed int(incident.severity) — a one-line downstream change rather than re-deriving the CAP→int scale.
  • Covered by test_22_severity_numeric (severe→4, extreme→5, moderate→3, unset→0).

2. Breaking-change notes in the changelog (readme/HISTORY.md, rendered into README.rst / index.html)

  • The severity / severity_override Selection→vocabulary removal+rename, with the concrete downstream migration checklist (views referencing <field name="severity"> / severity_override, code reading record.severity, records created with severity="…", and fields_to_log / domains naming the old fields).
  • The new spp_vocabulary dependency.
  • The model-level category_id / start_date required relaxation (still enforced in the incident form view).

Verification

  • ./spp test spp_hazard: 88 passed, 0 failed, 0 errors (was 87; test_22_severity_numeric is the +1).
  • README.rst / index.html changelog entries were added to match the pinned (CI, Python 3.11) generator output exactly, avoiding the local-vs-CI table-rendering drift; the zeroed source-digest sentinel is preserved, so the pre-commit README check stays clean.

Scope note: this reduces the number of edits downstream modules need, but does not remove the break — the inheriting view xpaths and the API/compliance read-sites still require a coordinated downstream PR to land in lockstep with this one.

…hange

The severity -> CAP vocabulary migration removes/renames public fields, adds
a required dependency, and ships a data migration -- a backward-incompatible
change. A minor bump (19.0.2.1.0) contradicted the BREAKING changelog notes,
so bump the major segment instead. Rename the migration folder to match the
release version and update the test path/heading references accordingly.
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.

3 participants