diff --git a/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsForm.js b/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsForm.js index fb6a8d8608..3871a0c8d8 100644 --- a/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsForm.js +++ b/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsForm.js @@ -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, }; diff --git a/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsFormFields.js b/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsFormFields.js index ae83aadfbd..d6692f05b3 100644 --- a/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsFormFields.js +++ b/src/openforms/js/components/admin/form_design/registrations/email/EmailOptionsFormFields.js @@ -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}) => { @@ -37,6 +38,7 @@ const EmailOptionsFormFields = ({name, schema}) => {
+ diff --git a/src/openforms/js/components/admin/form_design/registrations/email/fields/EmailRecipientsFromVariable.js b/src/openforms/js/components/admin/form_design/registrations/email/fields/EmailRecipientsFromVariable.js new file mode 100644 index 0000000000..565092b7a6 --- /dev/null +++ b/src/openforms/js/components/admin/form_design/registrations/email/fields/EmailRecipientsFromVariable.js @@ -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 ( + + + } + helpText={ + + } + > + { + setValue(event.target.value); + }} + /> + + + ); +}; + +EmailRecipientsFromVariable.propTypes = {}; + +export default EmailRecipientsFromVariable; diff --git a/src/openforms/registrations/contrib/email/config.py b/src/openforms/registrations/contrib/email/config.py index 7cda616e80..f309d2a8a3 100644 --- a/src/openforms/registrations/contrib/email/config.py +++ b/src/openforms/registrations/contrib/email/config.py @@ -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), @@ -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 diff --git a/src/openforms/registrations/tests/test_registration_hook.py b/src/openforms/registrations/tests/test_registration_hook.py index 65791261ec..201130f7c5 100644 --- a/src/openforms/registrations/tests/test_registration_hook.py +++ b/src/openforms/registrations/tests/test_registration_hook.py @@ -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"), @@ -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": ["foo@example.com"]}, + ) + + 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,