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..bd2d1a667d 100644 --- a/src/openforms/formio/components/custom.py +++ b/src/openforms/formio/components/custom.py @@ -425,12 +425,39 @@ 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 self.derive_address + and (validate := self.component.get("validate")) + ): + if validate["required"] is True: + 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,10 +484,16 @@ def validate_postcode(self, value: str) -> str: def validate(self, attrs): attrs = super().validate(attrs) + auto_populated = attrs.get("autoPopulated", False) city = attrs.get("city", "") street_name = attrs.get("streetName", "") if self.derive_address: + # 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", "") postcode = attrs.get("postcode", "") number = attrs.get("houseNumber", "") @@ -501,6 +534,8 @@ def build_serializer_field( validators.append(PluginValidator(plugin_ids)) extra["validators"] = validators + # if the component is not required, none of the fields should be required + # bakend bug: saving addressnl required -> should be able to save it with partial data return AddressValueSerializer( derive_address=component["deriveAddress"], 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..73442e1a14 100644 --- a/src/openforms/formio/tests/validation/test_addressnl.py +++ b/src/openforms/formio/tests/validation/test_addressnl.py @@ -106,12 +106,13 @@ 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}, } invalid_values = { @@ -129,6 +130,53 @@ def test_missing_keys(self): self.assertEqual(postcode_error.code, "required") self.assertEqual(house_number_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}, + } + + invalid_values = { + "addressNl": { + "houseLetter": "A", + } + } + + is_valid, _ = validate_formio_data(component, invalid_values) + + 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 = { + "addressNl": { + "houseLetter": "A", + } + } + + is_valid, errors = validate_formio_data(component, invalid_values) + + 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: register("postcode_validator")(PostcodeValidator) @@ -150,6 +198,8 @@ def test_plugin_validator(self): "houseNumber": "3", "houseLetter": "A", "houseNumberAddition": "", + "streetName": "Keizersgracht", + "city": "Amsterdam", } }, ) @@ -176,7 +226,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 +240,7 @@ def test_addressNL_field_secret_success(self): "city": "Amsterdam", "streetName": "Keizersgracht", "secretStreetCity": secret, + "autoPopulated": True, } } @@ -214,6 +265,7 @@ def test_addressNL_field_secret_failure(self): "city": "Amsterdam", "streetName": "Keizersgracht", "secretStreetCity": "invalid secret", + "autoPopulated": True, } } @@ -224,6 +276,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",