Skip to content

Commit

Permalink
Merge pull request #4029 from open-formulieren/task/72-backend-valida…
Browse files Browse the repository at this point in the history
…tion-of-form-rules

[#72] Add backend validation of form rules
  • Loading branch information
sergei-maertens authored Apr 2, 2024
2 parents bdf53af + 2db8fd0 commit 31380fa
Show file tree
Hide file tree
Showing 16 changed files with 1,346 additions and 48 deletions.
60 changes: 48 additions & 12 deletions src/openforms/formio/components/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import date
from typing import Protocol

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

Expand All @@ -14,6 +14,7 @@
from openforms.submissions.models import Submission
from openforms.typing import DataMapping
from openforms.utils.date import format_date_value
from openforms.utils.validators import BSNValidator
from openforms.validations.service import PluginValidator

from ..dynamic_config.date import mutate as mutate_min_max_validation
Expand All @@ -31,6 +32,7 @@
from .np_family_members.haal_centraal import get_np_family_members_haal_centraal
from .np_family_members.models import FamilyMembersTypeConfig
from .np_family_members.stuf_bg import get_np_family_members_stuf_bg
from .utils import _normalize_pattern

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -74,7 +76,7 @@ def build_serializer_field(
Additional validation is taken from the datePicker configuration, which is also
set dynamically through our own backend (see :meth:`mutate_config_dynamically`).
"""
# relevant validators: required, datePicker.minDate and datePicker.maxDdate
# relevant validators: required, datePicker.minDate and datePicker.maxDate
multiple = component.get("multiple", False)
validate = component.get("validate", {})
required = validate.get("required", False)
Expand Down Expand Up @@ -109,7 +111,7 @@ def mutate_config_dynamically(


@register("map")
class Map(BasePlugin):
class Map(BasePlugin[Component]):
formatter = MapFormatter

@staticmethod
Expand All @@ -122,9 +124,18 @@ def rewrite_for_request(component, request: Request):
component["initialCenter"]["lat"] = config.form_map_default_latitude
component["initialCenter"]["lng"] = config.form_map_default_longitude

def build_serializer_field(self, component: Component) -> serializers.ListField:
validate = component.get("validate", {})
required = validate.get("required", False)
base = serializers.FloatField(
required=required,
allow_null=not required,
)
return serializers.ListField(child=base, min_length=2, max_length=2)


@register("postcode")
class Postcode(BasePlugin):
class Postcode(BasePlugin[Component]):
formatter = TextFieldFormatter

@staticmethod
Expand All @@ -144,6 +155,36 @@ def normalizer(component: Component, value: str) -> str:
)
return value

def build_serializer_field(
self, component: Component
) -> serializers.CharField | serializers.ListField:
multiple = component.get("multiple", False)
validate = component.get("validate", {})
required = validate.get("required", False)
# dynamically add in more kwargs based on the component configuration
extra = {}
validators = []
# adding in the validator is more explicit than changing to serialiers.RegexField,
# which essentially does the same.
if pattern := validate.get("pattern"):
validators.append(
RegexValidator(
_normalize_pattern(pattern),
message=_("This value does not match the required pattern."),
)
)

if plugin_ids := validate.get("plugins", []):
validators += [PluginValidator(plugin) for plugin in plugin_ids]

if validators:
extra["validators"] = validators

base = serializers.CharField(
required=required, allow_blank=not required, **extra
)
return serializers.ListField(child=base) if multiple else base


class FamilyMembersHandler(Protocol):
def __call__(
Expand Down Expand Up @@ -234,7 +275,7 @@ def mutate_config_dynamically(


@register("bsn")
class BSN(BasePlugin):
class BSN(BasePlugin[Component]):
formatter = TextFieldFormatter

def build_serializer_field(
Expand All @@ -246,17 +287,12 @@ def build_serializer_field(

# dynamically add in more kwargs based on the component configuration
extra = {}
# maxLength because of the usage in appointments, even though our form builder
# does not expose it. See `openforms.appointments.contrib.qmatic.constants`.
if (max_length := validate.get("maxLength")) is not None:
extra["max_length"] = max_length

validators = []
validators = [BSNValidator()]
if plugin_ids := validate.get("plugins", []):
validators += [PluginValidator(plugin) for plugin in plugin_ids]

if validators:
extra["validators"] = validators
extra["validators"] = validators

base = serializers.CharField(
required=required,
Expand Down
9 changes: 9 additions & 0 deletions src/openforms/formio/components/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
def _normalize_pattern(pattern: str) -> str:
"""
Normalize a regex pattern so that it matches from beginning to the end of the value.
"""
if not pattern.startswith("^"):
pattern = f"^{pattern}"
if not pattern.endswith("$"):
pattern = f"{pattern}$"
return pattern
Loading

0 comments on commit 31380fa

Please sign in to comment.