-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor form processing into multiple processor subscribers
- Loading branch information
Showing
6 changed files
with
369 additions
and
332 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
backend/src/collective/volto/formsupport/processors/__init__.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
def filter_parameters(data, block): | ||
""" | ||
do not send attachments fields. | ||
""" | ||
# TODO: handle attachments for schemaForm block | ||
if block["@type"] == "schemaForm": | ||
return [{ | ||
"field_id": k, | ||
"value": v, | ||
"label": block["schema"]["properties"].get(k, {}).get("title", k), | ||
} for k, v in data["data"].items()] | ||
|
||
skip_fields = [ | ||
x.get("field_id", "") | ||
for x in block.get("subblocks", []) | ||
if x.get("field_type", "") == "attachment" | ||
] | ||
return [ | ||
x | ||
for x in data.get("data", []) | ||
if x.get("field_id", "") not in skip_fields | ||
] |
8 changes: 8 additions & 0 deletions
8
backend/src/collective/volto/formsupport/processors/configure.zcml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<configure | ||
xmlns="http://namespaces.zope.org/zope" | ||
> | ||
|
||
<subscriber name="email" factory=".email.EmailFormProcessor" /> | ||
<subscriber name="store" factory=".store.StoreFormProcessor" /> | ||
|
||
</configure> |
267 changes: 267 additions & 0 deletions
267
backend/src/collective/volto/formsupport/processors/email.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
from bs4 import BeautifulSoup | ||
from collective.volto.formsupport import _ | ||
from collective.volto.formsupport.processors import filter_parameters | ||
from collective.volto.formsupport.interfaces import FormSubmissionContext | ||
from collective.volto.formsupport.interfaces import IFormSubmissionProcessor | ||
from email import policy | ||
from email.message import EmailMessage | ||
from plone import api | ||
from plone.registry.interfaces import IRegistry | ||
from zExceptions import BadRequest | ||
from zope.component import adapter | ||
from zope.component import getMultiAdapter | ||
from zope.component import getUtility | ||
from zope.i18n import translate | ||
from zope.interface import implementer | ||
import codecs | ||
import os | ||
import re | ||
|
||
try: | ||
from plone.base.interfaces.controlpanel import IMailSchema | ||
except ImportError: | ||
from Products.CMFPlone.interfaces.controlpanel import IMailSchema | ||
|
||
CTE = os.environ.get("MAIL_CONTENT_TRANSFER_ENCODING", None) | ||
|
||
|
||
@implementer(IFormSubmissionProcessor) | ||
@adapter(FormSubmissionContext) | ||
class EmailFormProcessor: | ||
"""Sends an email with submitted form data""" | ||
|
||
order = 1 | ||
|
||
def __init__(self, context): | ||
self.context = context.context | ||
self.request = context.request | ||
self.block = context.block | ||
self.form_data = context.form_data | ||
|
||
def __call__(self): | ||
if not self.block.get("send"): | ||
return | ||
|
||
portal = api.portal.get() | ||
overview_controlpanel = getMultiAdapter( | ||
(portal, self.request), name="overview-controlpanel" | ||
) | ||
if overview_controlpanel.mailhost_warning(): | ||
raise BadRequest("MailHost is not configured.") | ||
registry = getUtility(IRegistry) | ||
mail_settings = registry.forInterface(IMailSchema, prefix="plone") | ||
charset = registry.get("plone.email_charset", "utf-8") | ||
|
||
subject = self.get_subject() | ||
|
||
# TODO | ||
mfrom = self.form_data.get("from", "") or self.block.get("default_from", "") or mail_settings.email_from_address | ||
mreply_to = self.get_reply_to() | ||
|
||
if not subject or not mfrom: | ||
raise BadRequest( | ||
translate( | ||
_( | ||
"send_required_field_missing", | ||
default="Missing required field: subject or from.", | ||
), | ||
context=self.request, | ||
) | ||
) | ||
|
||
# TODO sort out admin email vs acknowledgment | ||
send_to = self.block.get("send", ["recipient"]) | ||
if not isinstance(send_to, list): | ||
send_to = ["recipient"] if send_to else [] | ||
|
||
portal_transforms = api.portal.get_tool(name="portal_transforms") | ||
mto = self.block.get("recipients", mail_settings.email_from_address) | ||
message = self.prepare_message() | ||
text_message = ( | ||
portal_transforms.convertTo("text/plain", message, mimetype="text/html") | ||
.getData() | ||
.strip() | ||
) | ||
msg = EmailMessage(policy=policy.SMTP) | ||
msg.set_content(text_message, cte=CTE) | ||
msg.add_alternative(message, subtype="html", cte=CTE) | ||
msg["Subject"] = subject | ||
msg["From"] = mfrom | ||
msg["To"] = mto | ||
msg["Reply-To"] = mreply_to | ||
|
||
headers_to_forward = self.block.get("httpHeaders", []) | ||
for header in headers_to_forward: | ||
header_value = self.request.get(header) | ||
if header_value: | ||
msg[header] = header_value | ||
|
||
self.manage_attachments(msg=msg) | ||
|
||
if "recipient" in send_to: | ||
self.send_mail(msg=msg, charset=charset) | ||
|
||
# send a copy also to the fields with bcc flag | ||
for bcc in self.get_bcc(): | ||
msg.replace_header("To", bcc) | ||
self.send_mail(msg=msg, charset=charset) | ||
|
||
acknowledgement_message = self.block.get("acknowledgementMessage") | ||
if acknowledgement_message and "acknowledgement" in send_to: | ||
acknowledgement_address = self.get_acknowledgement_field_value() | ||
if acknowledgement_address: | ||
acknowledgement_mail = EmailMessage(policy=policy.SMTP) | ||
acknowledgement_mail["Subject"] = subject | ||
acknowledgement_mail["From"] = mfrom | ||
acknowledgement_mail["To"] = acknowledgement_address | ||
ack_msg = acknowledgement_message.get("data") | ||
ack_msg_text = ( | ||
portal_transforms.convertTo( | ||
"text/plain", ack_msg, mimetype="text/html" | ||
) | ||
.getData() | ||
.strip() | ||
) | ||
acknowledgement_mail.set_content(ack_msg_text, cte=CTE) | ||
acknowledgement_mail.add_alternative(ack_msg, subtype="html", cte=CTE) | ||
self.send_mail(msg=acknowledgement_mail, charset=charset) | ||
|
||
def get_reply_to(self): | ||
"""This method retrieves the 'reply to' email address. | ||
Three "levels" of logic: | ||
1. If there is a field marked with 'use_as_reply_to' set to True, that | ||
field wins and we use that. | ||
If not: | ||
2. We search for the "from" field. | ||
If not present: | ||
3. We use the fallback field: "default_from" | ||
""" | ||
|
||
subblocks = self.block.get("subblocks", "") | ||
if subblocks: | ||
for field in subblocks: | ||
if field.get("use_as_reply_to", False): | ||
field_id = field.get("field_id", "") | ||
if field_id: | ||
for data in data.get("data", ""): | ||
if data.get("field_id", "") == field_id: | ||
return data.get("value", "") | ||
|
||
return self.form_data.get("from", "") or self.block.get("default_from", "") | ||
|
||
def get_subject(self): | ||
subject = self.block.get("subject") or "${subject}" | ||
subject = self.substitute_variables(subject) | ||
return subject | ||
|
||
def substitute_variables(self, value): | ||
pattern = r"\$\{([^}]+)\}" | ||
return re.sub(pattern, lambda match: self.get_value(match.group(1), ""), value) | ||
|
||
def get_value(self, field_id, default=None): | ||
if self.block.get("@type") == "schemaForm": | ||
return self.form_data["data"].get(field_id, default) | ||
|
||
for field in self.form_data.get("data", []): | ||
if field.get("field_id") == field_id: | ||
return field.get("value", default) | ||
return default | ||
|
||
def get_bcc(self): | ||
# todo: handle bcc for schemaForm | ||
subblocks = self.block.get("subblocks", []) | ||
if not subblocks: | ||
return [] | ||
|
||
bcc = [] | ||
bcc_fields = [] | ||
for field in self.block.get("subblocks", []): | ||
if field.get("use_as_bcc", False): | ||
field_id = field.get("field_id", "") | ||
if field_id not in bcc_fields: | ||
bcc_fields.append(field_id) | ||
bcc = [] | ||
for field in self.form_data.get("data", []): | ||
value = field.get("value", "") | ||
if not value: | ||
continue | ||
if field.get("field_id", "") in bcc_fields: | ||
bcc.append(field["value"]) | ||
return bcc | ||
|
||
def prepare_message(self): | ||
mail_header = self.block.get("mail_header", {}).get("data", "") | ||
mail_footer = self.block.get("mail_footer", {}).get("data", "") | ||
|
||
# Check if there is content | ||
mail_header = BeautifulSoup(mail_header).get_text() if mail_header else None | ||
mail_footer = BeautifulSoup(mail_footer).get_text() if mail_footer else None | ||
|
||
# TODO | ||
email_format_page_template_mapping = { | ||
"list": "send_mail_template", | ||
"table": "send_mail_template_table", | ||
} | ||
email_format = self.block.get("email_format", "") | ||
template_name = email_format_page_template_mapping.get( | ||
email_format, "send_mail_template" | ||
) | ||
|
||
message_template = api.content.get_view( | ||
name=template_name, | ||
context=self.context, | ||
request=self.request, | ||
) | ||
parameters = { | ||
"parameters": filter_parameters(self.form_data, self.block), | ||
"url": self.context.absolute_url(), | ||
"title": self.context.Title(), | ||
"mail_header": mail_header, | ||
"mail_footer": mail_footer, | ||
} | ||
return message_template(**parameters) | ||
|
||
def manage_attachments(self, msg): | ||
attachments = self.form_data.get("attachments", {}) | ||
|
||
if not attachments: | ||
return [] | ||
for _key, value in attachments.items(): | ||
content_type = "application/octet-stream" | ||
filename = None | ||
if isinstance(value, dict): | ||
file_data = value.get("data", "") | ||
if not file_data: | ||
continue | ||
content_type = value.get("content-type", content_type) | ||
filename = value.get("filename", filename) | ||
if isinstance(file_data, str): | ||
file_data = file_data.encode("utf-8") | ||
if "encoding" in value: | ||
file_data = codecs.decode(file_data, value["encoding"]) | ||
if isinstance(file_data, str): | ||
file_data = file_data.encode("utf-8") | ||
else: | ||
file_data = value | ||
maintype, subtype = content_type.split("/") | ||
msg.add_attachment( | ||
file_data, | ||
maintype=maintype, | ||
subtype=subtype, | ||
filename=filename, | ||
) | ||
|
||
def send_mail(self, msg, charset): | ||
host = api.portal.get_tool(name="MailHost") | ||
# we set immediate=True because we need to catch exceptions. | ||
# by default (False) exceptions are handled by MailHost and we can't catch them. | ||
host.send(msg, charset=charset, immediate=True) | ||
|
||
def get_acknowledgement_field_value(self): | ||
acknowledgementField = self.block["acknowledgementFields"] | ||
for field in self.block.get("subblocks", []): | ||
if field.get("field_id") == acknowledgementField: | ||
for submitted in self.form_data.get("data", []): | ||
if submitted.get("field_id", "") == field.get("field_id"): | ||
return submitted.get("value") |
31 changes: 31 additions & 0 deletions
31
backend/src/collective/volto/formsupport/processors/store.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
from collective.volto.formsupport.interfaces import IFormDataStore | ||
from collective.volto.formsupport.interfaces import IFormSubmissionProcessor | ||
from collective.volto.formsupport.interfaces import FormSubmissionContext | ||
from collective.volto.formsupport.processors import filter_parameters | ||
from zExceptions import BadRequest | ||
from zope.component import adapter | ||
from zope.component import getMultiAdapter | ||
from zope.interface import implementer | ||
|
||
|
||
@implementer(IFormSubmissionProcessor) | ||
@adapter(FormSubmissionContext) | ||
class StoreFormProcessor: | ||
"""Stores submitted form data""" | ||
|
||
order = 2 | ||
|
||
def __init__(self, context): | ||
self.context = context.context | ||
self.request = context.request | ||
self.block = context.block | ||
self.form_data = context.form_data | ||
|
||
def __call__(self): | ||
if not self.block.get("store"): | ||
return | ||
|
||
store = getMultiAdapter((self.context, self.request), IFormDataStore) | ||
res = store.add(data=filter_parameters(self.form_data, self.block)) | ||
if not res: | ||
raise BadRequest("Unable to store data") |
Oops, something went wrong.