Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#3607] Refactor validation plugin, add BRK validator #3646

Merged
merged 14 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
Viicos marked this conversation as resolved.
Show resolved Hide resolved
"""
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):
Viicos marked this conversation as resolved.
Show resolved Hide resolved
"""
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
Loading