From eba53a2a694e579475f8cdcb49342aced7bf13cf Mon Sep 17 00:00:00 2001 From: vasileios Date: Wed, 22 Jan 2025 15:28:26 +0100 Subject: [PATCH] [#5006] Updated AddressValueSerializer to adapt manual filling of streetname and city Updated street name and city serializer fields in order to be able to require these fields in case the component is required. --- src/openforms/contrib/brk/constants.py | 1 + src/openforms/formio/components/custom.py | 43 ++++++- src/openforms/formio/formatters/custom.py | 1 + .../formio/tests/validation/test_addressnl.py | 116 +++++++++++++++++- 4 files changed, 154 insertions(+), 7 deletions(-) diff --git a/src/openforms/contrib/brk/constants.py b/src/openforms/contrib/brk/constants.py index 9fef1d3f96..493ade886a 100644 --- a/src/openforms/contrib/brk/constants.py +++ b/src/openforms/contrib/brk/constants.py @@ -9,3 +9,4 @@ class AddressValue(TypedDict): city: NotRequired[str] streetName: NotRequired[str] secretStreetCity: NotRequired[str] + autoPopulated: NotRequired[bool] diff --git a/src/openforms/formio/components/custom.py b/src/openforms/formio/components/custom.py index e36f585677..08bff2ddb2 100644 --- a/src/openforms/formio/components/custom.py +++ b/src/openforms/formio/components/custom.py @@ -425,12 +425,36 @@ class AddressValueSerializer(serializers.Serializer): required=False, allow_blank=True, ) + autoPopulated = serializers.BooleanField( + label=_("city and street name auto populated"), + help_text=_("Whether city and street name have been retrieved from the API"), + default=False, + ) def __init__(self, **kwargs): self.derive_address = kwargs.pop("derive_address", None) self.component = kwargs.pop("component", None) super().__init__(**kwargs) + def get_fields(self): + fields = super().get_fields() + + # Some fields have to be treated as required or not dynamically and based on + # specific situations. + if self.component and (validate := self.component.get("validate")): + if validate["required"] is True: + if self.derive_address: + fields["city"].required = True + fields["city"].allow_blank = False + fields["streetName"].required = True + fields["streetName"].allow_blank = False + elif validate["required"] is False: + fields["postcode"].required = False + fields["postcode"].allow_blank = True + fields["houseNumber"].required = False + fields["houseNumber"].allow_blank = True + return fields + def validate_city(self, value: str) -> str: if city_regex := glom( self.component, "openForms.components.city.validate.pattern", default="" @@ -457,18 +481,29 @@ def validate_postcode(self, value: str) -> str: def validate(self, attrs): attrs = super().validate(attrs) + auto_populated = attrs.get("autoPopulated", False) + postcode = attrs.get("postcode", "") + house_number = attrs.get("houseNumber", "") city = attrs.get("city", "") street_name = attrs.get("streetName", "") + if (postcode and not house_number) or (not postcode and house_number): + raise serializers.ValidationError( + _("Both postcode and house number are required or none of them"), + code="required", + ) + if self.derive_address: - existing_hmac = attrs.get("secretStreetCity", "") - postcode = attrs.get("postcode", "") - number = attrs.get("houseNumber", "") + # when the user fills in manually the city and the street name we do not + # need to check the secret city - street name combination + if not auto_populated: + return attrs + existing_hmac = attrs.get("secretStreetCity", "") computed_hmac = salt_location_message( { "postcode": postcode, - "number": number, + "number": house_number, "city": city, "street_name": street_name, } diff --git a/src/openforms/formio/formatters/custom.py b/src/openforms/formio/formatters/custom.py index 6c87744fc9..ce5d0f1963 100644 --- a/src/openforms/formio/formatters/custom.py +++ b/src/openforms/formio/formatters/custom.py @@ -42,6 +42,7 @@ class AddressValue(TypedDict): city: NotRequired[str] streetName: NotRequired[str] secretStreetCity: NotRequired[str] + autoPopulated: NotRequired[bool] class AddressNLFormatter(FormatterBase): diff --git a/src/openforms/formio/tests/validation/test_addressnl.py b/src/openforms/formio/tests/validation/test_addressnl.py index 365dd49a83..296f0d1ad3 100644 --- a/src/openforms/formio/tests/validation/test_addressnl.py +++ b/src/openforms/formio/tests/validation/test_addressnl.py @@ -106,12 +106,88 @@ def test_addressNL_field_regex_pattern_success(self): self.assertTrue(is_valid) - def test_missing_keys(self): + def test_missing_keys_when_component_optional(self): component: AddressNLComponent = { "key": "addressNl", "type": "addressNL", "label": "AddressNL missing keys", "deriveAddress": False, + "validate": {"required": False}, + } + + data = { + "addressNl": { + "houseLetter": "A", + } + } + + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid) + + def test_postcode_housenumber_combination(self): + component: AddressNLComponent = { + "key": "addressNl", + "type": "addressNL", + "label": "AddressNL missing keys", + "deriveAddress": False, + "validate": {"required": False}, + } + + with self.subTest("valid postcode and missing housenumber"): + data = { + "addressNl": { + "postcode": "1234AA", + } + } + + is_valid, errors = validate_formio_data(component, data) + + combination_error = extract_error(errors["addressNl"], "non_field_errors") + + self.assertFalse(is_valid) + self.assertEqual(combination_error.code, "required") + + with self.subTest("missing postcode and valid housenumber"): + data = { + "addressNl": { + "houseNumber": "2", + } + } + + is_valid, errors = validate_formio_data(component, data) + + combination_error = extract_error(errors["addressNl"], "non_field_errors") + + self.assertFalse(is_valid) + self.assertEqual(combination_error.code, "required") + + def test_missing_keys_when_autofill_and_component_optional(self): + component: AddressNLComponent = { + "key": "addressNl", + "type": "addressNL", + "label": "AddressNL missing keys", + "deriveAddress": True, + "validate": {"required": False}, + } + + data = { + "addressNl": { + "houseLetter": "A", + } + } + + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid) + + def test_missing_keys_when_component_required(self): + component: AddressNLComponent = { + "key": "addressNl", + "type": "addressNL", + "label": "AddressNL missing keys", + "deriveAddress": True, + "validate": {"required": True}, } invalid_values = { @@ -124,10 +200,14 @@ def test_missing_keys(self): postcode_error = extract_error(errors["addressNl"], "postcode") house_number_error = extract_error(errors["addressNl"], "houseNumber") + street_name_error = extract_error(errors["addressNl"], "streetName") + city_error = extract_error(errors["addressNl"], "city") self.assertFalse(is_valid) self.assertEqual(postcode_error.code, "required") self.assertEqual(house_number_error.code, "required") + self.assertEqual(street_name_error.code, "required") + self.assertEqual(city_error.code, "required") def test_plugin_validator(self): with replace_validators_registry() as register: @@ -138,7 +218,7 @@ def test_plugin_validator(self): "type": "addressNL", "label": "AddressNL plugin validator", "deriveAddress": False, - "validate": {"plugins": ["postcode_validator"]}, + "validate": {"required": False, "plugins": ["postcode_validator"]}, } with self.subTest("valid value"): @@ -150,6 +230,8 @@ def test_plugin_validator(self): "houseNumber": "3", "houseLetter": "A", "houseNumberAddition": "", + "streetName": "Keizersgracht", + "city": "Amsterdam", } }, ) @@ -176,7 +258,7 @@ def test_addressNL_field_secret_success(self): "key": "addressNl", "type": "addressNL", "label": "AddressNL secret success", - "deriveAddress": False, + "deriveAddress": True, } message = "1015CJ/117/Amsterdam/Keizersgracht" @@ -190,6 +272,7 @@ def test_addressNL_field_secret_success(self): "city": "Amsterdam", "streetName": "Keizersgracht", "secretStreetCity": secret, + "autoPopulated": True, } } @@ -214,6 +297,7 @@ def test_addressNL_field_secret_failure(self): "city": "Amsterdam", "streetName": "Keizersgracht", "secretStreetCity": "invalid secret", + "autoPopulated": True, } } @@ -224,6 +308,32 @@ def test_addressNL_field_secret_failure(self): self.assertFalse(is_valid) self.assertEqual(secret_error.code, "invalid") + def test_addressNL_field_secret_not_used_when_manual_address(self): + component: AddressNLComponent = { + "key": "addressNl", + "type": "addressNL", + "label": "AddressNL secret failure", + "deriveAddress": True, + "validate": {"required": False}, + } + + data = { + "addressNl": { + "postcode": "1015CJ", + "houseNumber": "117", + "houseLetter": "", + "houseNumberAddition": "", + "city": "Amsterdam", + "streetName": "Keizersgracht", + "secretStreetCity": "a secret", + "autoPopulated": False, + } + } + + is_valid, _ = validate_formio_data(component, data) + + self.assertTrue(is_valid) + def test_addressNL_field_missing_city(self): component: AddressNLComponent = { "key": "addressNl",