Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/openforms/formio/components/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
25 changes: 2 additions & 23 deletions src/openforms/formio/components/vanilla.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 31 additions & 1 deletion src/openforms/formio/tests/validation/test_profile.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down
46 changes: 46 additions & 0 deletions src/openforms/formio/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
2 changes: 1 addition & 1 deletion src/openforms/submissions/models/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/openforms/submissions/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,6 @@ class Meta:

class EmailVerificationFactory(factory.django.DjangoModelFactory):
submission = factory.SubFactory(SubmissionFactory)
component_key = "email"
email = factory.Faker("email")

class Meta:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ class SubmissionCosignData(TypedDict):
attribute: AuthAttribute
value: str
cosign_date: datetime


type EmailVerificationComponentType = Literal["email", "customerProfile"]
Loading