Skip to content

Commit

Permalink
Handle attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
davisagli committed Sep 24, 2024
1 parent eb20843 commit dec0e7e
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 447 deletions.
20 changes: 3 additions & 17 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,19 +126,7 @@ or store it into an internal catalog (or both).

If block is set to send data, an email with form data will be sent to the recipient set in block settings or (if not set) to the site address.

If there is an `attachments` field in the POST data, these files will be attached to the email sent.

#### XML attachments

An XML copy of the data can be optionally attached to the sent email by configuring the volto block's `attachXml` option.

The sent XML follows the same format as the feature in [collective.easyform](https://github.com/collective/collective.easyform). An example is shown below:

```xml
<?xml version='1.0' encoding='utf-8'?><form><field name="Custom field label">My value</field></form>
```

The field names in the XML will utilise the Data ID Mapping feature if it is used. Read more about this feature in the following Store section of the documentation.
If there are file upload fields in the POST data, these files will be attached to the email sent.

#### Acknowledgement email

Expand Down Expand Up @@ -220,9 +208,9 @@ If honeypot dependency is available in the buildout, the honeypot validation is

Default field name is `protected_1` and you can change it with an environment variable. See `collective.honeypot <https://github.com/collective/collective.honeypot#id7>`\_ for details.

## Attachments upload limits
## File upload limits

Forms can have one or more attachment field to allow users to upload some files.
Forms can have one or more file upload fields to allow users to upload some files.

These files will be sent via mail, so it could be a good idea setting a limit to them.
For example if you use Gmail as mail server, you can't send messages with attachments > 25MB.
Expand All @@ -237,8 +225,6 @@ There is an environment variable that you can use to set that limit (in MB):

By default this is not set.

The upload limit is also passed to the frontend in the form data with the `attachments_limit` key.

## Content-transfer-encoding

It is possible to set the content-transfer-encoding for the email body, settings the environment
Expand Down
27 changes: 26 additions & 1 deletion backend/src/collective/volto/formsupport/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,34 @@ class FormSubmissionContext:
context: DexterityContent
block: dict
form_data: dict
attachments: dict
request: BaseRequest

def get_records(self) -> list:
"""
Return field id, value, and label.
Skips file upload fields.
"""
records = []
for k, v in self.form_data.items():
field = self.block["schema"]["properties"].get(k, {})
if field.get("type") == "object":
continue
records.append({
"field_id": k,
"value": v,
"label": field.get("title", k),
})
return records

def get_attachments(self) -> dict:
attachments = {}
for k, v in self.form_data.items():
field = self.block["schema"]["properties"].get(k, {})
if field.get("factory") == "File Upload":
attachments[k] = v
return attachments


class IFormSubmissionProcessor(Interface):
"""Subscriber which processes form data when it is submitted"""
Expand Down
12 changes: 0 additions & 12 deletions backend/src/collective/volto/formsupport/processors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +0,0 @@
def filter_parameters(data, block):
"""
TODO do not send attachments fields.
"""
return [
{
"field_id": k,
"value": v,
"label": block["schema"]["properties"].get(k, {}).get("title", k),
}
for k, v in data.items()
]
23 changes: 13 additions & 10 deletions backend/src/collective/volto/formsupport/processors/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from collective.volto.formsupport import _
from collective.volto.formsupport.interfaces import FormSubmissionContext
from collective.volto.formsupport.interfaces import IFormSubmissionProcessor
from collective.volto.formsupport.processors import filter_parameters
from email import policy
from email.message import EmailMessage
from plone import api
Expand Down Expand Up @@ -39,7 +38,8 @@ def __init__(self, context: FormSubmissionContext):
self.request = context.request
self.block = context.block
self.form_data = context.form_data
self.attachments = context.attachments
self.records = context.get_records()
self.attachments = context.get_attachments()

def __call__(self):
if not self.block.get("send"):
Expand Down Expand Up @@ -93,7 +93,7 @@ def __call__(self):
if header_value:
msg[header] = header_value

self.manage_attachments(msg=msg)
self.add_attachments(msg=msg)

self.send_mail(msg=msg, charset=charset)

Expand Down Expand Up @@ -128,7 +128,12 @@ def get_reply_to(self):
return sender

def get_subject(self):
subject = self.block.get("subject") or "${subject}"
subject = self.block.get("subject")
if not subject:
if "subject" in self.block["schema"].get("properties", {}):
subject = "${subject}"
else:
subject = self.block.get("title") or "Form Submission"
subject = self.substitute_variables(subject)
return subject

Expand Down Expand Up @@ -168,20 +173,18 @@ def prepare_message(self):
request=self.request,
)
parameters = {
"parameters": filter_parameters(self.form_data, self.block),
"parameters": self.records,
"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.attachments

if not attachments:
def add_attachments(self, msg):
if not self.attachments:
return []
for _key, value in attachments.items():
for _key, value in self.attachments.items():
content_type = "application/octet-stream"
filename = None
if isinstance(value, dict):
Expand Down
4 changes: 2 additions & 2 deletions backend/src/collective/volto/formsupport/processors/store.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from collective.volto.formsupport.interfaces import FormSubmissionContext
from collective.volto.formsupport.interfaces import IFormDataStore
from collective.volto.formsupport.interfaces import IFormSubmissionProcessor
from collective.volto.formsupport.processors import filter_parameters
from zExceptions import BadRequest
from zope.component import adapter
from zope.component import getMultiAdapter
Expand All @@ -20,12 +19,13 @@ def __init__(self, context):
self.request = context.request
self.block = context.block
self.form_data = context.form_data
self.records = context.get_records()

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))
res = store.add(data=self.records)
if not res:
raise BadRequest("Unable to store data")
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ def reply(self):
if self.block_id:
self.block = self.get_block_data(block_id=self.block_id)
self.form_data = self.cleanup_data()
self.form_submission_context = FormSubmissionContext(
context=self.context,
request=self.request,
block=self.block,
form_data=self.form_data,
)

self.validate_form()

Expand All @@ -48,15 +54,8 @@ def reply(self):

notify(PostEventService(self.context, self.body))

form_submission_context = FormSubmissionContext(
context=self.context,
request=self.request,
block=self.block,
form_data=self.form_data,
attachments=self.body.get("attachments", {}),
)
for handler in sorted(
subscribers((form_submission_context,), IFormSubmissionProcessor),
subscribers((self.form_submission_context,), IFormSubmissionProcessor),
key=lambda h: h.order,
):
try:
Expand All @@ -83,7 +82,8 @@ def cleanup_data(self):
schema = self.block.get("schema", {})
form_data = self.body.get("data", {})
if not isinstance(form_data, dict):
raise BadRequest(translate(
raise BadRequest(
translate(
_(
"invalid_form_data",
default="Invalid form data.",
Expand Down Expand Up @@ -153,11 +153,10 @@ def validate_schema(self):
raise BadRequest(json.dumps(errors))

def validate_attachments(self):
# TODO handle schemaForm attachments
attachments_limit = os.environ.get("FORM_ATTACHMENTS_LIMIT", "")
if not attachments_limit:
return
attachments = self.body.get("attachments", {})
attachments = self.form_submission_context.get_attachments()
attachments_len = 0
for attachment in attachments.values():
data = attachment.get("data", "")
Expand Down
Loading

0 comments on commit dec0e7e

Please sign in to comment.