Skip to content

Commit

Permalink
[#3993] Added deriveAddress for addressNL component
Browse files Browse the repository at this point in the history
  • Loading branch information
vaszig committed Jun 11, 2024
1 parent a27ae39 commit 76b4f6a
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 10 deletions.
49 changes: 47 additions & 2 deletions docs/manual/forms/examples/autofill_address.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,51 @@
Adres automatisch aanvullen
===========================

.. warning::

The former functionality (via textbox) has been deprecated and will be removed
in a future version. Instead, you should use addressNL component which works in
the same way.

Configuratie (new version)
==========================

You first need to configure:

* :ref:`BAG configuratie <configuration_prefill_bag>`: Voor het opzoeken van adressen.


Formulier maken
===============

1. Create a form with the following information:

**Name** : Demo address

2. Click the **Steps and Fields** tab .

3. on the left Click **Add Step** and select **Create New form definition** .

4. Under the (Reusable) step data section , enter the following:

**Name** : Address details

5. Scroll to the **Fields** section .

6. Drag an **addressNL Field** component onto the white area:

**Label** : AddressNL

7. Check the **Derive address** property and then press Save .

8. Click Save at the bottom to save the form completely.

You can now view the form.

This works in the same way **textbox** component was working. By filling the required
data (postcode and house number), the city and the street name will be derived and
automatically filled in the suitable fields if found.

In dit voorbeeld maken we een deel-formulier bestaande uit 1 stap, waarbij de
straatnaam en stad automatisch worden ingevuld zodra de postcode en huisnummer
zijn ingevuld.
Expand All @@ -21,8 +66,8 @@ In dit voorbeeld gaan we er van uit dat u een
Download: :download:`autofill_address_2.zip <_assets/autofill_address_2.zip>`


Configuratie
============
Configuratie (deprecated)
==========================

Voor dit formulier is bepaalde configuratie nodig. Hieronder staan de onderdelen
die geconfigureerd moeten zijn:
Expand Down
4 changes: 4 additions & 0 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8289,6 +8289,10 @@ components:
city:
type: string
description: Found city
secretStreetCity:
type: string
title: city and street name secret
description: Secret for the combination of city and street name
required:
- city
- streetName
Expand Down
3 changes: 3 additions & 0 deletions src/openforms/contrib/brk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ class AddressValue(TypedDict):
house_number: str
house_letter: NotRequired[str]
house_number_addition: NotRequired[str]
city: str
streetName: str
secretStreetCity: str
11 changes: 11 additions & 0 deletions src/openforms/contrib/kadaster/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,23 @@ class GetStreetNameAndCityViewInputSerializer(serializers.Serializer):
label=_("house number"), help_text=_("House number to use in search")
)

secret_street_city = serializers.CharField(
label=_("city and street name secret"),
help_text=_("Secret for the combination of city and street name"),
required=False,
)


class GetStreetNameAndCityViewResultSerializer(serializers.Serializer):
street_name = serializers.CharField(
label=_("street name"), help_text=_("Found street name")
)
city = serializers.CharField(label=_("city"), help_text=_("Found city"))
secret_street_city = serializers.CharField(
label=_("city and street name secret"),
help_text=_("Secret for the combination of city and street name"),
required=False,
)


class LatitudeLongitudeSerializer(serializers.Serializer):
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/contrib/kadaster/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class AddressAutocompleteView(APIView):
"Get the street name and city for a given postal code and house number.\n\n"
"**NOTE** the `/api/v2/location/get-street-name-and-city/` endpoint will "
"be removed in v3. Use `/api/v2/geo/address-autocomplete/` instead."
"Deriving the city and street name from the texboxes is deprecated and will be removed"
"in a future version. Instead, the addressNL component should be used."
), # type: ignore
responses=GetStreetNameAndCityViewResultSerializer,
parameters=[
Expand Down
16 changes: 14 additions & 2 deletions src/openforms/contrib/kadaster/clients/bag.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import logging
from dataclasses import dataclass

from django.utils.crypto import salted_hmac

import elasticapm
import requests

Expand All @@ -13,6 +15,7 @@
class AddressResult:
street_name: str
city: str
secret_street_city: str = ""


class BAGClient(HALClient):
Expand Down Expand Up @@ -53,7 +56,16 @@ def get_address(
return None

first_result = response_data["_embedded"]["adressen"][0]
street_name = first_result.pop("korteNaam")
city = first_result.pop("woonplaatsNaam")

# put an extra layer of protection and make sure that the value is not tampered with
message = (
f"{postcode.upper().replace(' ','')}/{house_number}/{city}/{street_name}"
)

return AddressResult(
street_name=first_result.pop("korteNaam"),
city=first_result.pop("woonplaatsNaam"),
street_name=street_name,
city=city,
secret_street_city=salted_hmac("location_check", value=message).hexdigest(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def setUp(self):
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_getting_street_name_and_city(self, m_lookup_address):
m_lookup_address.return_value = AddressResult(
street_name="Keizersgracht", city="Amsterdam"
street_name="Keizersgracht", city="Amsterdam", secret_street_city=""
)

response = self.client.get(
Expand All @@ -44,9 +44,10 @@ def test_getting_street_name_and_city(self, m_lookup_address):
)

self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
self.assertEqual(len(response.json()), 3)
self.assertEqual(response.json()["streetName"], "Keizersgracht")
self.assertEqual(response.json()["city"], "Amsterdam")
self.assertEqual(response.json()["secretStreetCity"], "")

@patch(
"openforms.api.exception_handling.uuid.uuid4",
Expand Down Expand Up @@ -117,7 +118,7 @@ def test_getting_street_name_and_city_with_extra_query_params_ignores_extra_para
self, m_lookup_address
):
m_lookup_address.return_value = AddressResult(
street_name="Keizersgracht", city="Amsterdam"
street_name="Keizersgracht", city="Amsterdam", secret_street_city=""
)

response = self.client.get(
Expand All @@ -126,9 +127,10 @@ def test_getting_street_name_and_city_with_extra_query_params_ignores_extra_para
)

self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.json()), 2)
self.assertEqual(len(response.json()), 3)
self.assertEqual(response.json()["streetName"], "Keizersgracht")
self.assertEqual(response.json()["city"], "Amsterdam")
self.assertEqual(response.json()["secretStreetCity"], "")

@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_address_not_found_returns_empty_200_response(self, m_lookup_address):
Expand All @@ -146,7 +148,7 @@ def test_address_not_found_returns_empty_200_response(self, m_lookup_address):
@patch("openforms.contrib.kadaster.api.views.lookup_address")
def test_endpoint_uses_caching(self, m_lookup_address):
m_lookup_address.return_value = AddressResult(
street_name="Keizersgracht", city="Amsterdam"
street_name="Keizersgracht", city="Amsterdam", secret_street_city=""
)
endpoint = reverse("api:get-street-name-and-city-list")

Expand Down
43 changes: 43 additions & 0 deletions src/openforms/formio/components/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator
from django.utils import timezone
from django.utils.crypto import salted_hmac
from django.utils.html import format_html
from django.utils.translation import gettext as _

Expand Down Expand Up @@ -390,11 +391,53 @@ class AddressValueSerializer(serializers.Serializer):
required=False,
allow_blank=True,
)
streetName = serializers.CharField(
label=_("street name"),
help_text=_("Found street name"),
required=False,
allow_blank=True,
)
city = serializers.CharField(
label=_("city"),
help_text=_("Found city"),
required=False,
allow_blank=True,
)
secretStreetCity = serializers.CharField(
label=_("city and street name secret"),
help_text=_("Secret for the combination of city and street name"),
required=False,
allow_blank=True,
)

def validate_postcode(self, value: str) -> str:
"""Normalize the postcode so that it matches the regex from the BRK API."""
return value.upper().replace(" ", "")

def validate(self, attrs):
attrs = super().validate(attrs)

city = attrs.get("city", "")
street_name = attrs.get("streetName", "")

if city and street_name:
existing_hmac = attrs.get("secretStreetCity", "")
postcode = attrs.get("postcode", "")
number = attrs.get("houseNumber", "")

computed_message = f"{postcode}/{number}/{city}/{street_name}"
computed_hmac = salted_hmac(
"location_check", value=computed_message
).hexdigest()

if existing_hmac != computed_hmac:
raise serializers.ValidationError(
_("Invalid secret city - street name combination"),
code="invalid",
)

return attrs


@register("addressNL")
class AddressNL(BasePlugin):
Expand Down
74 changes: 74 additions & 0 deletions src/openforms/formio/tests/validation/test_addressnl.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.test import SimpleTestCase
from django.utils.crypto import salted_hmac

from rest_framework import serializers

Expand Down Expand Up @@ -161,3 +162,76 @@ def test_plugin_validator(self):
)

self.assertFalse(is_valid)

def test_addressNL_field_secret_success(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL secret success",
}

message = "1015CJ/117/Amsterdam/Keizersgracht"
secret = salted_hmac("location_check", value=message).hexdigest()
data = {
"addressNl": {
"postcode": "1015CJ",
"houseNumber": "117",
"houseLetter": "",
"houseNumberAddition": "",
"city": "Amsterdam",
"streetName": "Keizersgracht",
"secretStreetCity": secret,
}
}

is_valid, _ = validate_formio_data(component, data)

self.assertTrue(is_valid)

def test_addressNL_field_secret_failure(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL secret failure",
}

data = {
"addressNl": {
"postcode": "1015CJ",
"houseNumber": "117",
"houseLetter": "",
"houseNumberAddition": "",
"city": "Amsterdam",
"streetName": "Keizersgracht",
"secretStreetCity": "invalid secret",
}
}

is_valid, errors = validate_formio_data(component, data)

secret_error = extract_error(errors["addressNl"], "non_field_errors")

self.assertFalse(is_valid)
self.assertEqual(secret_error.code, "invalid")

def test_addressNL_field_missing_city(self):
component: Component = {
"key": "addressNl",
"type": "addressNL",
"label": "AddressNL missing city",
}

data = {
"addressNl": {
"postcode": "1015CJ",
"houseNumber": "117",
"houseLetter": "",
"houseNumberAddition": "",
"city": "",
"streetName": "Keizersgracht",
}
}

is_valid, _ = validate_formio_data(component, data)

self.assertTrue(is_valid)
3 changes: 2 additions & 1 deletion src/openforms/formio/typing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from .base import Component, FormioConfiguration, OptionDict
from .custom import DateComponent
from .custom import AddressNLComponent, DateComponent
from .vanilla import (
Column,
ColumnsComponent,
Expand Down Expand Up @@ -42,5 +42,6 @@
"FieldsetComponent",
# special
"EditGridComponent",
"AddressNLComponent",
# deprecated
]
4 changes: 4 additions & 0 deletions src/openforms/formio/typing/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@
class DateComponent(Component):
datePicker: NotRequired[DatePickerConfig]
customOptions: NotRequired[DatePickerCustomOptions]


class AddressNLComponent(Component):
deriveAddress: bool

0 comments on commit 76b4f6a

Please sign in to comment.