From 8800419d9ff73958e960d27f9c64272aeb8c47a9 Mon Sep 17 00:00:00 2001 From: robinvandermolen Date: Thu, 12 Dec 2024 13:51:36 +0100 Subject: [PATCH] :boom: [#2177] Changing map data to geoJson For the map interaction changes, we need to save the map data in a different format. This is to allow us to differentiate between point, line or polygon map information. --- src/openforms/formio/components/custom.py | 57 +- .../formio/tests/validation/test_map.py | 512 +++++++++++++++++- 2 files changed, 558 insertions(+), 11 deletions(-) diff --git a/src/openforms/formio/components/custom.py b/src/openforms/formio/components/custom.py index e36f585677..ff17d50447 100644 --- a/src/openforms/formio/components/custom.py +++ b/src/openforms/formio/components/custom.py @@ -9,6 +9,7 @@ from django.utils.html import format_html from django.utils.translation import gettext as _ +from drf_polymorphic.serializers import PolymorphicSerializer from glom import glom from rest_framework import ISO_8601, serializers from rest_framework.request import Request @@ -191,6 +192,53 @@ def build_serializer_field( return serializers.ListField(child=base) if multiple else base +class GeoJsonPointGeometrySerializer(serializers.Serializer): + coordinates = serializers.ListField( + child=serializers.FloatField(required=True, allow_null=False), + min_length=2, + max_length=2, + ) + + +class GeoJsonLineStringGeometrySerializer(serializers.Serializer): + coordinates = serializers.ListField( + child=serializers.ListField( + child=serializers.FloatField(required=True, allow_null=False), + min_length=2, + max_length=2, + ), + ) + + +class GeoJsonPolygonGeometrySerializer(serializers.Serializer): + coordinates = serializers.ListField( + child=serializers.ListField( + child=serializers.ListField( + child=serializers.FloatField(required=True, allow_null=False), + min_length=2, + max_length=2, + ), + ), + ) + + +class GeoJsonGeometryPolymorphicSerializer(PolymorphicSerializer): + type = serializers.RegexField("^Point|LineString|Polygon$", required=True) + + discriminator_field = "type" + serializer_mapping = { + str("Point"): GeoJsonPointGeometrySerializer, + str("LineString"): GeoJsonLineStringGeometrySerializer, + str("Polygon"): GeoJsonPolygonGeometrySerializer, + } + + +class GeoJsonSerializer(serializers.Serializer): + type = serializers.RegexField("^Feature$", required=True) + properties = serializers.DictField() + geometry = GeoJsonGeometryPolymorphicSerializer() + + @register("map") class Map(BasePlugin[MapComponent]): formatter = MapFormatter @@ -213,14 +261,11 @@ def rewrite_for_request(component: MapComponent, request: Request): component["initialCenter"]["lat"] = config.form_map_default_latitude component["initialCenter"]["lng"] = config.form_map_default_longitude - def build_serializer_field(self, component: MapComponent) -> serializers.ListField: + def build_serializer_field(self, component: MapComponent) -> GeoJsonSerializer: validate = component.get("validate", {}) required = validate.get("required", False) - base = serializers.FloatField( - required=required, - allow_null=not required, - ) - return serializers.ListField(child=base, min_length=2, max_length=2) + + return GeoJsonSerializer(required=required, allow_null=not required) @register("postcode") diff --git a/src/openforms/formio/tests/validation/test_map.py b/src/openforms/formio/tests/validation/test_map.py index d37101bc96..e944c2305b 100644 --- a/src/openforms/formio/tests/validation/test_map.py +++ b/src/openforms/formio/tests/validation/test_map.py @@ -4,6 +4,20 @@ from .helpers import extract_error, validate_formio_data +def _recursive_get_error_code(error): + error_codes = [] + if type(error) is dict: + for value in error.values(): + error_codes.append(_recursive_get_error_code(value)) + return error_codes + + if type(error) is list: + for value in error: + error_codes.append(value.code) + return error_codes + return error.code + + class MapValidationTests(SimpleTestCase): def test_map_field_required_validation(self): @@ -16,7 +30,6 @@ def test_map_field_required_validation(self): invalid_values = [ ({}, "required"), - ({"foo": ""}, "not_a_list"), ({"foo": None}, "null"), ] @@ -29,16 +42,39 @@ def test_map_field_required_validation(self): error = extract_error(errors, component["key"]) self.assertEqual(error.code, error_code) - def test_map_field_min_max_length_of_items(self): + def test_map_field_missing_keys(self): component: Component = { "type": "map", "key": "foo", "label": "Test", + "validate": {"required": True}, + } + + invalid_values = {"foo": {}} + + is_valid, errors = validate_formio_data(component, invalid_values) + + type_error = extract_error(errors["foo"], "type") + properties_error = extract_error(errors["foo"], "properties") + geometry_error = extract_error(errors["foo"], "geometry") + + self.assertFalse(is_valid) + self.assertIn(component["key"], errors) + + self.assertEqual(type_error.code, "required") + self.assertEqual(properties_error.code, "required") + self.assertEqual(geometry_error.code, "required") + + def test_map_field_unknown_type(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, } invalid_values = [ - ({"foo": [34.869343]}, "min_length"), - ({"foo": [34.869343, 24.080053, 24.074657]}, "max_length"), + ({"foo": {"type": "a-unknown-type"}}, "invalid"), ] for data, error_code in invalid_values: @@ -47,5 +83,471 @@ def test_map_field_min_max_length_of_items(self): self.assertFalse(is_valid) self.assertIn(component["key"], errors) - error = extract_error(errors, component["key"]) + error = extract_error(errors["foo"], "type") + self.assertEqual(error.code, error_code) + + def test_map_field_missing_geometry_keys(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = {"foo": {"type": "Feature", "properties": {}, "geometry": {}}} + + is_valid, errors = validate_formio_data(component, invalid_values) + print(errors) + + geometry_type_error = extract_error(errors["foo"]["geometry"], "type") + + self.assertFalse(is_valid) + self.assertIn(component["key"], errors) + + self.assertEqual(geometry_type_error.code, "required") + + def test_map_field_unknown_geometry_type(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + data = { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "unknown-geometry-type", "coordinates": [1, 1]}, + }, + } + + is_valid, errors = validate_formio_data(component, data) + geometry_type_error = extract_error(errors["foo"]["geometry"], "type") + + self.assertFalse(is_valid) + self.assertEqual(geometry_type_error.code, "invalid") + + def test_map_field_invalid_point_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point"}, + }, + }, + "required", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": None}, + }, + }, + "null", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": ""}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": 1}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": [[1, 2]]}, + }, + }, + ["invalid"], + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + self.assertFalse(is_valid) + self.assertIn(component["key"], errors) + + error = extract_error(errors["foo"]["geometry"], "coordinates") + error_codes = _recursive_get_error_code(error) + self.assertEqual(error_codes, error_code) + + def test_map_field_min_max_length_of_items_point_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": [1]}, + }, + }, + "min_length", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": [1, 2, 3]}, + }, + }, + "max_length", + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + error = extract_error(errors["foo"]["geometry"], "coordinates") + + self.assertFalse(is_valid) self.assertEqual(error.code, error_code) + + def test_map_field_valid_point_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + data = { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Point", "coordinates": [1, 1]}, + }, + } + + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid) + + def test_map_field_invalid_line_string_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString"}, + }, + }, + "required", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": None}, + }, + }, + "null", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": ""}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": 1}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": [1, 1]}, + }, + }, + ["not_a_list"], + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": [[[1, 2]]]}, + }, + }, + [["invalid"]], + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + self.assertFalse(is_valid) + self.assertIn(component["key"], errors) + + error = extract_error(errors["foo"]["geometry"], "coordinates") + error_codes = _recursive_get_error_code(error) + self.assertEqual(error_codes, error_code) + + def test_map_field_min_max_length_of_items_line_string_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": [[1]]}, + }, + }, + "min_length", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": [[1, 2, 3]]}, + }, + }, + "max_length", + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + error = extract_error(errors["foo"]["geometry"]["coordinates"], 0) + + self.assertFalse(is_valid) + self.assertEqual(error.code, error_code) + + def test_map_field_valid_line_string_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + data = { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "LineString", "coordinates": [[1, 1]]}, + }, + } + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid) + + def test_map_field_invalid_polygon_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon"}, + }, + }, + "required", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": None}, + }, + }, + "null", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": ""}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": 1}, + }, + }, + "not_a_list", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [1, 1]}, + }, + }, + ["not_a_list"], + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[1, 1]]}, + }, + }, + [["not_a_list"], ["not_a_list"]], + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[[[1, 2]]]]}, + }, + }, + [[["invalid"]]], + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + self.assertFalse(is_valid) + self.assertIn(component["key"], errors) + + error = extract_error(errors["foo"]["geometry"], "coordinates") + error_codes = _recursive_get_error_code(error) + self.assertEqual(error_codes, error_code) + + def test_map_field_min_max_length_of_items_polygon_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + invalid_values = [ + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[[1]]]}, + }, + }, + "min_length", + ), + ( + { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[[1, 2, 3]]]}, + }, + }, + "max_length", + ), + ] + + for data, error_code in invalid_values: + with self.subTest(data=data): + is_valid, errors = validate_formio_data(component, data) + + error = extract_error(errors["foo"]["geometry"]["coordinates"][0], 0) + + self.assertFalse(is_valid) + self.assertEqual(error.code, error_code) + + def test_map_field_valid_polygon_geometry(self): + component: Component = { + "type": "map", + "key": "foo", + "label": "Test", + "validate": {"required": True}, + } + + data = { + "foo": { + "type": "Feature", + "properties": {}, + "geometry": {"type": "Polygon", "coordinates": [[[1, 1]]]}, + }, + } + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid)