diff --git a/INSTALL.rst b/INSTALL.rst index fbd52c59d6..b717880660 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -432,11 +432,18 @@ After configuring the application groups in the admin through point-and-click, y call this script to dump the configuration into a fixture which will be loaded on all other installations. -``bin/generate_default_groups_fixtures.sh`` -------------------------------------------- +``bin/generate_default_groups_fixture.sh`` +------------------------------------------ After configuring the user groups with the appropriate permissions in the admin, -you can this script to dump the configuration into a fixture which will be loaded on +you call this script to dump the configuration into a fixture which will be loaded on +all other installations. + +``bin/generate_default_map_tile_layers_fixture.sh`` +----------------------------------------------------------- + +After configuring the map tile layers in the admin, +you call this script to dump the configuration into a fixture which will be loaded on all other installations. ``bin/generate_oas.sh`` diff --git a/bin/generate_default_map_tile_layers_fixture.sh b/bin/generate_default_map_tile_layers_fixture.sh new file mode 100755 index 0000000000..6293f8bc60 --- /dev/null +++ b/bin/generate_default_map_tile_layers_fixture.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# +# Dump the current (local database) config MapTileLayer to a JSON fixture. +# This overwrites the existing one. +# +# You can load this fixture with: +# $ src/manage.py loaddata default_map_tile_layers +# +# Run this script from the root of the repository + +src/manage.py dumpdata --indent=4 --natural-foreign --natural-primary config.MapTileLayer > src/openforms/fixtures/default_map_tile_layers.json diff --git a/package-lock.json b/package-lock.json index 8f7278b025..5a02c8508f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@open-formulieren/design-tokens": "^0.53.0", - "@open-formulieren/formio-builder": "^0.33.0", + "@open-formulieren/formio-builder": "^0.34.0", "@open-formulieren/leaflet-tools": "^1.0.0", "@open-formulieren/monaco-json-editor": "^0.2.0", "@tinymce/tinymce-react": "^4.3.2", @@ -4526,9 +4526,10 @@ "integrity": "sha512-3Pv32ULCuFOJZ2GaqcpvB45u6xScr0lmW5ETB9P1Ox9TG5nvMcVSwuwYe/GwxbzmvtZgiMQRMKRFT9lNYLeREQ==" }, "node_modules/@open-formulieren/formio-builder": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.33.0.tgz", - "integrity": "sha512-mNozndNckQIO/mxKJYuqbqvvKmVU3EZCFKm0raunpS7gKEPyJl8vNV5fF9o8gNghh9xwBFA4usjNuCSySJwFqQ==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.34.0.tgz", + "integrity": "sha512-inWjRzAdTCSfkeSQbtF5I9XA1EqfaAx1tLCAzZ+XRnrtfK2+/md0dp+dbKCsRwZUDDzzaojECtn5LGeIkubYRA==", + "license": "EUPL-1.2", "dependencies": { "@ckeditor/ckeditor5-react": "^6.2.0", "@floating-ui/react": "^0.26.4", @@ -23105,9 +23106,9 @@ "integrity": "sha512-3Pv32ULCuFOJZ2GaqcpvB45u6xScr0lmW5ETB9P1Ox9TG5nvMcVSwuwYe/GwxbzmvtZgiMQRMKRFT9lNYLeREQ==" }, "@open-formulieren/formio-builder": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.33.0.tgz", - "integrity": "sha512-mNozndNckQIO/mxKJYuqbqvvKmVU3EZCFKm0raunpS7gKEPyJl8vNV5fF9o8gNghh9xwBFA4usjNuCSySJwFqQ==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@open-formulieren/formio-builder/-/formio-builder-0.34.0.tgz", + "integrity": "sha512-inWjRzAdTCSfkeSQbtF5I9XA1EqfaAx1tLCAzZ+XRnrtfK2+/md0dp+dbKCsRwZUDDzzaojECtn5LGeIkubYRA==", "requires": { "@ckeditor/ckeditor5-react": "^6.2.0", "@floating-ui/react": "^0.26.4", diff --git a/package.json b/package.json index c2a8f3c2a2..ac2c6b996a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.1.1", "@open-formulieren/design-tokens": "^0.53.0", - "@open-formulieren/formio-builder": "^0.33.0", + "@open-formulieren/formio-builder": "^0.34.0", "@open-formulieren/leaflet-tools": "^1.0.0", "@open-formulieren/monaco-json-editor": "^0.2.0", "@tinymce/tinymce-react": "^4.3.2", diff --git a/src/openforms/config/admin.py b/src/openforms/config/admin.py index b050ba22af..7a8bb08a8d 100644 --- a/src/openforms/config/admin.py +++ b/src/openforms/config/admin.py @@ -9,7 +9,7 @@ from .admin_views import ThemePreviewView from .forms import GlobalConfigurationAdminForm, ThemeAdminForm -from .models import CSPSetting, GlobalConfiguration, RichTextColor, Theme +from .models import CSPSetting, GlobalConfiguration, MapTileLayer, RichTextColor, Theme @admin.register(GlobalConfiguration) @@ -221,6 +221,20 @@ class RichTextColorAdmin(admin.ModelAdmin): ] +@admin.register(MapTileLayer) +class MapTileLayerAdmin(admin.ModelAdmin): + fields = ( + "label", + "identifier", + "url", + ) + list_display = ( + "label", + "identifier", + "url", + ) + + @admin.register(CSPSetting) class CSPSettingAdmin(admin.ModelAdmin): readonly_fields = ("content_type_link",) diff --git a/src/openforms/config/migrations/0069_maptilelayer.py b/src/openforms/config/migrations/0069_maptilelayer.py new file mode 100644 index 0000000000..f14b8cb3a2 --- /dev/null +++ b/src/openforms/config/migrations/0069_maptilelayer.py @@ -0,0 +1,56 @@ +# Generated by Django 4.2.17 on 2024-12-17 12:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("config", "0068_update_summary_tags"), + ] + + operations = [ + migrations.CreateModel( + name="MapTileLayer", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "identifier", + models.SlugField( + help_text="A unique identifier for the tile layer.", + unique=True, + verbose_name="identifier", + ), + ), + ( + "url", + models.URLField( + help_text="URL to the tile layer image, used to define the map component background. To ensure correct functionality of the map, EPSG 28992 projection should be used. Example value: https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png", + max_length=255, + verbose_name="tile layer url", + ), + ), + ( + "label", + models.CharField( + help_text="An easily recognizable name for the tile layer, used to identify it.", + max_length=100, + verbose_name="label", + ), + ), + ], + options={ + "verbose_name": "map tile layer", + "verbose_name_plural": "map tile layers", + "ordering": ("label",), + }, + ), + ] diff --git a/src/openforms/config/models/__init__.py b/src/openforms/config/models/__init__.py index ae0efffa9d..9e082fe22c 100644 --- a/src/openforms/config/models/__init__.py +++ b/src/openforms/config/models/__init__.py @@ -1,11 +1,13 @@ from .color import RichTextColor from .config import GlobalConfiguration from .csp import CSPSetting +from .map import MapTileLayer from .theme import Theme __all__ = [ "CSPSetting", "GlobalConfiguration", "RichTextColor", + "MapTileLayer", "Theme", ] diff --git a/src/openforms/config/models/map.py b/src/openforms/config/models/map.py new file mode 100644 index 0000000000..1d22532fa2 --- /dev/null +++ b/src/openforms/config/models/map.py @@ -0,0 +1,36 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class MapTileLayer(models.Model): + identifier = models.SlugField( + _("identifier"), + unique=True, + max_length=50, + help_text=_("A unique identifier for the tile layer."), + ) + url = models.URLField( + _("tile layer url"), + max_length=255, + help_text=_( + "URL to the tile layer image, used to define the map component " + "background. To ensure correct functionality of the map, " + "EPSG 28992 projection should be used. " + "Example value: https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png" + ), + ) + label = models.CharField( + _("label"), + max_length=100, + help_text=_( + "An easily recognizable name for the tile layer, used to identify it." + ), + ) + + class Meta: + verbose_name = _("map tile layer") + verbose_name_plural = _("map tile layers") + ordering = ("label",) + + def __str__(self): + return self.label diff --git a/src/openforms/config/tests/factories.py b/src/openforms/config/tests/factories.py index 4cec877d42..08476e714b 100644 --- a/src/openforms/config/tests/factories.py +++ b/src/openforms/config/tests/factories.py @@ -14,3 +14,12 @@ class ThemeFactory(factory.django.DjangoModelFactory): class Meta: model = "config.Theme" + + +class MapTileLayerFactory(factory.django.DjangoModelFactory): + identifier = factory.Faker("word") + url = factory.Sequence(lambda n: f"http://example-{n}.com") + label = factory.Faker("word") + + class Meta: + model = "config.MapTileLayer" diff --git a/src/openforms/config/tests/test_admin.py b/src/openforms/config/tests/test_admin.py index cc924ed832..2f60b53bd7 100644 --- a/src/openforms/config/tests/test_admin.py +++ b/src/openforms/config/tests/test_admin.py @@ -5,7 +5,7 @@ from openforms.accounts.tests.factories import SuperUserFactory -from .factories import RichTextColorFactory +from .factories import MapTileLayerFactory, RichTextColorFactory @disable_admin_mfa() @@ -27,3 +27,24 @@ def test_color_detail(self): response = self.app.get(url, user=user) self.assertEqual(response.status_code, 200) + + +@disable_admin_mfa() +class MapTileLayerTests(WebTest): + def test_map_tile_layer_changelist(self): + MapTileLayerFactory.create_batch(9) + url = reverse("admin:config_maptilelayer_changelist") + user = SuperUserFactory.create() + + response = self.app.get(url, user=user) + + self.assertEqual(response.status_code, 200) + + def test_map_tile_layer_detail(self): + map = MapTileLayerFactory.create() + url = reverse("admin:config_maptilelayer_change", args=(map.pk,)) + user = SuperUserFactory.create() + + response = self.app.get(url, user=user) + + self.assertEqual(response.status_code, 200) diff --git a/src/openforms/fixtures/default_admin_index.json b/src/openforms/fixtures/default_admin_index.json index 5728763f45..24c1df32a8 100644 --- a/src/openforms/fixtures/default_admin_index.json +++ b/src/openforms/fixtures/default_admin_index.json @@ -162,6 +162,10 @@ "config", "globalconfiguration" ], + [ + "config", + "maptilelayer" + ], [ "config", "theme" diff --git a/src/openforms/fixtures/default_map_tile_layers.json b/src/openforms/fixtures/default_map_tile_layers.json new file mode 100644 index 0000000000..ece464ffb0 --- /dev/null +++ b/src/openforms/fixtures/default_map_tile_layers.json @@ -0,0 +1,20 @@ +[ +{ + "model": "config.maptilelayer", + "pk": 1, + "fields": { + "identifier": "brt", + "url": "https://service.pdok.nl/brt/achtergrondkaart/wmts/v2_0/standaard/EPSG:28992/{z}/{x}/{y}.png", + "label": "BRT" + } +}, +{ + "model": "config.maptilelayer", + "pk": 2, + "fields": { + "identifier": "luchtfoto", + "url": "https://service.pdok.nl/hwh/luchtfotorgb/wmts/v1_0/Actueel_orthoHR/EPSG:28992/{z}/{x}/{y}.png", + "label": "Luchtfoto" + } +} +] diff --git a/src/openforms/formio/components/custom.py b/src/openforms/formio/components/custom.py index e7521c482b..e36f585677 100644 --- a/src/openforms/formio/components/custom.py +++ b/src/openforms/formio/components/custom.py @@ -14,7 +14,7 @@ from rest_framework.request import Request from openforms.authentication.service import AuthAttribute -from openforms.config.models import GlobalConfiguration +from openforms.config.models import GlobalConfiguration, MapTileLayer from openforms.submissions.models import Submission from openforms.typing import DataMapping from openforms.utils.date import TIMEZONE_AMS, datetime_in_amsterdam, format_date_value @@ -31,7 +31,13 @@ ) from ..formatters.formio import DefaultFormatter, TextFieldFormatter from ..registry import BasePlugin, register -from ..typing import AddressNLComponent, Component, DateComponent, DatetimeComponent +from ..typing import ( + AddressNLComponent, + Component, + DateComponent, + DatetimeComponent, + MapComponent, +) from ..utils import conform_to_mask from .np_family_members.constants import FamilyMembersDataAPIChoices from .np_family_members.haal_centraal import get_np_family_members_haal_centraal @@ -186,11 +192,20 @@ def build_serializer_field( @register("map") -class Map(BasePlugin[Component]): +class Map(BasePlugin[MapComponent]): formatter = MapFormatter + def mutate_config_dynamically( + self, component: MapComponent, submission: Submission, data: DataMapping + ) -> None: + if (identifier := component.get("tileLayerIdentifier")) is not None: + tile_layer = MapTileLayer.objects.filter(identifier=identifier).first() + if tile_layer is not None: + # Add the tile layer url information + component["tileLayerUrl"] = tile_layer.url + @staticmethod - def rewrite_for_request(component, request: Request): + def rewrite_for_request(component: MapComponent, request: Request): if component.get("useConfigDefaultMapSettings", False): config = GlobalConfiguration.get_solo() component["defaultZoom"] = config.form_map_default_zoom_level @@ -198,7 +213,7 @@ def rewrite_for_request(component, request: Request): component["initialCenter"]["lat"] = config.form_map_default_latitude component["initialCenter"]["lng"] = config.form_map_default_longitude - def build_serializer_field(self, component: Component) -> serializers.ListField: + def build_serializer_field(self, component: MapComponent) -> serializers.ListField: validate = component.get("validate", {}) required = validate.get("required", False) base = serializers.FloatField( diff --git a/src/openforms/formio/formatters/custom.py b/src/openforms/formio/formatters/custom.py index a76a4cec6d..6c87744fc9 100644 --- a/src/openforms/formio/formatters/custom.py +++ b/src/openforms/formio/formatters/custom.py @@ -6,7 +6,7 @@ from django.utils.html import format_html from django.utils.safestring import mark_safe -from ..typing import AddressNLComponent, Component +from ..typing import AddressNLComponent, Component, MapComponent from .base import FormatterBase @@ -22,7 +22,7 @@ def format(self, component: Component, value: str) -> str: class MapFormatter(FormatterBase): - def format(self, component: Component, value: list[float]) -> str: + def format(self, component: MapComponent, value: list[float]) -> str: # use a comma here since its a single data element return ", ".join((str(x) for x in value)) diff --git a/src/openforms/formio/tests/test_dynamic_config.py b/src/openforms/formio/tests/test_dynamic_config.py new file mode 100644 index 0000000000..5395377403 --- /dev/null +++ b/src/openforms/formio/tests/test_dynamic_config.py @@ -0,0 +1,292 @@ +from unittest.mock import Mock, patch + +from django.test import TestCase + +from rest_framework.test import APIRequestFactory + +from openforms.config.models import GlobalConfiguration +from openforms.config.tests.factories import MapTileLayerFactory +from openforms.formio.datastructures import FormioConfigurationWrapper +from openforms.formio.dynamic_config import ( + rewrite_formio_components, + rewrite_formio_components_for_request, +) +from openforms.submissions.tests.factories import SubmissionFactory + +rf = APIRequestFactory() + + +class DynamicConfigTests(TestCase): + @patch("openforms.formio.components.vanilla.GlobalConfiguration.get_solo") + def test_map_without_default_map_config(self, m_solo: Mock): + m_solo.return_value = GlobalConfiguration( + form_map_default_zoom_level=8, + form_map_default_latitude=55.123, + form_map_default_longitude=56.456, + ) + + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + } + self.assertEqual(configuration["components"][0], expected) + + @patch("openforms.formio.components.vanilla.GlobalConfiguration.get_solo") + def test_map_with_default_map_config(self, m_solo: Mock): + m_solo.return_value = GlobalConfiguration( + form_map_default_zoom_level=8, + form_map_default_latitude=55.123, + form_map_default_longitude=56.456, + ) + + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": True, + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 8, + "initialCenter": { + "lat": 55.123, + "lng": 56.456, + }, + "useConfigDefaultMapSettings": True, + } + self.assertEqual(configuration["components"][0], expected) + + def test_map_without_tile_layer_identifier(self): + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": None, + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": None, + } + self.assertEqual(configuration["components"][0], expected) + + def test_map_with_invalid_tile_layer_identifier(self): + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "", + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "", + } + self.assertEqual(configuration["components"][0], expected) + + def test_map_with_valid_unknown_tile_layer_identifier(self): + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "identifier", + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "identifier", + } + self.assertEqual(configuration["components"][0], expected) + + def test_map_with_valid_known_tile_layer_identifier(self): + map = MapTileLayerFactory.create(identifier="identifier") + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "identifier", + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": False, + "tileLayerIdentifier": "identifier", + "tileLayerUrl": map.url, + } + self.assertEqual(configuration["components"][0], expected) + + @patch("openforms.formio.components.vanilla.GlobalConfiguration.get_solo") + def test_map_with_valid_known_tile_layer_identifier_and_use_config_default_map_settings( + self, m_solo: Mock + ): + m_solo.return_value = GlobalConfiguration( + form_map_default_zoom_level=8, + form_map_default_latitude=55.123, + form_map_default_longitude=56.456, + ) + map = MapTileLayerFactory.create(identifier="identifier") + configuration = { + "components": [ + { + "type": "map", + "key": "map", + "defaultZoom": 3, + "initialCenter": { + "lat": 43.23, + "lng": 41.23, + }, + "useConfigDefaultMapSettings": True, + "tileLayerIdentifier": "identifier", + } + ] + } + formio_configuration = FormioConfigurationWrapper(configuration) + submission = SubmissionFactory.create() + rewrite_formio_components(formio_configuration, submission) + + request = rf.get("/dummy") + rewrite_formio_components_for_request(formio_configuration, request) + + expected = { + "type": "map", + "key": "map", + "defaultZoom": 8, + "initialCenter": { + "lat": 55.123, + "lng": 56.456, + }, + "useConfigDefaultMapSettings": True, + "tileLayerIdentifier": "identifier", + "tileLayerUrl": map.url, + } + self.assertEqual(configuration["components"][0], expected) diff --git a/src/openforms/formio/typing/__init__.py b/src/openforms/formio/typing/__init__.py index a086a4a252..8283e31bee 100644 --- a/src/openforms/formio/typing/__init__.py +++ b/src/openforms/formio/typing/__init__.py @@ -7,7 +7,7 @@ """ from .base import Component, FormioConfiguration, OptionDict -from .custom import AddressNLComponent, DateComponent +from .custom import AddressNLComponent, DateComponent, MapComponent from .vanilla import ( Column, ColumnsComponent, @@ -43,5 +43,6 @@ # special "EditGridComponent", "AddressNLComponent", + "MapComponent", # deprecated ] diff --git a/src/openforms/formio/typing/custom.py b/src/openforms/formio/typing/custom.py index c1db889011..76792272da 100644 --- a/src/openforms/formio/typing/custom.py +++ b/src/openforms/formio/typing/custom.py @@ -2,6 +2,7 @@ from .base import Component from .dates import DatePickerConfig, DatePickerCustomOptions +from .map import MapInitialCenter class DateComponent(Component): @@ -11,3 +12,12 @@ class DateComponent(Component): class AddressNLComponent(Component): deriveAddress: bool + + +class MapComponent(Component): + useConfigDefaultMapSettings: bool + defaultZoom: NotRequired[int] + initialCenter: NotRequired[MapInitialCenter] + tileLayerIdentifier: NotRequired[str] + # The tileLayerUrl will be dynamically generated from the tileLayerIdentifier + tileLayerUrl: NotRequired[str] diff --git a/src/openforms/formio/typing/map.py b/src/openforms/formio/typing/map.py new file mode 100644 index 0000000000..cfc712f65d --- /dev/null +++ b/src/openforms/formio/typing/map.py @@ -0,0 +1,6 @@ +from typing import NotRequired, TypedDict + + +class MapInitialCenter(TypedDict): + lat: NotRequired[float] + lng: NotRequired[float] diff --git a/src/openforms/forms/admin/mixins.py b/src/openforms/forms/admin/mixins.py index 84000b2003..2f8361f86d 100644 --- a/src/openforms/forms/admin/mixins.py +++ b/src/openforms/forms/admin/mixins.py @@ -5,7 +5,7 @@ from zgw_consumers.api_models.constants import VertrouwelijkheidsAanduidingen from openforms.config.constants import UploadFileType -from openforms.config.models import GlobalConfiguration, RichTextColor +from openforms.config.models import GlobalConfiguration, MapTileLayer, RichTextColor def get_rich_text_colors(): @@ -17,6 +17,10 @@ def get_rich_text_colors(): return colors +def get_map_tile_layers(): + return list(MapTileLayer.objects.values("identifier", "url", "label")) + + class FormioConfigMixin: def render_change_form( self, request, context, add=False, change=False, form_url="", obj=None @@ -26,6 +30,7 @@ def render_change_form( { "required_default": config.form_fields_required_default, "rich_text_colors": get_rich_text_colors(), + "map_tile_layers": get_map_tile_layers(), "upload_filetypes": [ {"label": label, "value": value} for value, label in UploadFileType.choices diff --git a/src/openforms/forms/templates/admin/forms/includes/formio_config.html b/src/openforms/forms/templates/admin/forms/includes/formio_config.html index f512ba493a..51ce7aaecb 100644 --- a/src/openforms/forms/templates/admin/forms/includes/formio_config.html +++ b/src/openforms/forms/templates/admin/forms/includes/formio_config.html @@ -1,5 +1,6 @@ {{ feature_flags|json_script:"feature-flags" }} {{ required_default|json_script:'config-REQUIRED_DEFAULT' }} {{ rich_text_colors|json_script:'config-RICH_TEXT_COLORS' }} +{{ map_tile_layers|json_script:'config-MAP_TILE_LAYERS' }} {{ upload_filetypes|json_script:'config-UPLOAD_FILETYPES' }} {{ confidentiality_levels|json_script:'CONFIDENTIALITY_LEVELS' }} diff --git a/src/openforms/js/components/form/map.js b/src/openforms/js/components/form/map.js index af1ffbd2fa..f9aa737ed8 100644 --- a/src/openforms/js/components/form/map.js +++ b/src/openforms/js/components/form/map.js @@ -5,6 +5,8 @@ import {CRS_RD, TILE_LAYER_RD} from '@open-formulieren/leaflet-tools'; import * as L from 'leaflet'; import {Formio} from 'react-formio'; +import jsonScriptToVar from 'utils/json-script'; + import {localiseSchema} from './i18n'; const TextFieldComponent = Formio.Components.components.textfield; @@ -17,6 +19,10 @@ const MAP_DEFAULTS = { zoom: 3, }; +const MAP_TILE_LAYERS = jsonScriptToVar('config-MAP_TILE_LAYERS', { + default: [], +}); + export default class Map extends TextFieldComponent { static schema(...extend) { const schema = TextFieldComponent.schema( @@ -70,8 +76,23 @@ export default class Map extends TextFieldComponent { const map = L.map(`map-${this.id}`, MAP_DEFAULTS); - const {url: tileUrl, ...options} = TILE_LAYER_RD; - const tiles = L.tileLayer(tileUrl, options); + const tiles = L.tileLayer( + this.getTileLayerUrl(this.originalComponent?.tileLayerIdentifier), + TILE_LAYER_RD + ); map.addLayer(tiles); } + + // Try to get the tile layer url for the component. + // If it cannot be found, return the default url. + getTileLayerUrl(tileLayerIdentifier) { + if (!Array.isArray(MAP_TILE_LAYERS) || MAP_TILE_LAYERS.length === 0 || !tileLayerIdentifier) { + return TILE_LAYER_RD.url; + } + + return ( + MAP_TILE_LAYERS.find(tileLayer => tileLayer?.identifier === tileLayerIdentifier)?.url ?? + TILE_LAYER_RD.url + ); + } } diff --git a/src/openforms/js/components/formio_builder/WebformBuilder.js b/src/openforms/js/components/formio_builder/WebformBuilder.js index fe598f4abe..c48b1e2811 100644 --- a/src/openforms/js/components/formio_builder/WebformBuilder.js +++ b/src/openforms/js/components/formio_builder/WebformBuilder.js @@ -37,6 +37,7 @@ const CONFIDENTIALITY_LEVELS = jsonScriptToVar('CONFIDENTIALITY_LEVELS', {defaul const FILE_TYPES = jsonScriptToVar('config-UPLOAD_FILETYPES', {default: []}); const MAX_FILE_UPLOAD_SIZE = jsonScriptToVar('setting-MAX_FILE_UPLOAD_SIZE', {default: 'unknown'}); const RICH_TEXT_COLORS = jsonScriptToVar('config-RICH_TEXT_COLORS', {default: []}); +const MAP_TILE_LAYERS = jsonScriptToVar('config-MAP_TILE_LAYERS', {default: []}); const WebformBuilderFormio = Formio.Builders.builders.webform; @@ -162,6 +163,7 @@ class WebformBuilder extends WebformBuilderFormio { supportedLanguageCodes={LANGUAGES} theme={currentTheme.getValue()} richTextColors={RICH_TEXT_COLORS} + getMapTileLayers={async () => MAP_TILE_LAYERS} getFormComponents={() => this.webform.form.components} getValidatorPlugins={getValidatorPlugins} getRegistrationAttributes={getRegistrationAttributes}