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

Refactor validation plugins #3741

Merged
merged 1 commit into from
Jan 12, 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
6 changes: 1 addition & 5 deletions src/openforms/contrib/brk/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ class BRKValidatorIntegrationTestCase(
def setUp(self) -> None:
super().setUp()
register = Registry()
register(
"brk-zakelijk-gerechtigd",
verbose_name="dummy",
for_components=("addressNL",),
)(BRKZakelijkGerechtigdeValidator)
register("brk-zakelijk-gerechtigd")(BRKZakelijkGerechtigdeValidator)

patcher = patch("openforms.validations.api.views.register", new=register)
patcher.start()
Expand Down
12 changes: 6 additions & 6 deletions src/openforms/contrib/brk/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class BRKValidatorTestCase(OFVCRMixin, BRKTestMixin, TestCase):
VCR_TEST_FILES = TEST_FILES

def test_brk_validator_no_auth(self):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_no_auth = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand All @@ -35,7 +35,7 @@ def test_brk_validator_no_auth(self):
)

def test_brk_validator_no_bsn(self):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_no_bsn = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand All @@ -52,7 +52,7 @@ def test_brk_validator_no_bsn(self):
)

def test_brk_validator_wrong_bsn(self):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_wrong_bsn = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand All @@ -73,7 +73,7 @@ def test_brk_validator_wrong_bsn(self):
)

def test_brk_validator_bsn(self):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_bsn = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand Down Expand Up @@ -112,7 +112,7 @@ def test_brk_validator_bsn(self):

@requests_mock.Mocker()
def test_brk_validator_requests_error(self, m: requests_mock.Mocker):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_bsn = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand Down Expand Up @@ -150,7 +150,7 @@ def setUp(self):
self.addCleanup(patcher.stop)

def test_brk_validator_not_configured(self):
validator = BRKZakelijkGerechtigdeValidator()
validator = BRKZakelijkGerechtigdeValidator("brk_validator")

submission_bsn = SubmissionFactory.create(
form__generate_minimal_setup=True,
Expand Down
11 changes: 5 additions & 6 deletions src/openforms/contrib/brk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from openforms.authentication.constants import AuthAttribute
from openforms.submissions.models import Submission
from openforms.validations.base import BasePlugin
from openforms.validations.registry import register

from .client import NoServiceConfigured, SearchParams, get_client
Expand Down Expand Up @@ -56,15 +57,13 @@ class ValueSerializer(serializers.Serializer):
value = AddressValueSerializer()


@register(
"brk-zakelijk-gerechtigd",
verbose_name=_("BRK - Zakelijk gerechtigd"),
for_components=("addressNL",),
)
@register("brk-zakelijk-gerechtigd")
@deconstructible
class BRKZakelijkGerechtigdeValidator:
class BRKZakelijkGerechtigdeValidator(BasePlugin[AddressValue]):

value_serializer = ValueSerializer
verbose_name = _("BRK - Zakelijk gerechtigd")
for_components = ("addressNL",)

error_messages = {
"no_bsn": _("No BSN is available to validate your address."),
Expand Down
8 changes: 4 additions & 4 deletions src/openforms/contrib/kvk/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_kvkNumber_validator(self, m):
status_code=500,
)

validator = partial(KVKNumberRemoteValidator(), submission=Submission())
validator = partial(KVKNumberRemoteValidator("id"), submission=Submission())
validator("69599084")

with self.assertRaisesMessage(
Expand Down Expand Up @@ -96,7 +96,7 @@ def test_kvkNumber_validator_emptyish_results(self, m):
{"resultaten": []},
{},
)
validate = partial(KVKNumberRemoteValidator(), submission=Submission())
validate = partial(KVKNumberRemoteValidator("id"), submission=Submission())

for response_json in bad_responses:
with self.subTest(response_json=response_json):
Expand All @@ -120,7 +120,7 @@ def test_rsin_validator(self, m):
status_code=404,
)

validator = partial(KVKRSINRemoteValidator(), submission=Submission())
validator = partial(KVKRSINRemoteValidator("id"), submission=Submission())
validator("111222333")

with self.assertRaisesMessage(
Expand Down Expand Up @@ -151,7 +151,7 @@ def test_branchNumber_validator(self, m):
status_code=404,
)

validator = partial(KVKBranchNumberRemoteValidator(), submission=Submission())
validator = partial(KVKBranchNumberRemoteValidator(""), submission=Submission())
validator("112233445566")

with self.assertRaisesMessage(
Expand Down
48 changes: 26 additions & 22 deletions src/openforms/contrib/kvk/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from requests import RequestException

from openforms.utils.validators import validate_digits, validate_rsin
from openforms.validations.registry import StringValueSerializer, register
from openforms.validations.base import BasePlugin
from openforms.validations.registry import register

from .client import NoServiceConfigured, SearchParams, get_client

Expand All @@ -20,7 +21,7 @@ class NumericBaseValidator:
"too_short": _("%(type)s should have %(size)i characters."),
}

def __call__(self, value):
def __call__(self, value: str):
validate_digits(value)

if len(value) != self.value_size:
Expand All @@ -47,18 +48,16 @@ class KVKBranchNumberValidator(NumericBaseValidator):
validate_branchNumber = KVKBranchNumberValidator()


class KVKRemoteBaseValidator:
class KVKRemoteValidatorMixin:
query_param: Literal["kvkNummer", "rsin", "vestigingsnummer"]
value_label: str

value_serializer = StringValueSerializer

error_messages = {
"not_found": _("%(type)s does not exist."),
"too_short": _("%(type)s should have %(size)i characters."),
}

def __call__(self, value: str) -> bool:
def validate(self, value: str) -> bool:
assert self.query_param
query = cast(
SearchParams, {self.query_param: value}
Expand All @@ -84,38 +83,43 @@ def __call__(self, value: str) -> bool:
return True


@register("kvk-kvkNumber", verbose_name=_("KvK number"), for_components=("textfield",))
@register("kvk-kvkNumber")
@deconstructible
class KVKNumberRemoteValidator(KVKRemoteBaseValidator):
class KVKNumberRemoteValidator(KVKRemoteValidatorMixin, BasePlugin[str]):
query_param = "kvkNummer"
value_label = _("KvK number")

def __call__(self, value, submission):
verbose_name = _("KvK number")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_kvk(value)
super().__call__(value)
self.validate(value)


@register("kvk-rsin", verbose_name=_("KvK RSIN"), for_components=("textfield",))
@register("kvk-rsin")
@deconstructible
class KVKRSINRemoteValidator(KVKRemoteBaseValidator):
class KVKRSINRemoteValidator(KVKRemoteValidatorMixin, BasePlugin[str]):
query_param = "rsin"
value_label = _("RSIN")

def __call__(self, value, submission):
verbose_name = _("KvK RSIN")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_rsin(value)
super().__call__(value)
self.validate(value)


@register(
"kvk-branchNumber",
verbose_name=_("KvK branch number"),
for_components=("textfield",),
)
@register("kvk-branchNumber")
@deconstructible
class KVKBranchNumberRemoteValidator(KVKRemoteBaseValidator):
class KVKBranchNumberRemoteValidator(KVKRemoteValidatorMixin, BasePlugin[str]):
query_param = "vestigingsnummer"
value_label = _("Branch number")

def __call__(self, value, submission):
verbose_name = _("KvK branch number")
for_components = ("textfield",)

def __call__(self, value: str, submission):
validate_branchNumber(value)
super().__call__(value)
self.validate(value)
2 changes: 1 addition & 1 deletion src/openforms/payments/tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_long_identifier(self):

with self.assertRaisesMessage(
ValueError,
f"The unique identifier '{long_identifier}' is longer then {UNIQUE_ID_MAX_LENGTH} characters.",
f"The unique identifier '{long_identifier}' is longer than {UNIQUE_ID_MAX_LENGTH} characters.",
):
register(long_identifier)(Plugin)

Expand Down
8 changes: 4 additions & 4 deletions src/openforms/plugins/registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
from typing import TYPE_CHECKING, Callable, Generic, Iterator, TypeVar

from django.db import OperationalError

Expand Down Expand Up @@ -35,7 +35,7 @@ def __call__(
def decorator(plugin_cls: type[PluginType_co]) -> type[PluginType_co]:
if len(unique_identifier) > UNIQUE_ID_MAX_LENGTH:
raise ValueError(
f"The unique identifier '{unique_identifier}' is longer then {UNIQUE_ID_MAX_LENGTH} characters."
f"The unique identifier '{unique_identifier}' is longer than {UNIQUE_ID_MAX_LENGTH} characters."
)
if unique_identifier in self._registry:
raise ValueError(
Expand All @@ -50,7 +50,7 @@ def decorator(plugin_cls: type[PluginType_co]) -> type[PluginType_co]:

return decorator

def check_plugin(self, plugin):
def check_plugin(self, plugin: PluginType_co):
# validation hook
pass

Expand All @@ -63,7 +63,7 @@ def __getitem__(self, key: str):
def __contains__(self, key: str):
return key in self._registry

def iter_enabled_plugins(self):
def iter_enabled_plugins(self) -> Iterator[PluginType_co]:
try:
config = GlobalConfiguration.get_solo()
assert isinstance(config, GlobalConfiguration)
Expand Down
2 changes: 1 addition & 1 deletion src/openforms/registrations/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Registry(BaseRegistry[BasePlugin]):

module = "registrations"

def check_plugin(self, plugin):
def check_plugin(self, plugin: BasePlugin):
if not plugin.configuration_options:
raise ValueError(
"Please specify 'configuration_options' attribute for plugin class."
Expand Down
15 changes: 8 additions & 7 deletions src/openforms/validations/api/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Iterable

from django.utils.translation import gettext_lazy as _

from drf_spectacular.types import OpenApiTypes
Expand All @@ -21,7 +19,8 @@
ValidatorsFilterSerializer,
)

from ..registry import RegisteredValidator, register
from ..base import BasePlugin
from ..registry import register


@extend_schema_view(
Expand All @@ -39,16 +38,18 @@ class ValidatorsListView(ListMixin, APIView):
permission_classes = (permissions.IsAdminUser,)
serializer_class = ValidationPluginSerializer

def get_objects(self) -> list[RegisteredValidator]:
def get_objects(self) -> list[BasePlugin]:
filter_serializer = ValidatorsFilterSerializer(data=self.request.query_params)
if not filter_serializer.is_valid(raise_exception=False):
return []

plugins: Iterable[RegisteredValidator] = register.iter_enabled_plugins()
for_component = filter_serializer.validated_data.get("component_type") or ""
plugins = register.iter_enabled_plugins()
for_component: str = (
filter_serializer.validated_data.get("component_type") or ""
)

if not for_component:
return plugins
return list(plugins)
return [plugin for plugin in plugins if for_component in plugin.for_components]


Expand Down
39 changes: 39 additions & 0 deletions src/openforms/validations/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from abc import ABC, abstractmethod
from typing import ClassVar, Generic, TypeVar

from rest_framework import serializers

from openforms.plugins.plugin import AbstractBasePlugin
from openforms.submissions.models import Submission
from openforms.typing import JSONValue

T = TypeVar("T", bound=JSONValue)
"""A type variable representing the type of the value being validated by the plugin."""


class StringValueSerializer(serializers.Serializer):
"""A default serializer that accepts ``value`` as a string."""

value = serializers.CharField()


class BasePlugin(ABC, AbstractBasePlugin, Generic[T]):
"""The base class for validation plugins.

This class is generic over the type of the validated value, defaulting to ``str``.
"""

value_serializer: ClassVar[type[serializers.BaseSerializer]] = StringValueSerializer
"""The serializer to be used to validate the value."""

for_components: ClassVar[tuple[str, ...]] = tuple()
"""The components that can make use of this validator."""

@property
def is_enabled(self) -> bool:
# TODO always enabled for now, see: https://github.com/open-formulieren/open-forms/issues/1149
return True

@abstractmethod
def __call__(self, value: T, submission: Submission) -> bool:
pass

Check warning on line 39 in src/openforms/validations/base.py

View check run for this annotation

Codecov / codecov/patch

src/openforms/validations/base.py#L39

Added line #L39 was not covered by tests
Loading
Loading