Skip to content

Commit

Permalink
Merge pull request #3646 from open-formulieren/feature/3607-brk-valid…
Browse files Browse the repository at this point in the history
…ator

[#3607] Refactor validation plugin, add BRK validator
  • Loading branch information
sergei-maertens authored Jan 5, 2024
2 parents 570df04 + e11ab50 commit 3b16fff
Show file tree
Hide file tree
Showing 54 changed files with 1,635 additions and 59 deletions.
1 change: 1 addition & 0 deletions requirements/base.in
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ xmltodict
self-certifi
semantic-version
tabulate
typing-extensions
weasyprint
zeep
# Pinned setuptools to a version lower than 58 to allow pyzmail36 to be
Expand Down
2 changes: 2 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,8 @@ tinycss2==1.1.0
# weasyprint
tornado==6.3.3
# via flower
typing-extensions==4.9.0
# via -r requirements/base.in
tzdata==2023.3
# via pytz-deprecation-shim
tzlocal==4.3.1
Expand Down
7 changes: 5 additions & 2 deletions requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -958,8 +958,11 @@ tornado==6.3.3
# -c requirements/base.txt
# -r requirements/base.txt
# flower
typing-extensions==4.7.1
# via pyee
typing-extensions==4.9.0
# via
# -c requirements/base.txt
# -r requirements/base.txt
# pyee
tzdata==2023.3
# via
# -c requirements/base.txt
Expand Down
2 changes: 1 addition & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1133,7 +1133,7 @@ tornado==6.3.3
# -c requirements/ci.txt
# -r requirements/ci.txt
# flower
typing-extensions==4.7.1
typing-extensions==4.9.0
# via
# -c requirements/ci.txt
# -r requirements/ci.txt
Expand Down
4 changes: 4 additions & 0 deletions requirements/extensions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,10 @@ tornado==6.3.3
# via
# -r requirements/base.txt
# flower
typing-extensions==4.9.0
# via
# -c requirements/base.in
# -r requirements/base.txt
tzdata==2023.3
# via
# -r requirements/base.txt
Expand Down
7 changes: 6 additions & 1 deletion src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5276,6 +5276,7 @@ paths:
schema:
type: string
enum:
- brk-zakelijk-gerechtigd
- kvk-branchNumber
- kvk-kvkNumber
- kvk-rsin
Expand Down Expand Up @@ -9440,9 +9441,13 @@ components:
type: object
properties:
value:
description: Value to be validated.
submissionUuid:
type: string
description: Value to be validated
format: uuid
description: UUID of the submission.
required:
- submissionUuid
- value
ValidationPlugin:
type: object
Expand Down
21 changes: 20 additions & 1 deletion src/openforms/api/drf_spectacular/plumbing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Any

from drf_spectacular.plumbing import (
ResolvedComponent,
build_basic_type,
build_parameter_type,
)
from drf_spectacular.utils import OpenApiParameter
from drf_spectacular.utils import OpenApiParameter, inline_serializer
from rest_framework.fields import Field
from rest_framework.serializers import Serializer


def build_response_header_parameter(
Expand Down Expand Up @@ -53,3 +57,18 @@ def build_response_header_component(
object=name,
)
return component


def extend_inline_serializer(
serializer: type[Serializer],
fields: dict[str, Field],
name: str = "",
**kwargs: Any,
) -> Serializer:
"""Return an inline serializer, with fields extended from the provided serializer.
If one of the provided extra fields already exists on the base serializer, it will be overridden.
"""
return inline_serializer(
name or serializer.__name__, serializer().get_fields() | fields, **kwargs
)
1 change: 1 addition & 0 deletions src/openforms/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@
"openforms.ui",
"openforms.submissions",
"openforms.logging.apps.LoggingAppConfig",
"openforms.contrib.brk",
"openforms.contrib.brp",
"openforms.contrib.digid_eherkenning",
"openforms.contrib.haal_centraal",
Expand Down
2 changes: 2 additions & 0 deletions src/openforms/config/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.views.generic import TemplateView

from openforms.appointments.registry import register as appointments_register
from openforms.contrib.brk.checks import BRKValidatorCheck
from openforms.contrib.kadaster.config_check import BAGCheck, LocatieServerCheck
from openforms.contrib.kvk.checks import KVKRemoteValidatorCheck
from openforms.dmn.registry import register as dmn_register
Expand Down Expand Up @@ -75,6 +76,7 @@ def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
"entries": [
# uses KVK 'zoeken' client
self.get_plugin_entry(KVKRemoteValidatorCheck),
self.get_plugin_entry(BRKValidatorCheck),
],
},
]
Expand Down
Empty file.
10 changes: 10 additions & 0 deletions src/openforms/contrib/brk/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.contrib import admin

from solo.admin import SingletonModelAdmin

from .models import BRKConfig


@admin.register(BRKConfig)
class BRKConfigAdmin(SingletonModelAdmin):
pass
12 changes: 12 additions & 0 deletions src/openforms/contrib/brk/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _


class BRKApp(AppConfig):
name = "openforms.contrib.brk"
label = "brk"
verbose_name = _("BRK configuration")

def ready(self):
# register the plugin
from . import validators # noqa
48 changes: 48 additions & 0 deletions src/openforms/contrib/brk/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _

import requests

from openforms.config.data import Action
from openforms.plugins.exceptions import InvalidPluginConfiguration

from .client import NoServiceConfigured, get_client
from .models import BRKConfig


class BRKValidatorCheck:
verbose_name: str = _("Validation plugin config: BRK - Zakelijk gerechtigd") # type: ignore

@staticmethod
def check_config():
try:
with get_client() as client:
results = client.get_real_estate_by_address(
{"postcode": "1234AB", "huisnummer": "1"}
)
except NoServiceConfigured as exc:
msg = _("{api_name} endpoint is not configured.").format(api_name="KVK")
raise InvalidPluginConfiguration(msg) from exc
except requests.RequestException as exc:
raise InvalidPluginConfiguration(
_("Invalid response: {exception}").format(exception=exc)
) from exc

if not isinstance(results, dict):
raise InvalidPluginConfiguration(_("Response data is not a dictionary"))

items = results.get("_embedded")
if items is None or not isinstance(items, dict):
raise InvalidPluginConfiguration(_("Response does not contain results"))

@staticmethod
def get_config_actions() -> list[Action]:
return [
(
gettext("Configuration"),
reverse(
"admin:brk_brkconfig_change",
args=(BRKConfig.singleton_instance_id,),
),
),
]
71 changes: 71 additions & 0 deletions src/openforms/contrib/brk/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import logging
from typing import TypedDict

import requests
from ape_pie.client import APIClient
from typing_extensions import NotRequired

from zgw_consumers_ext.api_client import ServiceClientFactory

from .models import BRKConfig

logger = logging.getLogger(__name__)


class NoServiceConfigured(RuntimeError):
pass


def get_client() -> "BRKClient":
config = BRKConfig.get_solo()
assert isinstance(config, BRKConfig)
if not (service := config.service):
raise NoServiceConfigured("No BRK service configured!")
service_client_factory = ServiceClientFactory(service)
return BRKClient.configure_from(service_client_factory)


class SearchParams(TypedDict):
postcode: str
huisnummer: str
huisletter: NotRequired[str]
huisnummertoevoeging: NotRequired[str]


class BRKClient(APIClient):
def get_real_estate_by_address(self, query_params: SearchParams):
"""
Search for real estate by querying for a specific address.
API docs: https://vng-realisatie.github.io/Haal-Centraal-BRK-bevragen/swagger-ui-2.0#/Kadastraal%20Onroerende%20Zaken/GetKadastraalOnroerendeZaken
"""
assert query_params, "You must provide at least one query parameter"

try:
response = self.get(
"kadastraalonroerendezaken",
params=query_params,
)
response.raise_for_status()
except requests.RequestException as exc:
logger.exception("exception while making BRK request", exc_info=exc)
raise exc

return response.json()

def get_cadastral_titleholders_by_cadastral_id(self, cadastral_id: str):
"""
Look up the rightholders of a property (e.g. a house) in the Dutch cadastre.
API docs: https://vng-realisatie.github.io/Haal-Centraal-BRK-bevragen/swagger-ui-2.0#/Zakelijke%20Gerechtigden/GetZakelijkGerechtigden
"""
try:
response = self.get(
f"kadastraalonroerendezaken/{cadastral_id}/zakelijkgerechtigden",
)
response.raise_for_status()
except requests.RequestException as exc:
logger.exception("exception while making BRK request", exc_info=exc)
raise exc

return response.json()
10 changes: 10 additions & 0 deletions src/openforms/contrib/brk/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from typing import TypedDict

from typing_extensions import NotRequired


class AddressValue(TypedDict):
postcode: str
house_number: str
house_letter: NotRequired[str]
house_number_addition: NotRequired[str]
45 changes: 45 additions & 0 deletions src/openforms/contrib/brk/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 3.2.23 on 2023-11-29 16:05

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

initial = True

dependencies = [
("zgw_consumers", "0019_alter_service_uuid"),
]

operations = [
migrations.CreateModel(
name="BRKConfig",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"service",
models.OneToOneField(
help_text="Service for API interaction with the BRK.",
limit_choices_to={"api_type": "orc"},
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="zgw_consumers.service",
verbose_name="BRK API",
),
),
],
options={
"verbose_name": "BRK configuration",
},
),
]
Empty file.
36 changes: 36 additions & 0 deletions src/openforms/contrib/brk/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from django.db import models
from django.utils.translation import gettext_lazy as _

from solo.models import SingletonModel
from zgw_consumers.constants import APITypes


class BRKConfigManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
return qs.select_related(
"service",
"service__client_certificate",
"service__server_certificate",
)


class BRKConfig(SingletonModel):
"""
Global configuration and defaults.
"""

service = models.OneToOneField(
"zgw_consumers.Service",
verbose_name=_("BRK API"),
help_text=_("Service for API interaction with the BRK."),
on_delete=models.PROTECT,
limit_choices_to={"api_type": APITypes.orc},
related_name="+",
null=True,
)

objects = BRKConfigManager()

class Meta:
verbose_name = _("BRK configuration")
Empty file.
Loading

0 comments on commit 3b16fff

Please sign in to comment.