Skip to content

Commit 8d660e6

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 934bb7a commit 8d660e6

File tree

8 files changed

+206
-18
lines changed

8 files changed

+206
-18
lines changed

package-lock.json

+7-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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
@@ -490,12 +490,36 @@ class AddressValueSerializer(serializers.Serializer):
490490
required=False,
491491
allow_blank=True,
492492
)
493+
autoPopulated = serializers.BooleanField(
494+
label=_("city and street name auto populated"),
495+
help_text=_("Whether city and street name have been retrieved from the API"),
496+
default=False,
497+
)
493498

494499
def __init__(self, **kwargs):
495500
self.derive_address = kwargs.pop("derive_address", None)
496501
self.component = kwargs.pop("component", None)
497502
super().__init__(**kwargs)
498503

504+
def get_fields(self):
505+
fields = super().get_fields()
506+
507+
# Some fields have to be treated as required or not dynamically and based on
508+
# specific situations.
509+
if self.component and (validate := self.component.get("validate")):
510+
if validate["required"] is True:
511+
if self.derive_address:
512+
fields["city"].required = True
513+
fields["city"].allow_blank = False
514+
fields["streetName"].required = True
515+
fields["streetName"].allow_blank = False
516+
elif validate["required"] is False:
517+
fields["postcode"].required = False
518+
fields["postcode"].allow_blank = True
519+
fields["houseNumber"].required = False
520+
fields["houseNumber"].allow_blank = True
521+
return fields
522+
499523
def validate_city(self, value: str) -> str:
500524
if city_regex := glom(
501525
self.component, "openForms.components.city.validate.pattern", default=""
@@ -522,18 +546,46 @@ def validate_postcode(self, value: str) -> str:
522546
def validate(self, attrs):
523547
attrs = super().validate(attrs)
524548

549+
auto_populated = attrs.get("autoPopulated", False)
550+
postcode = attrs.get("postcode", "")
551+
house_number = attrs.get("houseNumber", "")
525552
city = attrs.get("city", "")
526553
street_name = attrs.get("streetName", "")
527554

555+
# Allow users to save(pause) the form even if one of the fields is missing.
556+
# We validate the combination of them only during the subission of the form.
557+
if self.context.get("validate_on_complete", False):
558+
if postcode and not house_number:
559+
raise serializers.ValidationError(
560+
{
561+
"houseNumber": _(
562+
'This field is required if "postcode" is provided'
563+
)
564+
},
565+
code="required",
566+
)
567+
568+
if not postcode and house_number:
569+
raise serializers.ValidationError(
570+
{
571+
"postcode": _(
572+
'This field is required if "house number" is provided'
573+
)
574+
},
575+
code="required",
576+
)
577+
528578
if self.derive_address:
529-
existing_hmac = attrs.get("secretStreetCity", "")
530-
postcode = attrs.get("postcode", "")
531-
number = attrs.get("houseNumber", "")
579+
# When the user fills in manually the city and the street name we do not
580+
# need to check the secret city - street name combination
581+
if not auto_populated:
582+
return attrs
532583

584+
existing_hmac = attrs.get("secretStreetCity", "")
533585
computed_hmac = salt_location_message(
534586
{
535587
"postcode": postcode,
536-
"number": number,
588+
"number": house_number,
537589
"city": city,
538590
"street_name": street_name,
539591
}

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(
@@ -1029,6 +1036,7 @@ def test_required_field(self):
10291036
"houseNumberAddition": "",
10301037
},
10311038
expected_ui_error="Dit veld mag niet leeg zijn.",
1039+
expected_error_keys=["postcode", "houseNumber"],
10321040
)
10331041

10341042
def test_regex_failure(self):

0 commit comments

Comments
 (0)