diff --git a/spp_drims/README.rst b/spp_drims/README.rst index 2ab588c6c..817b42add 100644 --- a/spp_drims/README.rst +++ b/spp_drims/README.rst @@ -179,6 +179,14 @@ Dependencies Changelog ========= +19.0.2.1.0 +~~~~~~~~~~ + +- feat: incident areas expose ``effective_severity_id`` (area override + or incident severity) and a numeric 1-5 ``severity_numeric`` derived + from CAP severity codes for choropleth visualization (re-land from + #76, follows the spp_hazard severity vocabulary change). + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_drims/__manifest__.py b/spp_drims/__manifest__.py index 5cd7472c7..c41f94e22 100644 --- a/spp_drims/__manifest__.py +++ b/spp_drims/__manifest__.py @@ -5,7 +5,7 @@ "and distribution tracking. Links to hazard incidents with multi-tier " "approval workflows and warehouse operations.", "category": "OpenSPP/Inventory", - "version": "19.0.2.0.0", + "version": "19.0.2.1.0", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_drims/models/hazard_incident_area.py b/spp_drims/models/hazard_incident_area.py index 4ecfa632e..e3f4d62f6 100644 --- a/spp_drims/models/hazard_incident_area.py +++ b/spp_drims/models/hazard_incident_area.py @@ -1,6 +1,16 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models +# Map CAP severity vocabulary codes to a 1-5 numeric scale (higher = more +# severe) for choropleth map visualization. +CAP_SEVERITY_NUMERIC = { + "extreme": 5, + "severe": 4, + "moderate": 3, + "minor": 2, + "unknown": 1, +} + class HazardIncidentArea(models.Model): """Extend incident area with GIS polygon for map visualization.""" @@ -17,17 +27,12 @@ class HazardIncidentArea(models.Model): ) # Computed severity that falls back to incident severity - effective_severity = fields.Selection( - [ - ("1", "Level 1 - Minor"), - ("2", "Level 2 - Moderate"), - ("3", "Level 3 - Significant"), - ("4", "Level 4 - Severe"), - ("5", "Level 5 - Catastrophic"), - ], + effective_severity_id = fields.Many2one( + "spp.vocabulary.code", + string="Effective Severity", compute="_compute_effective_severity", store=True, - help="Area-specific severity or inherited from incident", + help="Area-specific severity override, or inherited from the incident", ) # Additional fields for GIS popup display and grouping @@ -61,14 +66,14 @@ class HazardIncidentArea(models.Model): help="Numeric severity (1-5) for choropleth map visualization", ) - @api.depends("severity_override", "incident_id.severity") + @api.depends("severity_override_id", "incident_id.severity_id") def _compute_effective_severity(self): - """Compute effective severity from override or incident default.""" + """Compute effective severity from area override or incident default.""" for rec in self: - rec.effective_severity = rec.severity_override or rec.incident_id.severity or "3" + rec.effective_severity_id = rec.severity_override_id or rec.incident_id.severity_id - @api.depends("effective_severity") + @api.depends("effective_severity_id") def _compute_severity_numeric(self): - """Compute numeric severity from selection value for choropleth.""" + """Compute numeric severity from the CAP severity code for choropleth.""" for rec in self: - rec.severity_numeric = int(rec.effective_severity) if rec.effective_severity else 3 + rec.severity_numeric = CAP_SEVERITY_NUMERIC.get(rec.effective_severity_id.code, 0) diff --git a/spp_drims/readme/HISTORY.md b/spp_drims/readme/HISTORY.md index 4aaf9afef..eb677e216 100644 --- a/spp_drims/readme/HISTORY.md +++ b/spp_drims/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.1.0 + +- feat: incident areas expose `effective_severity_id` (area override or incident severity) and a numeric 1-5 `severity_numeric` derived from CAP severity codes for choropleth visualization (re-land from #76, follows the spp_hazard severity vocabulary change). + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_drims/static/description/index.html b/spp_drims/static/description/index.html index dbf283ed0..67c6f8b92 100644 --- a/spp_drims/static/description/index.html +++ b/spp_drims/static/description/index.html @@ -565,6 +565,15 @@

Changelog

+

19.0.2.1.0

+ +
+

19.0.2.0.0

+

19.0.2.0.1

+ +
+

19.0.2.0.0

+

19.0.2.1.0

+ +
+

19.0.2.0.2

-
+

19.0.2.0.1

-
+

19.0.2.0.0

  • Initial migration to OpenSPP2
  • diff --git a/spp_hazard/tests/__init__.py b/spp_hazard/tests/__init__.py index 9956de3b9..746c00317 100644 --- a/spp_hazard/tests/__init__.py +++ b/spp_hazard/tests/__init__.py @@ -5,4 +5,6 @@ from . import test_hazard_impact from . import test_hazard_impact_type from . import test_geofence +from . import test_alert_ingestion from . import test_registrant +from . import test_migration_severity diff --git a/spp_hazard/tests/common.py b/spp_hazard/tests/common.py index baef332d1..329497408 100644 --- a/spp_hazard/tests/common.py +++ b/spp_hazard/tests/common.py @@ -53,6 +53,17 @@ def setUpClass(cls): } ) + # Resolve CAP vocabulary codes (loaded from data/vocabulary_cap.xml) + VocabCode = cls.env["spp.vocabulary.code"] + cls.severity_extreme = VocabCode.get_code("urn:oasis:names:tc:cap:severity", "extreme") + cls.severity_severe = VocabCode.get_code("urn:oasis:names:tc:cap:severity", "severe") + cls.severity_moderate = VocabCode.get_code("urn:oasis:names:tc:cap:severity", "moderate") + cls.severity_minor = VocabCode.get_code("urn:oasis:names:tc:cap:severity", "minor") + cls.urgency_immediate = VocabCode.get_code("urn:oasis:names:tc:cap:urgency", "immediate") + cls.certainty_observed = VocabCode.get_code("urn:oasis:names:tc:cap:certainty", "observed") + cls.msg_type_alert = VocabCode.get_code("urn:oasis:names:tc:cap:msg-type", "alert") + cls.msg_type_cancel = VocabCode.get_code("urn:oasis:names:tc:cap:msg-type", "cancel") + # Create impact type cls.impact_type_displacement = cls.env["spp.hazard.impact.type"].create( { diff --git a/spp_hazard/tests/test_alert_ingestion.py b/spp_hazard/tests/test_alert_ingestion.py new file mode 100644 index 000000000..c349d0cf6 --- /dev/null +++ b/spp_hazard/tests/test_alert_ingestion.py @@ -0,0 +1,364 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +"""Tests for alert ingestion business logic (create_from_alert, update_from_alert, etc.).""" + +import logging + +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +from .common import HazardTestCase + +_logger = logging.getLogger(__name__) + +# Sample polygon covering a small area +SAMPLE_POLYGON = { + "type": "Polygon", + "coordinates": [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ] + ], +} + +SAMPLE_POLYGON_2 = { + "type": "Polygon", + "coordinates": [ + [ + [102.0, 2.0], + [103.0, 2.0], + [103.0, 3.0], + [102.0, 3.0], + [102.0, 2.0], + ] + ], +} + + +@tagged("post_install", "-at_install") +class TestCreateFromAlert(HazardTestCase): + """Tests for create_from_alert method.""" + + def test_create_from_alert_happy_path(self): + """Create incident from alert with all properties.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Flood", + "headline": "Major Flood in Region A", + "severity": "extreme", + "urgency": "immediate", + "certainty": "observed", + "effective": "2026-04-01T00:00:00Z", + "expires": "2026-04-15T00:00:00Z", + "source": "INAM Mozambique", + "source_alert_id": "MOZ-FLOOD-2026-001", + "cap_msg_type": "alert", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + self.assertTrue(incident) + self.assertEqual(incident.name, "Major Flood in Region A") + self.assertEqual(incident.cap_event, "Flood") + self.assertEqual(incident.severity_id, self.severity_extreme) + self.assertEqual(incident.cap_urgency_id, self.urgency_immediate) + self.assertEqual(incident.cap_certainty_id, self.certainty_observed) + self.assertEqual(incident.source, "INAM Mozambique") + self.assertEqual(incident.source_alert_id, "MOZ-FLOOD-2026-001") + self.assertTrue(incident.uuid) + # Auto-populated dates + self.assertTrue(incident.start_date) + self.assertTrue(incident.effective) + + def test_create_from_alert_creates_geofence(self): + """create_from_alert creates a hazard_zone geofence linked to the incident.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Storm", + "headline": "Storm Alert", + "source_alert_id": "STORM-001", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + geofence = self.env["spp.gis.geofence"].search( + [("incident_id", "=", incident.id), ("geofence_type", "=", "hazard_zone")] + ) + self.assertEqual(len(geofence), 1) + self.assertEqual(geofence.created_from, "api") + + def test_create_from_alert_resolves_category(self): + """create_from_alert resolves event string to hazard category by name.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Typhoon", + "headline": "Typhoon Alert", + "source_alert_id": "TYP-001", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + self.assertEqual(incident.category_id, self.category_typhoon) + self.assertEqual(incident.cap_event, "Typhoon") + + def test_create_from_alert_missing_category_ok(self): + """create_from_alert works when event doesn't match any category.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Alien Invasion", + "headline": "Unknown Event", + "source_alert_id": "UNK-001", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + self.assertFalse(incident.category_id) + self.assertEqual(incident.cap_event, "Alien Invasion") + + def test_create_from_alert_auto_code(self): + """create_from_alert auto-generates code from source_alert_id.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Flood", + "headline": "Auto Code Test", + "source_alert_id": "AUTO-CODE-001", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + self.assertEqual(incident.code, "AUTO-CODE-001") + + def test_create_from_alert_bad_geometry_raises(self): + """create_from_alert raises ValidationError for Point geometry.""" + Incident = self.env["spp.hazard.incident"] + bad_geom = {"type": "Point", "coordinates": [100.0, 0.0]} + properties = { + "event": "Flood", + "headline": "Bad Geom", + "source_alert_id": "BAD-GEOM-001", + } + + with self.assertRaises(ValidationError): + Incident.create_from_alert(bad_geom, properties) + + def test_create_from_alert_minimal_properties(self): + """create_from_alert works with only event and headline.""" + Incident = self.env["spp.hazard.incident"] + properties = { + "event": "Drought", + "headline": "Minimal Alert", + "source_alert_id": "MIN-001", + } + + incident = Incident.create_from_alert(SAMPLE_POLYGON, properties) + + self.assertTrue(incident) + self.assertEqual(incident.name, "Minimal Alert") + self.assertFalse(incident.severity_id) + self.assertFalse(incident.cap_urgency_id) + + +@tagged("post_install", "-at_install") +class TestUpdateFromAlert(HazardTestCase): + """Tests for update_from_alert method.""" + + def setUp(self): + super().setUp() + Incident = self.env["spp.hazard.incident"] + self.incident = Incident.create_from_alert( + SAMPLE_POLYGON, + { + "event": "Flood", + "headline": "Initial Flood Alert", + "severity": "moderate", + "source_alert_id": "UPD-001", + }, + ) + + def test_update_properties_only(self): + """update_from_alert updates properties without geometry change.""" + self.incident.update_from_alert( + None, + { + "headline": "Updated Flood Alert", + "severity": "extreme", + }, + ) + + self.assertEqual(self.incident.name, "Updated Flood Alert") + self.assertEqual(self.incident.severity_id, self.severity_extreme) + + def test_update_with_new_geometry(self): + """update_from_alert updates the linked geofence geometry.""" + self.incident.update_from_alert( + SAMPLE_POLYGON_2, + {"headline": "Flood Moved East"}, + ) + + geofence = self.env["spp.gis.geofence"].search( + [("incident_id", "=", self.incident.id), ("geofence_type", "=", "hazard_zone")], + limit=1, + ) + self.assertTrue(geofence) + self.assertEqual(self.incident.name, "Flood Moved East") + + # Verify the geofence geometry was updated to SAMPLE_POLYGON_2. + # SAMPLE_POLYGON_2 starts at longitude 102, which is distinct from + # SAMPLE_POLYGON which starts at longitude 100. + self.assertTrue(geofence.geometry, "Geofence geometry must not be empty after update") + from shapely.geometry import mapping + + stored_geom = mapping(geofence.geometry) + coords = stored_geom.get("coordinates", [[[]]]) + outer_ring = coords[0] + longitudes = [pt[0] for pt in outer_ring] + self.assertTrue( + any(lon >= 102.0 for lon in longitudes), + f"Expected geometry with longitudes >= 102 (SAMPLE_POLYGON_2), got: {longitudes}", + ) + + def test_update_cancel_closes_incident(self): + """update_from_alert with cap_msg_type=cancel closes the incident.""" + self.incident.update_from_alert( + None, + {"cap_msg_type": "cancel"}, + ) + + self.assertEqual(self.incident.status, "closed") + + +@tagged("post_install", "-at_install") +class TestToGeojson(HazardTestCase): + """Tests for to_geojson and related methods.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + Incident = cls.env["spp.hazard.incident"] + cls.incident = Incident.create_from_alert( + SAMPLE_POLYGON, + { + "event": "Flood", + "headline": "GeoJSON Test Flood", + "severity": "severe", + "urgency": "immediate", + "source": "Test Agency", + "source_alert_id": "GEO-001", + }, + ) + + def test_to_geojson_structure(self): + """to_geojson returns valid GeoJSON Feature.""" + feature = self.incident.to_geojson() + + self.assertEqual(feature["type"], "Feature") + self.assertEqual(feature["id"], self.incident.uuid) + self.assertIsNotNone(feature["geometry"]) + self.assertEqual(feature["geometry"]["type"], "Polygon") + self.assertIsInstance(feature["properties"], dict) + + def test_to_geojson_properties(self): + """to_geojson includes all CAP-aligned properties.""" + props = self.incident.to_geojson()["properties"] + + self.assertEqual(props["code"], "GEO-001") + self.assertEqual(props["event"], "Flood") + self.assertEqual(props["severity"], "severe") + self.assertEqual(props["urgency"], "immediate") + self.assertEqual(props["headline"], "GeoJSON Test Flood") + self.assertEqual(props["source"], "Test Agency") + self.assertEqual(props["source_alert_id"], "GEO-001") + + def test_to_geojson_without_geofence(self): + """to_geojson returns null geometry if no geofence linked.""" + incident = self.env["spp.hazard.incident"].create( + { + "name": "No Geofence Incident", + "code": "NOGEO-001", + "start_date": "2026-01-01", + } + ) + + feature = incident.to_geojson() + + self.assertEqual(feature["type"], "Feature") + self.assertIsNone(feature["geometry"]) + + def test_to_geojson_multiple_geofences_uses_first(self): + """to_geojson uses the first (oldest) geofence geometry.""" + # Create a second geofence for the same incident + self.env["spp.gis.geofence"].create_from_geojson( + geojson_str=SAMPLE_POLYGON_2, + name="Second zone", + geofence_type="hazard_zone", + created_from="api", + incident_id=self.incident.id, + ) + + feature = self.incident.to_geojson() + + # Should use first geofence (SAMPLE_POLYGON), not second + self.assertIsNotNone(feature["geometry"]) + + +@tagged("post_install", "-at_install") +class TestLinkAreasFromGeometry(HazardTestCase): + """Tests for _link_areas_from_geometry method.""" + + def test_link_areas_no_match(self): + """_link_areas_from_geometry with non-overlapping geometry links no areas.""" + incident = self.env["spp.hazard.incident"].create( + { + "name": "No Match Incident", + "code": "NOMATCH-001", + "start_date": "2026-01-01", + } + ) + + # Polygon far from any test area + far_polygon = { + "type": "Polygon", + "coordinates": [ + [ + [170.0, 70.0], + [171.0, 70.0], + [171.0, 71.0], + [170.0, 71.0], + [170.0, 70.0], + ] + ], + } + + incident._link_areas_from_geometry(far_polygon) + + # Should not fail, just log and link nothing + self.assertEqual(len(incident.area_ids), 0) + + def test_auto_populate_dates_from_effective(self): + """Auto-populate start_date from effective on create.""" + incident = self.env["spp.hazard.incident"].create( + { + "name": "Date Auto Test", + "code": "DATE-AUTO-001", + "effective": "2026-06-15 10:00:00", + } + ) + + self.assertEqual(str(incident.start_date), "2026-06-15") + + def test_auto_populate_end_date_from_expires(self): + """Auto-populate end_date from expires on create.""" + incident = self.env["spp.hazard.incident"].create( + { + "name": "Date Auto End Test", + "code": "DATE-AUTO-002", + "effective": "2026-06-15 10:00:00", + "expires": "2026-07-01 10:00:00", + } + ) + + self.assertEqual(str(incident.start_date), "2026-06-15") + self.assertEqual(str(incident.end_date), "2026-07-01") diff --git a/spp_hazard/tests/test_geofence.py b/spp_hazard/tests/test_geofence.py index 13a935f27..4091cd4eb 100644 --- a/spp_hazard/tests/test_geofence.py +++ b/spp_hazard/tests/test_geofence.py @@ -35,7 +35,7 @@ def setUpClass(cls): "code": "GEO-TEST-INC-001", "category_id": cls.category_typhoon.id, "start_date": "2024-06-01", - "severity": "3", + "severity_id": cls.severity_severe.id, } ) diff --git a/spp_hazard/tests/test_hazard_incident.py b/spp_hazard/tests/test_hazard_incident.py index c92f4e43b..c8c94c309 100644 --- a/spp_hazard/tests/test_hazard_incident.py +++ b/spp_hazard/tests/test_hazard_incident.py @@ -22,7 +22,7 @@ def setUpClass(cls): "code": "TEST-INC-001", "category_id": cls.category_typhoon.id, "start_date": "2024-01-01", - "severity": "3", + "severity_id": cls.severity_severe.id, } ) @@ -108,12 +108,12 @@ def test_07_incident_area_details(self): { "incident_id": self.incident.id, "area_id": self.area.id, - "severity_override": "5", + "severity_override_id": self.severity_extreme.id, "affected_population_estimate": 1000, } ) self.assertTrue(incident_area) - self.assertEqual(incident_area.severity_override, "5") + self.assertEqual(incident_area.severity_override_id, self.severity_extreme) self.assertEqual(incident_area.affected_population_estimate, 1000) def test_08_identify_potentially_affected(self): diff --git a/spp_hazard/tests/test_migration_severity.py b/spp_hazard/tests/test_migration_severity.py new file mode 100644 index 000000000..307168e1a --- /dev/null +++ b/spp_hazard/tests/test_migration_severity.py @@ -0,0 +1,109 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. +import importlib.util +from pathlib import Path + +from odoo.tests.common import TransactionCase + +CAP_SEVERITY_NS = "urn:oasis:names:tc:cap:severity" + +MIGRATION_PATH = Path(__file__).parent.parent / "migrations" / "19.0.2.1.0" / "post-migration.py" + + +def _load_migration(): + spec = importlib.util.spec_from_file_location("spp_hazard_severity_migration", MIGRATION_PATH) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class TestSeverityMigration(TransactionCase): + """Exercise the 19.0.2.1.0 post-migration that backfills severity + vocabulary codes from the legacy 1-5 Selection columns. + + The legacy columns do not exist in a fresh database, so each test + recreates them with SQL before running the migration (Postgres DDL is + transactional and rolls back with the test). + """ + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.VocabCode = cls.env["spp.vocabulary.code"] + cls.category = cls.env["spp.hazard.category"].create({"name": "Migration Cat", "code": "MIG-CAT"}) + cls.area = cls.env["spp.area"].create({"draft_name": "Migration Area", "code": "MIG-AREA"}) + + def _make_incident(self, code): + return self.env["spp.hazard.incident"].create( + {"name": f"Migration incident {code}", "code": code, "category_id": self.category.id} + ) + + def _add_legacy_columns(self): + self.env.cr.execute("ALTER TABLE spp_hazard_incident ADD COLUMN IF NOT EXISTS severity varchar") + self.env.cr.execute("ALTER TABLE spp_hazard_incident_area ADD COLUMN IF NOT EXISTS severity_override varchar") + + def _set_legacy(self, incident, value): + self.env.cr.execute("UPDATE spp_hazard_incident SET severity = %s WHERE id = %s", (value, incident.id)) + + def _cap_code(self, code): + return self.VocabCode.get_code(CAP_SEVERITY_NS, code) + + def _migrate(self): + _load_migration().migrate(self.env.cr, "19.0.2.0.2") + self.env.invalidate_all() + + def test_01_mapping_a_backfills_incident_severity(self): + """Legacy 1-5 values map label-faithfully to CAP codes (mapping A).""" + expected = {"1": "minor", "2": "moderate", "3": "severe", "4": "severe", "5": "extreme"} + self._add_legacy_columns() + incidents = {} + for legacy in expected: + incidents[legacy] = self._make_incident(f"MIG-{legacy}") + self._set_legacy(incidents[legacy], legacy) + + self._migrate() + + for legacy, cap in expected.items(): + self.assertEqual( + incidents[legacy].severity_id, + self._cap_code(cap), + f"legacy severity {legacy!r} should map to CAP {cap!r}", + ) + + def test_02_backfills_area_severity_override(self): + self._add_legacy_columns() + incident = self._make_incident("MIG-AREA-1") + area_link = self.env["spp.hazard.incident.area"].create({"incident_id": incident.id, "area_id": self.area.id}) + self.env.cr.execute( + "UPDATE spp_hazard_incident_area SET severity_override = %s WHERE id = %s", ("5", area_link.id) + ) + + self._migrate() + + self.assertEqual(area_link.severity_override_id, self._cap_code("extreme")) + + def test_03_does_not_overwrite_existing_value(self): + """Idempotent: a severity_id already set (e.g. manually re-entered) wins.""" + self._add_legacy_columns() + incident = self._make_incident("MIG-KEEP") + incident.severity_id = self._cap_code("minor") + self._set_legacy(incident, "5") + + self._migrate() + + self.assertEqual(incident.severity_id, self._cap_code("minor")) + + def test_04_unmapped_value_left_empty(self): + self._add_legacy_columns() + incident = self._make_incident("MIG-JUNK") + self._set_legacy(incident, "not-a-level") + + with self.assertLogs("odoo.addons.spp_hazard.migrations.severity", level="WARNING"): + self._migrate() + + self.assertFalse(incident.severity_id) + + def test_05_noop_without_legacy_columns(self): + """Fresh installs have no legacy columns; the migration must not fail.""" + incident = self._make_incident("MIG-FRESH") + self._migrate() + self.assertFalse(incident.severity_id) diff --git a/spp_hazard/tests/test_registrant.py b/spp_hazard/tests/test_registrant.py index e343295b7..a4c578875 100644 --- a/spp_hazard/tests/test_registrant.py +++ b/spp_hazard/tests/test_registrant.py @@ -17,7 +17,7 @@ def setUpClass(cls): "code": "REG-TEST-INC-001", "category_id": cls.category_typhoon.id, "start_date": "2024-01-01", - "severity": "3", + "severity_id": cls.severity_severe.id, } ) cls.impact = cls.env["spp.hazard.impact"].create( diff --git a/spp_hazard/views/hazard_incident_views.xml b/spp_hazard/views/hazard_incident_views.xml index 50a7f6c28..6bfba2971 100644 --- a/spp_hazard/views/hazard_incident_views.xml +++ b/spp_hazard/views/hazard_incident_views.xml @@ -11,10 +11,10 @@ decoration-warning="status == 'recovery'" decoration-muted="status == 'closed'" > - - - - + + + + + + - - @@ -142,13 +141,22 @@ /> - + - + @@ -161,6 +169,55 @@ readonly="status == 'closed'" /> + + + + + + + + + + + + + + + + + + + +

    - + +

    No impacts recorded. Impacts track damage assessments linked to this incident.

    @@ -245,6 +309,8 @@ + + + + + - + diff --git a/spp_hazard_programs/README.rst b/spp_hazard_programs/README.rst index 0b6fdee90..76170e367 100644 --- a/spp_hazard_programs/README.rst +++ b/spp_hazard_programs/README.rst @@ -324,6 +324,12 @@ Test Scenario 9: Incident List View Column Changelog ========= +19.0.2.0.1 +~~~~~~~~~~ + +- fix: program view shows the vocabulary-backed ``severity_id`` field + (follows the spp_hazard severity vocabulary change, re-land from #76). + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_hazard_programs/__manifest__.py b/spp_hazard_programs/__manifest__.py index 1871df01a..dfb115f96 100644 --- a/spp_hazard_programs/__manifest__.py +++ b/spp_hazard_programs/__manifest__.py @@ -7,7 +7,7 @@ "summary": "Links hazard impacts to program eligibility and entitlements. " "Enables emergency programs to use hazard data for targeting and benefit calculation.", "category": "OpenSPP/Targeting", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "sequence": 1, "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_hazard_programs/readme/HISTORY.md b/spp_hazard_programs/readme/HISTORY.md index 4aaf9afef..fa327bcbc 100644 --- a/spp_hazard_programs/readme/HISTORY.md +++ b/spp_hazard_programs/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.0.1 + +- fix: program view shows the vocabulary-backed `severity_id` field (follows the spp_hazard severity vocabulary change, re-land from #76). + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_hazard_programs/static/description/index.html b/spp_hazard_programs/static/description/index.html index 8a1fc0b68..15b6c575a 100644 --- a/spp_hazard_programs/static/description/index.html +++ b/spp_hazard_programs/static/description/index.html @@ -698,6 +698,13 @@

    Changelog

+

19.0.2.0.1

+ +
+

19.0.2.0.0