From 3115eb7570a7f14de37e81d788624ba20fb82a00 Mon Sep 17 00:00:00 2001 From: Sonny Bakker Date: Thu, 10 Sep 2026 15:41:59 +0200 Subject: [PATCH] [#6622] apply email verification validation to customerProfile component --- src/openforms/formio/components/custom.py | 4 ++ src/openforms/formio/components/vanilla.py | 25 +--------- .../formio/tests/validation/test_profile.py | 32 ++++++++++++- src/openforms/formio/validators.py | 46 +++++++++++++++++++ .../submissions/models/submission.py | 2 +- src/openforms/submissions/tests/factories.py | 1 - .../submissions/{models => }/typing.py | 3 ++ 7 files changed, 87 insertions(+), 26 deletions(-) rename src/openforms/submissions/{models => }/typing.py (84%) diff --git a/src/openforms/formio/components/custom.py b/src/openforms/formio/components/custom.py index 643e3b82b8..85afb160da 100644 --- a/src/openforms/formio/components/custom.py +++ b/src/openforms/formio/components/custom.py @@ -33,6 +33,7 @@ update_customer_interaction_data, ) from openforms.formio.typing.map import Overlay +from openforms.formio.validators import EmailVerificationValidator from openforms.forms.models import FormVariable from openforms.prefill.contrib.family_members.plugin import ( PLUGIN_IDENTIFIER as FM_PLUGIN_IDENTIFIER, @@ -1304,6 +1305,9 @@ def build_serializer_field( digital_address_types=component["digitalAddressTypes"], required=required, allow_null=not required, + validators=[ + EmailVerificationValidator(component["key"], "customerProfile") + ], ) @staticmethod diff --git a/src/openforms/formio/components/vanilla.py b/src/openforms/formio/components/vanilla.py index f6ac347430..b090d7f0dc 100644 --- a/src/openforms/formio/components/vanilla.py +++ b/src/openforms/formio/components/vanilla.py @@ -30,9 +30,9 @@ from csp_post_processor import post_process_html from openforms.config.constants import UploadFileType from openforms.config.models import GlobalConfiguration +from openforms.formio.validators import EmailVerificationValidator from openforms.submissions.attachments import temporary_upload_from_url from openforms.submissions.form_logic import process_visibility -from openforms.submissions.models import EmailVerification from openforms.typing import JSONObject, JSONValue from openforms.utils.json_schema import to_multiple from openforms.utils.urls import build_absolute_uri @@ -167,27 +167,6 @@ def as_json_schema(component: TextFieldComponent) -> JSONObject: return to_multiple(base) if multiple else base -class EmailVerificationValidator: - message = _("The email address {value} has not been verified yet.") - requires_context = True - - def __init__(self, component_key: str): - self.component_key = component_key - - def __call__(self, value: str, field: serializers.Field) -> None: - submission: Submission = field.context["submission"] - has_verification = EmailVerification.objects.filter( - submission=submission, - component_key=self.component_key, - email=value, - verified_on__isnull=False, - ).exists() - if not has_verification: - raise serializers.ValidationError( - self.message.format(value=value), code="unverified" - ) - - @register("email") class Email(BasePlugin): formatter = EmailFormatter @@ -213,7 +192,7 @@ def build_serializer_field( validators.append(PluginValidator(plugin_ids)) if verification_required: - validators.append(EmailVerificationValidator(component["key"])) + validators.append(EmailVerificationValidator(component["key"], "email")) if validators: extra["validators"] = validators diff --git a/src/openforms/formio/tests/validation/test_profile.py b/src/openforms/formio/tests/validation/test_profile.py index 625f160604..2731f3709a 100644 --- a/src/openforms/formio/tests/validation/test_profile.py +++ b/src/openforms/formio/tests/validation/test_profile.py @@ -1,7 +1,10 @@ from django.test import TestCase, tag from django.utils.translation import gettext_lazy as _ -from openforms.submissions.tests.factories import SubmissionFactory +from openforms.submissions.tests.factories import ( + EmailVerificationFactory, + SubmissionFactory, +) from ...typing.custom import CustomerProfileComponent from .helpers import extract_error, validate_formio_data @@ -25,6 +28,12 @@ def test_validate_profile_not_empty_valid(self): form__generate_minimal_setup=True, form__formstep__form_definition__configuration={"components": [component]}, ) + EmailVerificationFactory.create( + submission=submission, + component_key="profile", + email="john@smith.org", + verified=True, + ) valid_values = { "profile": [ { @@ -55,6 +64,9 @@ def test_validate_profile_empty_valid(self): form__generate_minimal_setup=True, form__formstep__form_definition__configuration={"components": [component]}, ) + EmailVerificationFactory.create( + submission=submission, component_key="profile", email="", verified=True + ) valid_values = { "profile": [ { @@ -141,6 +153,12 @@ def test_validate_profile_wrong_address_format_email(self): ] }, ) + EmailVerificationFactory.create( + submission=submission, + component_key="profile", + email="some-incorrect-email", + verified=True, + ) values = { "profile": [ { @@ -204,6 +222,18 @@ def test_validate_profile_duplicated_types(self): ] }, ) + EmailVerificationFactory.create( + submission=submission, + component_key="profile", + email="john@smith.org", + verified=True, + ) + EmailVerificationFactory.create( + submission=submission, + component_key="profile", + email="another@email.org", + verified=True, + ) values = { "profile": [ { diff --git a/src/openforms/formio/validators.py b/src/openforms/formio/validators.py index a97db9aee5..dcfbf022d0 100644 --- a/src/openforms/formio/validators.py +++ b/src/openforms/formio/validators.py @@ -3,6 +3,11 @@ from django.utils.regex_helper import _lazy_re_compile from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from openforms.formio.typing.custom import DigitalAddress +from openforms.submissions.typing import EmailVerificationComponentType + # Regex and message adapted from # https://github.com/formio/formio.js/blob/4.13.x/src/components/_classes/component/editForm/Component.edit.api.js#L10 variable_key_validator = RegexValidator( @@ -41,3 +46,44 @@ def validate_formio_js_schema(value: dict): _("The 'components' value must be a list of components."), code="invalid", ) + + +class EmailVerificationValidator: + message = _("The email address {value} has not been verified yet.") + requires_context = True + + component_key: str + component_type: EmailVerificationComponentType + + def __init__( + self, component_key: str, component_type: EmailVerificationComponentType + ) -> None: + self.component_key = component_key + self.component_type = component_type + + def __call__(self, value: str | DigitalAddress, field: serializers.Field) -> None: + from openforms.submissions.models import EmailVerification, Submission + + address: str + error_format: str | dict[str, list[str]] + if self.component_type == "customerProfile": + assert isinstance(value, dict) + if value.get("type", "") == "phoneNumber": + return + address = value.get("address") or "" + error_format = {"address": [self.message.format(value=address)]} + else: + assert isinstance(value, str) + address = value + error_format = self.message.format(value=address) + + submission: Submission = field.context["submission"] + has_verification = EmailVerification.objects.filter( + submission=submission, + component_key=self.component_key, + email=address, + verified_on__isnull=False, + ).exists() + + if not has_verification: + raise serializers.ValidationError(error_format, code="unverified") diff --git a/src/openforms/submissions/models/submission.py b/src/openforms/submissions/models/submission.py index ef0e578fa3..d489626fcb 100644 --- a/src/openforms/submissions/models/submission.py +++ b/src/openforms/submissions/models/submission.py @@ -40,8 +40,8 @@ from ..pricing import get_submission_price from ..query import SubmissionQuerySet, SubmissionsManagerType from ..serializers import CoSignDataSerializer +from ..typing import SubmissionCosignData from .submission_step import SubmissionStep -from .typing import SubmissionCosignData if TYPE_CHECKING: from openforms.authentication.models import AuthInfo, RegistratorInfo diff --git a/src/openforms/submissions/tests/factories.py b/src/openforms/submissions/tests/factories.py index cf62417ce4..258ebea0d1 100644 --- a/src/openforms/submissions/tests/factories.py +++ b/src/openforms/submissions/tests/factories.py @@ -464,7 +464,6 @@ class Meta: class EmailVerificationFactory(factory.django.DjangoModelFactory): submission = factory.SubFactory(SubmissionFactory) - component_key = "email" email = factory.Faker("email") class Meta: diff --git a/src/openforms/submissions/models/typing.py b/src/openforms/submissions/typing.py similarity index 84% rename from src/openforms/submissions/models/typing.py rename to src/openforms/submissions/typing.py index cda77d3da7..4c8ca99125 100644 --- a/src/openforms/submissions/models/typing.py +++ b/src/openforms/submissions/typing.py @@ -14,3 +14,6 @@ class SubmissionCosignData(TypedDict): attribute: AuthAttribute value: str cosign_date: datetime + + +type EmailVerificationComponentType = Literal["email", "customerProfile"]