Skip to content

Commit e645729

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.
1 parent 1658eca commit e645729

File tree

7 files changed

+199
-10
lines changed

7 files changed

+199
-10
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"dependencies": {
3131
"@fortawesome/fontawesome-free": "^6.1.1",
3232
"@open-formulieren/design-tokens": "^0.53.0",
33-
"@open-formulieren/formio-builder": "^0.35.0",
33+
"@open-formulieren/formio-builder": "^0.36.0",
3434
"@open-formulieren/leaflet-tools": "^1.0.0",
3535
"@open-formulieren/monaco-json-editor": "^0.2.0",
3636
"@tinymce/tinymce-react": "^4.3.2",

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

+56-4
Original file line numberDiff line numberDiff line change
@@ -427,12 +427,36 @@ class AddressValueSerializer(serializers.Serializer):
427427
required=False,
428428
allow_blank=True,
429429
)
430+
autoPopulated = serializers.BooleanField(
431+
label=_("city and street name auto populated"),
432+
help_text=_("Whether city and street name have been retrieved from the API"),
433+
default=False,
434+
)
430435

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

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

486+
auto_populated = attrs.get("autoPopulated", False)
487+
postcode = attrs.get("postcode", "")
488+
house_number = attrs.get("houseNumber", "")
462489
city = attrs.get("city", "")
463490
street_name = attrs.get("streetName", "")
464491

492+
# Allow users to save(pause) the form even if one of the fields is missing.
493+
# We validate the combination of them only during the subission of the form.
494+
if self.context.get("validate_on_complete", False):
495+
if postcode and not house_number:
496+
raise serializers.ValidationError(
497+
{
498+
"houseNumber": _(
499+
'This field is required if "postcode" is provided'
500+
)
501+
},
502+
code="required",
503+
)
504+
505+
if not postcode and house_number:
506+
raise serializers.ValidationError(
507+
{
508+
"postcode": _(
509+
'This field is required if "house number" is provided'
510+
)
511+
},
512+
code="required",
513+
)
514+
465515
if self.derive_address:
466-
existing_hmac = attrs.get("secretStreetCity", "")
467-
postcode = attrs.get("postcode", "")
468-
number = attrs.get("houseNumber", "")
516+
# When the user fills in manually the city and the street name we do not
517+
# need to check the secret city - street name combination
518+
if not auto_populated:
519+
return attrs
469520

521+
existing_hmac = attrs.get("secretStreetCity", "")
470522
computed_hmac = salt_location_message(
471523
{
472524
"postcode": postcode,
473-
"number": number,
525+
"number": house_number,
474526
"city": city,
475527
"street_name": street_name,
476528
}

src/openforms/formio/formatters/custom.py

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class AddressValue(TypedDict):
5050
city: NotRequired[str]
5151
streetName: NotRequired[str]
5252
secretStreetCity: NotRequired[str]
53+
autoPopulated: NotRequired[bool]
5354

5455

5556
class AddressNLFormatter(FormatterBase):

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

+130-3
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,51 @@ 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_missing_keys_when_autofill_enabled_and_component_optional(self):
129+
component: AddressNLComponent = {
130+
"key": "addressNl",
131+
"type": "addressNL",
132+
"label": "AddressNL missing keys",
133+
"deriveAddress": True,
134+
"validate": {"required": False},
135+
}
136+
137+
data = {
138+
"addressNl": {
139+
"houseLetter": "A",
140+
}
141+
}
142+
143+
is_valid, _ = validate_formio_data(component, data)
144+
145+
self.assertTrue(is_valid)
146+
147+
def test_missing_keys_when_component_required(self):
148+
component: AddressNLComponent = {
149+
"key": "addressNl",
150+
"type": "addressNL",
151+
"label": "AddressNL missing keys",
152+
"deriveAddress": True,
153+
"validate": {"required": True},
115154
}
116155

117156
invalid_values = {
@@ -124,10 +163,14 @@ def test_missing_keys(self):
124163

125164
postcode_error = extract_error(errors["addressNl"], "postcode")
126165
house_number_error = extract_error(errors["addressNl"], "houseNumber")
166+
street_name_error = extract_error(errors["addressNl"], "streetName")
167+
city_error = extract_error(errors["addressNl"], "city")
127168

128169
self.assertFalse(is_valid)
129170
self.assertEqual(postcode_error.code, "required")
130171
self.assertEqual(house_number_error.code, "required")
172+
self.assertEqual(street_name_error.code, "required")
173+
self.assertEqual(city_error.code, "required")
131174

132175
def test_plugin_validator(self):
133176
with replace_validators_registry() as register:
@@ -138,7 +181,7 @@ def test_plugin_validator(self):
138181
"type": "addressNL",
139182
"label": "AddressNL plugin validator",
140183
"deriveAddress": False,
141-
"validate": {"plugins": ["postcode_validator"]},
184+
"validate": {"required": False, "plugins": ["postcode_validator"]},
142185
}
143186

144187
with self.subTest("valid value"):
@@ -150,6 +193,8 @@ def test_plugin_validator(self):
150193
"houseNumber": "3",
151194
"houseLetter": "A",
152195
"houseNumberAddition": "",
196+
"streetName": "Keizersgracht",
197+
"city": "Amsterdam",
153198
}
154199
},
155200
)
@@ -171,12 +216,66 @@ def test_plugin_validator(self):
171216

172217
self.assertFalse(is_valid)
173218

219+
def test_non_required_postcode_is_required_if_houseNumber_is_provided(
220+
self,
221+
):
222+
component: AddressNLComponent = {
223+
"key": "addressNl",
224+
"type": "addressNL",
225+
"label": "AddressNL",
226+
"deriveAddress": False,
227+
}
228+
229+
invalid_values = {
230+
"addressNl": {
231+
"postcode": "",
232+
"houseNumber": "117",
233+
"houseLetter": "",
234+
"houseNumberAddition": "",
235+
"city": "Amsterdam",
236+
"streetName": "",
237+
}
238+
}
239+
240+
is_valid, errors = validate_formio_data(component, invalid_values)
241+
postcode_error = extract_error(errors["addressNl"], "postcode")
242+
243+
self.assertFalse(is_valid)
244+
self.assertEqual(postcode_error.code, "blank")
245+
246+
def test_non_required_house_number_is_required_if_postcode_is_provided(
247+
self,
248+
):
249+
component: AddressNLComponent = {
250+
"key": "addressNl",
251+
"type": "addressNL",
252+
"label": "AddressNL",
253+
"deriveAddress": False,
254+
}
255+
256+
invalid_values = {
257+
"addressNl": {
258+
"postcode": "1234 AB",
259+
"houseNumber": "",
260+
"houseLetter": "",
261+
"houseNumberAddition": "",
262+
"city": "Amsterdam",
263+
"streetName": "",
264+
}
265+
}
266+
267+
is_valid, errors = validate_formio_data(component, invalid_values)
268+
house_number_error = extract_error(errors["addressNl"], "houseNumber")
269+
270+
self.assertFalse(is_valid)
271+
self.assertEqual(house_number_error.code, "blank")
272+
174273
def test_addressNL_field_secret_success(self):
175274
component: AddressNLComponent = {
176275
"key": "addressNl",
177276
"type": "addressNL",
178277
"label": "AddressNL secret success",
179-
"deriveAddress": False,
278+
"deriveAddress": True,
180279
}
181280

182281
message = "1015CJ/117/Amsterdam/Keizersgracht"
@@ -190,6 +289,7 @@ def test_addressNL_field_secret_success(self):
190289
"city": "Amsterdam",
191290
"streetName": "Keizersgracht",
192291
"secretStreetCity": secret,
292+
"autoPopulated": True,
193293
}
194294
}
195295

@@ -214,6 +314,7 @@ def test_addressNL_field_secret_failure(self):
214314
"city": "Amsterdam",
215315
"streetName": "Keizersgracht",
216316
"secretStreetCity": "invalid secret",
317+
"autoPopulated": True,
217318
}
218319
}
219320

@@ -224,6 +325,32 @@ def test_addressNL_field_secret_failure(self):
224325
self.assertFalse(is_valid)
225326
self.assertEqual(secret_error.code, "invalid")
226327

328+
def test_addressNL_field_secret_not_used_when_manual_address(self):
329+
component: AddressNLComponent = {
330+
"key": "addressNl",
331+
"type": "addressNL",
332+
"label": "AddressNL secret failure",
333+
"deriveAddress": True,
334+
"validate": {"required": False},
335+
}
336+
337+
data = {
338+
"addressNl": {
339+
"postcode": "1015CJ",
340+
"houseNumber": "117",
341+
"houseLetter": "",
342+
"houseNumberAddition": "",
343+
"city": "Amsterdam",
344+
"streetName": "Keizersgracht",
345+
"secretStreetCity": "a secret",
346+
"autoPopulated": False,
347+
}
348+
}
349+
350+
is_valid, _ = validate_formio_data(component, data)
351+
352+
self.assertTrue(is_valid)
353+
227354
def test_addressNL_field_missing_city(self):
228355
component: AddressNLComponent = {
229356
"key": "addressNl",

src/openforms/submissions/api/serializers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def _run_formio_validation(self, data: dict) -> None:
300300
step_data_serializer = build_serializer(
301301
configuration["components"],
302302
data=data,
303-
context={"submission": submission},
303+
context={"submission": submission, "validate_on_complete": False},
304304
)
305305
step_data_serializer.is_valid(raise_exception=True)
306306

src/openforms/tests/e2e/test_input_validation.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -981,14 +981,21 @@ def assertAddressNLValidationIsAligned(
981981
ui_inputs: dict[str, str],
982982
expected_ui_error: str,
983983
api_value: dict[str, Any],
984+
expected_error_keys: list[str] = [],
984985
) -> None:
985986
form = create_form(component)
986987

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

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

9931000
@async_to_sync
9941001
async def _assertAddressNLFrontendValidation(
@@ -1028,6 +1035,7 @@ def test_required_field(self):
10281035
"houseLetter": "",
10291036
"houseNumberAddition": "",
10301037
},
1038+
expected_error_keys=["postcode", "houseNumber"],
10311039
expected_ui_error="Het verplichte veld Required AddressNL is niet ingevuld.",
10321040
)
10331041

0 commit comments

Comments
 (0)