Skip to content

Commit

Permalink
✨ [#4650] Add variable select input to email registration
Browse files Browse the repository at this point in the history
Either the `to_emails` or the `to_email_from_variable` fields should used when registering the email backend plugin.

When a variable is selected, the resulting email address will be used for mailings. When the variable doesn't contain an email address, the `to_emails` will be used as fallback.
  • Loading branch information
robinmolen committed Dec 24, 2024
1 parent b0b5055 commit 473b0bb
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ EmailOptionsForm.propTypes = {
emailSubject: PropTypes.string,
paymentEmails: PropTypes.arrayOf(PropTypes.string),
toEmails: PropTypes.arrayOf(PropTypes.string),
toEmailsFromVariable: PropTypes.string,
}),
onChange: PropTypes.func.isRequired,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import EmailHasAttachmentSelect from './fields/EmailHasAttachmentSelect';
import EmailPaymentSubject from './fields/EmailPaymentSubject';
import EmailPaymentUpdateRecipients from './fields/EmailPaymentUpdateRecipients';
import EmailRecipients from './fields/EmailRecipients';
import EmailRecipientsFromVariable from './fields/EmailRecipientsFromVariable';
import EmailSubject from './fields/EmailSubject';

const EmailOptionsFormFields = ({name, schema}) => {
Expand All @@ -37,6 +38,7 @@ const EmailOptionsFormFields = ({name, schema}) => {
<ValidationErrorsProvider errors={relevantErrors}>
<Fieldset>
<EmailRecipients />
<EmailRecipientsFromVariable />
<EmailSubject />
<EmailContentTemplateHTML />
<EmailContentTemplateText />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {useField} from 'formik';
import React from 'react';
import {FormattedMessage} from 'react-intl';

import Field from 'components/admin/forms/Field';
import FormRow from 'components/admin/forms/FormRow';
import VariableSelection from 'components/admin/forms/VariableSelection';

const EmailRecipientsFromVariable = () => {
const [fieldProps, , fieldHelpers] = useField('toEmailsFromVariable');
const {setValue} = fieldHelpers;
return (
<FormRow>
<Field
name="toEmailsFromVariable"
label={
<FormattedMessage
description="Email registration options 'toEmailsFromVariable' label"
defaultMessage="Using a variable to decide to which email address the
submission details will be sent"
/>
}
helpText={
<FormattedMessage
description="Email registration options 'toEmailsFromVariable' helpText"
defaultMessage="The email address described in this variable will be used
for the mailing. If a variable is selected, the general registration
addresses will be used as fallback option. "
/>
}
>
<VariableSelection
name="toEmailsFromVariable"
value={fieldProps.value}
onChange={event => {
setValue(event.target.value);
}}
/>
</Field>
</FormRow>
);
};

EmailRecipientsFromVariable.propTypes = {};

export default EmailRecipientsFromVariable;
59 changes: 42 additions & 17 deletions src/openforms/registrations/contrib/email/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,40 @@
from .constants import AttachmentFormat


class Options(TypedDict):
"""
Shape of the email registration plugin options.
This describes the shape of :attr:`EmailOptionsSerializer.validated_data`, after
the input data has been cleaned/validated.
"""

to_emails: NotRequired[list[str]]
to_emails_from_variable: NotRequired[str]
attachment_formats: NotRequired[list[AttachmentFormat | str]]
payment_emails: NotRequired[list[str]]
attach_files_to_email: bool | None
email_subject: NotRequired[str]
email_payment_subject: NotRequired[str]
email_content_template_html: NotRequired[str]
email_content_template_text: NotRequired[str]


class EmailOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer):
to_emails = serializers.ListField(
child=serializers.EmailField(),
label=_("The email addresses to which the submission details will be sent"),
required=True,
required=False, # Either to_emails or to_emails_from_variable should be required
)
to_emails_from_variable = serializers.CharField(
label=_("Key of the target variable containing the email address"),
required=False,
allow_blank=True,
help_text=_(
"Key of the target variable whose value will be used for the mailing. "
"When using this field, the mailing will only be send to this email address. "
"The email addresses field would then be ignored. "
),
)
attachment_formats = serializers.ListField(
child=serializers.ChoiceField(choices=AttachmentFormat.choices),
Expand Down Expand Up @@ -99,23 +128,19 @@ class Meta:
),
]

def validate(self, attrs: Options) -> Options:
# The email registration requires either `to_emails` or `to_emails_from_variable`
# to determine which email address to use.
# Both may be set - in that case, `to_emails_from_variable` is preferred.
if not attrs.get("to_emails") and not attrs.get("to_emails_from_variable"):
raise serializers.ValidationError(
{
"to_emails": _("This field is required."),
},
code="required",
)

class Options(TypedDict):
"""
Shape of the email registration plugin options.
This describes the shape of :attr:`EmailOptionsSerializer.validated_data`, after
the input data has been cleaned/validated.
"""

to_emails: list[str]
attachment_formats: NotRequired[list[AttachmentFormat | str]]
payment_emails: NotRequired[list[str]]
attach_files_to_email: bool | None
email_subject: NotRequired[str]
email_payment_subject: NotRequired[str]
email_content_template_html: NotRequired[str]
email_content_template_text: NotRequired[str]
return attrs


# sanity check for development - keep serializer and type definitions in sync
Expand Down
44 changes: 43 additions & 1 deletion src/openforms/registrations/tests/test_registration_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def test_registration_backend_invalid_options(self):
completed=True,
form__registration_backend="email",
form__registration_backend_options={},
) # Missing "to_email" option
) # Missing "to_emails" and "to_emails_from_variable" option

with (
self.subTest("On completion - does NOT raise"),
Expand All @@ -334,6 +334,48 @@ def test_registration_backend_invalid_options(self):
):
register_submission(submission.id, PostSubmissionEvents.on_retry)

def test_email_registration_backend_with_to_emails(self):
submission = SubmissionFactory.create(
completed=True,
form__registration_backend="email",
form__registration_backend_options={"to_emails": ["[email protected]"]},
)

with (
self.subTest("On completion - logs as successful"),
self.assertLogs(level="INFO") as logs,
):
register_submission(submission.id, PostSubmissionEvents.on_completion)

submission.refresh_from_db()
self.assertIn(
"Registration using plugin '%r' for submission '%s' succeeded",
logs.records[-1].msg,
)
self.assertFalse(submission.needs_on_completion_retry)

def test_email_registration_backend_with_to_emails_from_variable(self):
submission = SubmissionFactory.create(
completed=True,
form__registration_backend="email",
form__registration_backend_options={
"to_emails_from_variable": "email_example_variable"
},
)

with (
self.subTest("On completion - logs as successful"),
self.assertLogs(level="INFO") as logs,
):
register_submission(submission.id, PostSubmissionEvents.on_completion)

submission.refresh_from_db()
self.assertIn(
"Registration using plugin '%r' for submission '%s' succeeded",
logs.records[-1].msg,
)
self.assertFalse(submission.needs_on_completion_retry)

def test_calling_registration_task_with_serialized_args(self):
submission = SubmissionFactory.create(
completed=True,
Expand Down

0 comments on commit 473b0bb

Please sign in to comment.