Skip to content

Commit a4d8f3e

Browse files
committed
fix: use incident code instead of uuid in geojson properties, add missing tests
Bug fix: - spp_api_v2_gis _get_geojson_properties used incident_id.uuid but spp.hazard.incident has no uuid field; use .code instead New tests in spp_gis (11 added, 78 total): - UUID copy=False generates new UUID on copy - Rename to duplicate active name raises ValidationError - Reactivate archived record with duplicate name raises ValidationError - create_from_geojson accepts dict input (not just string) - Different geometry sizes produce different area_sqkm values - Empty recordset to_geojson_collection returns empty features - GeoJSON property values are correct (not just keys present) - Description defaults to empty string in properties - geofence_type defaults to "custom" when omitted - area_sqkm is strictly positive for known test polygon - Archived geofences excluded from default search New tests in spp_hazard (5 added, 38 total): - hazard_zone geofence type via selection_add - Geofence linked to actual hazard incident - incident_id ondelete set null behavior - hazard_zone type label in GeoJSON properties - Geofence without incident works fine New test in spp_api_v2_gis (1 added, 191 total): - Linked incident code and name appear in GeoJSON properties
1 parent ae647c6 commit a4d8f3e

File tree

5 files changed

+359
-8
lines changed

5 files changed

+359
-8
lines changed

spp_api_v2_gis/models/geofence.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ class GisGeofence(models.Model):
2121
def _get_geojson_properties(self):
2222
"""Extend properties with incident info from spp_hazard."""
2323
props = super()._get_geojson_properties()
24-
props["incident_id"] = self.incident_id.uuid if self.incident_id and hasattr(self.incident_id, "uuid") else None
24+
props["incident_id"] = self.incident_id.code if self.incident_id else None
2525
props["incident_name"] = self.incident_id.name if self.incident_id else None
2626
return props

spp_api_v2_gis/tests/test_geofence_model.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,8 @@ def test_geofence_targeting_area_type(self):
6161

6262
self.assertEqual(geofence.geofence_type, "targeting_area")
6363

64-
def test_geojson_properties_include_incident(self):
65-
"""Test that incident fields appear in properties when spp_hazard adds them."""
66-
# incident_id should be available since spp_hazard is a dependency
64+
def test_geojson_properties_incident_none(self):
65+
"""Test that incident fields are None when no incident is linked."""
6766
geofence = self.env["spp.gis.geofence"].create(
6867
{
6968
"name": "Incident Props Test",
@@ -72,16 +71,45 @@ def test_geojson_properties_include_incident(self):
7271
}
7372
)
7473

75-
feature = geofence.to_geojson()
76-
props = feature["properties"]
74+
props = geofence.to_geojson()["properties"]
7775

78-
# spp_api_v2_gis extends _get_geojson_properties with incident fields
7976
self.assertIn("incident_id", props)
8077
self.assertIn("incident_name", props)
81-
# No incident linked, so values should be None
8278
self.assertIsNone(props["incident_id"])
8379
self.assertIsNone(props["incident_name"])
8480

81+
def test_geojson_properties_with_linked_incident(self):
82+
"""Test that incident code and name appear in properties when linked."""
83+
# Create a hazard incident
84+
category = self.env["spp.hazard.category"].create(
85+
{
86+
"name": "Test Cat GIS",
87+
"code": "TEST_CAT_GIS_API",
88+
}
89+
)
90+
incident = self.env["spp.hazard.incident"].create(
91+
{
92+
"name": "API Test Incident",
93+
"code": "API-INC-001",
94+
"category_id": category.id,
95+
"start_date": "2024-01-01",
96+
}
97+
)
98+
99+
geofence = self.env["spp.gis.geofence"].create(
100+
{
101+
"name": "Linked Incident Test",
102+
"geometry": json.dumps(self.sample_polygon),
103+
"geofence_type": "hazard_zone",
104+
"incident_id": incident.id,
105+
}
106+
)
107+
108+
props = geofence.to_geojson()["properties"]
109+
110+
self.assertEqual(props["incident_id"], "API-INC-001")
111+
self.assertEqual(props["incident_name"], "API Test Incident")
112+
85113
def test_geofence_type_label_service_area(self):
86114
"""Test that service_area type label is correct."""
87115
geofence = self.env["spp.gis.geofence"].create(

spp_gis/tests/test_geofence.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,3 +502,204 @@ def test_geofence_uuid_in_properties(self):
502502
self.assertIn("uuid", props)
503503
self.assertNotIn("id", props)
504504
self.assertEqual(props["uuid"], geofence.uuid)
505+
506+
def test_geofence_uuid_copy_generates_new(self):
507+
"""Test that copying a geofence generates a new UUID."""
508+
geofence = self.env["spp.gis.geofence"].create(
509+
{
510+
"name": "UUID Copy Original",
511+
"geometry": json.dumps(self.sample_polygon),
512+
"geofence_type": "custom",
513+
}
514+
)
515+
516+
copied = geofence.copy({"name": "UUID Copy Clone"})
517+
518+
self.assertTrue(copied.uuid)
519+
self.assertNotEqual(geofence.uuid, copied.uuid)
520+
self.assertEqual(len(copied.uuid), 36)
521+
522+
def test_geofence_rename_to_duplicate_raises(self):
523+
"""Test that renaming a geofence to an existing active name raises error."""
524+
self.env["spp.gis.geofence"].create(
525+
{
526+
"name": "Existing Name",
527+
"geometry": json.dumps(self.sample_polygon),
528+
"geofence_type": "custom",
529+
}
530+
)
531+
532+
geofence2 = self.env["spp.gis.geofence"].create(
533+
{
534+
"name": "Different Name",
535+
"geometry": json.dumps(self.sample_polygon),
536+
"geofence_type": "custom",
537+
}
538+
)
539+
540+
with self.assertRaises(ValidationError):
541+
geofence2.write({"name": "Existing Name"})
542+
543+
def test_geofence_reactivate_duplicate_name_raises(self):
544+
"""Test that reactivating an archived geofence with a duplicate name raises error."""
545+
self.env["spp.gis.geofence"].create(
546+
{
547+
"name": "Active Name",
548+
"geometry": json.dumps(self.sample_polygon),
549+
"geofence_type": "custom",
550+
}
551+
)
552+
553+
archived = self.env["spp.gis.geofence"].create(
554+
{
555+
"name": "Temp Name",
556+
"geometry": json.dumps(self.sample_polygon),
557+
"geofence_type": "custom",
558+
}
559+
)
560+
archived.write({"active": False, "name": "Active Name"})
561+
562+
with self.assertRaises(ValidationError):
563+
archived.write({"active": True})
564+
565+
def test_create_from_geojson_dict_input(self):
566+
"""Test creating geofence from dict input (not string)."""
567+
geofence = self.env["spp.gis.geofence"].create_from_geojson(
568+
geojson_str=self.sample_polygon,
569+
name="Dict Input",
570+
geofence_type="custom",
571+
)
572+
573+
self.assertTrue(geofence)
574+
self.assertEqual(geofence.name, "Dict Input")
575+
576+
def test_geofence_area_varies_by_geometry_size(self):
577+
"""Test that different sized geometries produce different areas."""
578+
small_geofence = self.env["spp.gis.geofence"].create(
579+
{
580+
"name": "Small Area",
581+
"geometry": json.dumps(self.sample_polygon),
582+
"geofence_type": "custom",
583+
}
584+
)
585+
586+
# 2x2 degree polygon (roughly 4x the area of 1x1)
587+
larger_polygon = {
588+
"type": "Polygon",
589+
"coordinates": [
590+
[
591+
[100.0, 0.0],
592+
[102.0, 0.0],
593+
[102.0, 2.0],
594+
[100.0, 2.0],
595+
[100.0, 0.0],
596+
]
597+
],
598+
}
599+
large_geofence = self.env["spp.gis.geofence"].create(
600+
{
601+
"name": "Large Area",
602+
"geometry": json.dumps(larger_polygon),
603+
"geofence_type": "custom",
604+
}
605+
)
606+
607+
self.assertGreater(small_geofence.area_sqkm, 0)
608+
self.assertGreater(large_geofence.area_sqkm, small_geofence.area_sqkm)
609+
610+
def test_to_geojson_collection_empty_recordset(self):
611+
"""Test to_geojson_collection with empty recordset."""
612+
empty = self.env["spp.gis.geofence"].browse([])
613+
collection = empty.to_geojson_collection()
614+
615+
self.assertEqual(collection["type"], "FeatureCollection")
616+
self.assertEqual(collection["features"], [])
617+
618+
def test_geojson_properties_values_correct(self):
619+
"""Test that GeoJSON property values are correct, not just present."""
620+
geofence = self.env["spp.gis.geofence"].create(
621+
{
622+
"name": "Values Check",
623+
"geometry": json.dumps(self.sample_polygon),
624+
"geofence_type": "area_of_interest",
625+
"created_from": "api",
626+
"tag_ids": [(6, 0, [self.tag1.id])],
627+
}
628+
)
629+
630+
props = geofence.to_geojson()["properties"]
631+
632+
self.assertEqual(props["uuid"], geofence.uuid)
633+
self.assertEqual(props["name"], "Values Check")
634+
self.assertEqual(props["description"], "")
635+
self.assertEqual(props["geofence_type"], "area_of_interest")
636+
self.assertEqual(props["geofence_type_label"], "Area of Interest")
637+
self.assertIsInstance(props["area_sqkm"], float)
638+
self.assertEqual(props["tags"], ["Test Tag 1"])
639+
self.assertEqual(props["created_from"], "api")
640+
self.assertEqual(props["created_by"], self.env.user.name)
641+
self.assertIsNotNone(props["create_date"])
642+
643+
def test_geofence_description_none_becomes_empty_string(self):
644+
"""Test that missing description becomes empty string in properties."""
645+
geofence = self.env["spp.gis.geofence"].create(
646+
{
647+
"name": "No Description",
648+
"geometry": json.dumps(self.sample_polygon),
649+
"geofence_type": "custom",
650+
}
651+
)
652+
653+
props = geofence.to_geojson()["properties"]
654+
self.assertEqual(props["description"], "")
655+
656+
def test_geofence_type_defaults_to_custom(self):
657+
"""Test that geofence_type defaults to custom when not specified."""
658+
geofence = self.env["spp.gis.geofence"].create(
659+
{
660+
"name": "Default Type Test",
661+
"geometry": json.dumps(self.sample_polygon),
662+
}
663+
)
664+
665+
self.assertEqual(geofence.geofence_type, "custom")
666+
667+
def test_geofence_area_positive_for_known_polygon(self):
668+
"""Test that area is strictly positive for the known ~12,300 sqkm test polygon."""
669+
geofence = self.env["spp.gis.geofence"].create(
670+
{
671+
"name": "Positive Area Test",
672+
"geometry": json.dumps(self.sample_polygon),
673+
"geofence_type": "custom",
674+
}
675+
)
676+
677+
# 1 degree x 1 degree near equator is roughly 12,300 sq km
678+
self.assertGreater(geofence.area_sqkm, 0)
679+
680+
def test_geofence_archive_excludes_from_search(self):
681+
"""Test that archived geofences are excluded from default search."""
682+
geofence = self.env["spp.gis.geofence"].create(
683+
{
684+
"name": "Archive Search Test",
685+
"geometry": json.dumps(self.sample_polygon),
686+
"geofence_type": "custom",
687+
}
688+
)
689+
690+
# Should be found by default search
691+
found = self.env["spp.gis.geofence"].search([("name", "=", "Archive Search Test")])
692+
self.assertEqual(len(found), 1)
693+
694+
# Archive it
695+
geofence.write({"active": False})
696+
697+
# Should no longer appear in default search
698+
found = self.env["spp.gis.geofence"].search([("name", "=", "Archive Search Test")])
699+
self.assertEqual(len(found), 0)
700+
701+
# Should appear when explicitly searching inactive
702+
found = (
703+
self.env["spp.gis.geofence"].with_context(active_test=False).search([("name", "=", "Archive Search Test")])
704+
)
705+
self.assertEqual(len(found), 1)

spp_hazard/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from . import test_hazard_incident
55
from . import test_hazard_impact
66
from . import test_hazard_impact_type
7+
from . import test_geofence

0 commit comments

Comments
 (0)