Skip to content

Commit 1c03258

Browse files
committed
[#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. Additionally, the postcode and housenumber fields are always both required or none.
1 parent 46f3859 commit 1c03258

File tree

5 files changed

+163
-8
lines changed

5 files changed

+163
-8
lines changed

src/openforms/contrib/brk/constants.py

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ class AddressValue(TypedDict):
99
city: NotRequired[str]
1010
streetName: NotRequired[str]
1111
secretStreetCity: NotRequired[str]
12+
autoPopulated: NotRequired[bool]

src/openforms/formio/components/custom.py

+39-4
Original file line numberDiff line numberDiff line change
@@ -425,12 +425,36 @@ class AddressValueSerializer(serializers.Serializer):
425425
required=False,
426426
allow_blank=True,
427427
)
428+
autoPopulated = serializers.BooleanField(
429+
label=_("city and street name auto populated"),
430+
help_text=_("Whether city and street name have been retrieved from the API"),
431+
default=False,
432+
)
428433

429434
def __init__(self, **kwargs):
430435
self.derive_address = kwargs.pop("derive_address", None)
431436
self.component = kwargs.pop("component", None)
432437
super().__init__(**kwargs)
433438

439+
def get_fields(self):
440+
fields = super().get_fields()
441+
442+
# Some fields have to be treated as required or not dynamically and based on
443+
# specific situations.
444+
if self.component and (validate := self.component.get("validate")):
445+
if validate["required"] is True:
446+
if self.derive_address:
447+
fields["city"].required = True
448+
fields["city"].allow_blank = False
449+
fields["streetName"].required = True
450+
fields["streetName"].allow_blank = False
451+
elif validate["required"] is False:
452+
fields["postcode"].required = False
453+
fields["postcode"].allow_blank = True
454+
fields["houseNumber"].required = False
455+
fields["houseNumber"].allow_blank = True
456+
return fields
457+
434458
def validate_city(self, value: str) -> str:
435459
if city_regex := glom(
436460
self.component, "openForms.components.city.validate.pattern", default=""
@@ -457,18 +481,29 @@ def validate_postcode(self, value: str) -> str:
457481
def validate(self, attrs):
458482
attrs = super().validate(attrs)
459483

484+
auto_populated = attrs.get("autoPopulated", False)
485+
postcode = attrs.get("postcode", "")
486+
house_number = attrs.get("houseNumber", "")
460487
city = attrs.get("city", "")
461488
street_name = attrs.get("streetName", "")
462489

490+
if (postcode and not house_number) or (not postcode and house_number):
491+
raise serializers.ValidationError(
492+
_("Both postcode and house number are required or none of them"),
493+
code="required",
494+
)
495+
463496
if self.derive_address:
464-
existing_hmac = attrs.get("secretStreetCity", "")
465-
postcode = attrs.get("postcode", "")
466-
number = attrs.get("houseNumber", "")
497+
# when the user fills in manually the city and the street name we do not
498+
# need to check the secret city - street name combination
499+
if not auto_populated:
500+
return attrs
467501

502+
existing_hmac = attrs.get("secretStreetCity", "")
468503
computed_hmac = salt_location_message(
469504
{
470505
"postcode": postcode,
471-
"number": number,
506+
"number": house_number,
472507
"city": city,
473508
"street_name": street_name,
474509
}

src/openforms/formio/formatters/custom.py

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class AddressValue(TypedDict):
4242
city: NotRequired[str]
4343
streetName: NotRequired[str]
4444
secretStreetCity: NotRequired[str]
45+
autoPopulated: NotRequired[bool]
4546

4647

4748
class AddressNLFormatter(FormatterBase):

src/openforms/formio/tests/validation/test_addressnl.py

+113-3
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,88 @@ def test_addressNL_field_regex_pattern_success(self):
106106

107107
self.assertTrue(is_valid)
108108

109-
def test_missing_keys(self):
109+
def test_missing_keys_when_component_optional(self):
110110
component: AddressNLComponent = {
111111
"key": "addressNl",
112112
"type": "addressNL",
113113
"label": "AddressNL missing keys",
114114
"deriveAddress": False,
115+
"validate": {"required": False},
116+
}
117+
118+
data = {
119+
"addressNl": {
120+
"houseLetter": "A",
121+
}
122+
}
123+
124+
is_valid, _ = validate_formio_data(component, data)
125+
126+
self.assertTrue(is_valid)
127+
128+
def test_postcode_housenumber_combination(self):
129+
component: AddressNLComponent = {
130+
"key": "addressNl",
131+
"type": "addressNL",
132+
"label": "AddressNL missing keys",
133+
"deriveAddress": False,
134+
"validate": {"required": False},
135+
}
136+
137+
with self.subTest("valid postcode and missing housenumber"):
138+
data = {
139+
"addressNl": {
140+
"postcode": "1234AA",
141+
}
142+
}
143+
144+
is_valid, errors = validate_formio_data(component, data)
145+
146+
combination_error = extract_error(errors["addressNl"], "non_field_errors")
147+
148+
self.assertFalse(is_valid)
149+
self.assertEqual(combination_error.code, "required")
150+
151+
with self.subTest("missing postcode and valid housenumber"):
152+
data = {
153+
"addressNl": {
154+
"houseNumber": "2",
155+
}
156+
}
157+
158+
is_valid, errors = validate_formio_data(component, data)
159+
160+
combination_error = extract_error(errors["addressNl"], "non_field_errors")
161+
162+
self.assertFalse(is_valid)
163+
self.assertEqual(combination_error.code, "required")
164+
165+
def test_missing_keys_when_autofill_and_component_optional(self):
166+
component: AddressNLComponent = {
167+
"key": "addressNl",
168+
"type": "addressNL",
169+
"label": "AddressNL missing keys",
170+
"deriveAddress": True,
171+
"validate": {"required": False},
172+
}
173+
174+
data = {
175+
"addressNl": {
176+
"houseLetter": "A",
177+
}
178+
}
179+
180+
is_valid, _ = validate_formio_data(component, data)
181+
182+
self.assertTrue(is_valid)
183+
184+
def test_missing_keys_when_component_required(self):
185+
component: AddressNLComponent = {
186+
"key": "addressNl",
187+
"type": "addressNL",
188+
"label": "AddressNL missing keys",
189+
"deriveAddress": True,
190+
"validate": {"required": True},
115191
}
116192

117193
invalid_values = {
@@ -124,10 +200,14 @@ def test_missing_keys(self):
124200

125201
postcode_error = extract_error(errors["addressNl"], "postcode")
126202
house_number_error = extract_error(errors["addressNl"], "houseNumber")
203+
street_name_error = extract_error(errors["addressNl"], "streetName")
204+
city_error = extract_error(errors["addressNl"], "city")
127205

128206
self.assertFalse(is_valid)
129207
self.assertEqual(postcode_error.code, "required")
130208
self.assertEqual(house_number_error.code, "required")
209+
self.assertEqual(street_name_error.code, "required")
210+
self.assertEqual(city_error.code, "required")
131211

132212
def test_plugin_validator(self):
133213
with replace_validators_registry() as register:
@@ -138,7 +218,7 @@ def test_plugin_validator(self):
138218
"type": "addressNL",
139219
"label": "AddressNL plugin validator",
140220
"deriveAddress": False,
141-
"validate": {"plugins": ["postcode_validator"]},
221+
"validate": {"required": False, "plugins": ["postcode_validator"]},
142222
}
143223

144224
with self.subTest("valid value"):
@@ -150,6 +230,8 @@ def test_plugin_validator(self):
150230
"houseNumber": "3",
151231
"houseLetter": "A",
152232
"houseNumberAddition": "",
233+
"streetName": "Keizersgracht",
234+
"city": "Amsterdam",
153235
}
154236
},
155237
)
@@ -176,7 +258,7 @@ def test_addressNL_field_secret_success(self):
176258
"key": "addressNl",
177259
"type": "addressNL",
178260
"label": "AddressNL secret success",
179-
"deriveAddress": False,
261+
"deriveAddress": True,
180262
}
181263

182264
message = "1015CJ/117/Amsterdam/Keizersgracht"
@@ -190,6 +272,7 @@ def test_addressNL_field_secret_success(self):
190272
"city": "Amsterdam",
191273
"streetName": "Keizersgracht",
192274
"secretStreetCity": secret,
275+
"autoPopulated": True,
193276
}
194277
}
195278

@@ -214,6 +297,7 @@ def test_addressNL_field_secret_failure(self):
214297
"city": "Amsterdam",
215298
"streetName": "Keizersgracht",
216299
"secretStreetCity": "invalid secret",
300+
"autoPopulated": True,
217301
}
218302
}
219303

@@ -224,6 +308,32 @@ def test_addressNL_field_secret_failure(self):
224308
self.assertFalse(is_valid)
225309
self.assertEqual(secret_error.code, "invalid")
226310

311+
def test_addressNL_field_secret_not_used_when_manual_address(self):
312+
component: AddressNLComponent = {
313+
"key": "addressNl",
314+
"type": "addressNL",
315+
"label": "AddressNL secret failure",
316+
"deriveAddress": True,
317+
"validate": {"required": False},
318+
}
319+
320+
data = {
321+
"addressNl": {
322+
"postcode": "1015CJ",
323+
"houseNumber": "117",
324+
"houseLetter": "",
325+
"houseNumberAddition": "",
326+
"city": "Amsterdam",
327+
"streetName": "Keizersgracht",
328+
"secretStreetCity": "a secret",
329+
"autoPopulated": False,
330+
}
331+
}
332+
333+
is_valid, _ = validate_formio_data(component, data)
334+
335+
self.assertTrue(is_valid)
336+
227337
def test_addressNL_field_missing_city(self):
228338
component: AddressNLComponent = {
229339
"key": "addressNl",

src/openforms/tests/e2e/test_input_validation.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -979,14 +979,21 @@ def assertAddressNLValidationIsAligned(
979979
ui_inputs: dict[str, str],
980980
expected_ui_error: str,
981981
api_value: dict[str, Any],
982+
expected_error_keys: list[str] = [],
982983
) -> None:
983984
form = create_form(component)
984985

985986
with self.subTest("frontend validation"):
986987
self._assertAddressNLFrontendValidation(form, ui_inputs, expected_ui_error)
987988

988989
with self.subTest("backend validation"):
989-
self._assertBackendValidation(form, component["key"], api_value)
990+
if not expected_error_keys:
991+
self._assertBackendValidation(form, component["key"], api_value)
992+
else:
993+
for key in expected_error_keys:
994+
self._assertBackendValidation(
995+
form, f"{component['key']}.{key}", api_value
996+
)
990997

991998
@async_to_sync
992999
async def _assertAddressNLFrontendValidation(
@@ -1026,6 +1033,7 @@ def test_required_field(self):
10261033
"houseLetter": "",
10271034
"houseNumberAddition": "",
10281035
},
1036+
expected_error_keys=["postcode", "houseNumber"],
10291037
expected_ui_error="Het verplichte veld Required AddressNL is niet ingevuld.",
10301038
)
10311039

0 commit comments

Comments
 (0)