Skip to content

Commit

Permalink
XML Sending and custom field mapping (#22)
Browse files Browse the repository at this point in the history
* Allow sending XML

* Fix serialization bug

* Use custom_field_id if it's available when sending an XML

* Allow storing custom_field_id in-place of a field's label

* Bump dev version

* Fix attachment sending

* Changelog

* Docs

* Add test for XML sending

* Add test for field renaming

---------

Co-authored-by: Andrea Cecchi <[email protected]>
  • Loading branch information
JeffersonBledsoe and cekk authored May 23, 2023
1 parent cf4b239 commit 15101d6
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog
2.7.1 (unreleased)
------------------

- Allow attaching an XML version of the form data to the sent email #22
[JeffersonBledsoe]
- Allow the IDs of fields to be customised for CSV download and XML attaachments #22
[JeffersonBledsoe]
- Add Spanish translation.
[macagua]

Expand Down
22 changes: 20 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
:target: https://pypi.python.org/pypi/collective.volto.formsupport
:alt: Egg Status

.. image:: https://img.shields.io/pypi/pyversions/collective.volto.formsupport.svg?style=plastic
.. image:: https://img.shields.io/pypi/pyversions/collective.volto.formsupport.svg?style=plastic
:target: https://pypi.python.org/pypi/collective.volto.formsupport/
:alt: Supported - Python Versions

Expand Down Expand Up @@ -104,7 +104,20 @@ Send

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 ther is an ``attachments`` field in the POST data, these files will be attached to the emal sent.
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.

Store
-----
Expand All @@ -122,6 +135,11 @@ Each Record stores also two *service* attributes:

We store these attributes because the form can change over time and we want to have a snapshot of the fields in the Record.

Data ID Mapping
^^^^^^^^^^^^^^^

The exported CSV file may need to be used by further processes which require specific values for the columns of the CSV. In such a case, the `Data ID Mapping` feature can be used to change the column name to custom text for each field.

Block serializer
================

Expand Down
15 changes: 13 additions & 2 deletions src/collective/volto/formsupport/datamanager/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@ def get_form_fields(self):
form_block = deepcopy(block)
if not form_block:
return {}
return form_block.get("subblocks", [])

subblocks = form_block.get("subblocks", [])

# Add the 'custom_field_id' field back in as this isn't stored with each subblock
for index, field in enumerate(subblocks):
if form_block.get(field["field_id"]):
subblocks[index]["custom_field_id"] = form_block.get(field["field_id"])

return subblocks

def add(self, data):
form_fields = self.get_form_fields()
Expand All @@ -72,7 +80,10 @@ def add(self, data):
)
return None

fields = {x["field_id"]: x.get("label", x["field_id"]) for x in form_fields}
fields = {
x["field_id"]: x.get("custom_field_id", x.get("label", x["field_id"]))
for x in form_fields
}
record = Record()
fields_labels = {}
fields_order = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from collective.volto.formsupport.interfaces import IFormDataStore
from collective.volto.formsupport.interfaces import IPostEvent
from collective.volto.formsupport.utils import get_blocks
from datetime import datetime
from email.message import EmailMessage
from plone import api
from plone.protect.interfaces import IDisableCSRFProtection
from plone.registry.interfaces import IRegistry
from plone.restapi.deserializer import json_body
from plone.restapi.services import Service
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
from xml.etree.ElementTree import ElementTree, Element, SubElement
from zExceptions import BadRequest
from zope.component import getMultiAdapter
from zope.component import getUtility
Expand Down Expand Up @@ -311,6 +313,10 @@ def send_mail(self, msg, charset):

def manage_attachments(self, msg):
attachments = self.form_data.get("attachments", {})

if self.block.get("attachXml", False):
self.attach_xml(msg=msg)

if not attachments:
return []
for key, value in attachments.items():
Expand All @@ -330,13 +336,40 @@ def manage_attachments(self, msg):
file_data = file_data.encode("utf-8")
else:
file_data = value
maintype, subtype = content_type.split("/")
msg.add_attachment(
file_data,
maintype=content_type,
subtype=content_type,
maintype=maintype,
subtype=subtype,
filename=filename,
)

def attach_xml(self, msg):
now = (
datetime.now()
.isoformat(timespec="seconds")
.replace(" ", "-")
.replace(":", "")
)
filename = f"formdata_{now}.xml"
output = six.BytesIO()
xmlRoot = Element("form")

for field in self.filter_parameters():
SubElement(
xmlRoot, "field", name=field.get("custom_field_id", field["label"])
).text = str(field["value"])

doc = ElementTree(xmlRoot)
doc.write(output, encoding="utf-8", xml_declaration=True)
xmlstr = output.getvalue()
msg.add_attachment(
xmlstr,
maintype="application",
subtype="xml",
filename=filename,
)

def store_data(self):
store = getMultiAdapter((self.context, self.request), IFormDataStore)
res = store.add(data=self.filter_parameters())
Expand Down
37 changes: 37 additions & 0 deletions src/collective/volto/formsupport/tests/test_send_action_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collective.volto.formsupport.testing import ( # noqa: E501,
VOLTO_FORMSUPPORT_API_FUNCTIONAL_TESTING,
)
from email.parser import Parser
from plone import api
from plone.app.testing import setRoles
from plone.app.testing import SITE_OWNER_NAME
Expand All @@ -10,6 +11,8 @@
from plone.registry.interfaces import IRegistry
from plone.restapi.testing import RelativeSession
from Products.MailHost.interfaces import IMailHost
from six import StringIO
import xml.etree.ElementTree as ET
from zope.component import getUtility

import transaction
Expand Down Expand Up @@ -579,3 +582,37 @@ def test_send_attachment_validate_size(
response.json()["message"],
)
self.assertEqual(len(self.mailhost.messages), 0)

def test_send_xml(self):
self.document.blocks = {
"form-id": {"@type": "form", "send": True, "attachXml": True},
}
transaction.commit()

form_data = [
{"label": "Message", "value": "just want to say hi"},
{"label": "Name", "value": "John"},
]

response = self.submit_form(
data={
"from": "[email protected]",
"data": form_data,
"subject": "test subject",
"block_id": "form-id",
},
)
transaction.commit()
self.assertEqual(response.status_code, 204)
msg = self.mailhost.messages[0]
if isinstance(msg, bytes) and bytes is not str:
# Python 3 with Products.MailHost 4.10+
msg = msg.decode("utf-8")

parsed_msgs = Parser().parse(StringIO(msg))
# 1st index is the XML attachment
msg_contents = parsed_msgs.get_payload()[1].get_payload(decode=True)
xml_tree = ET.fromstring(msg_contents)
for index, field in enumerate(xml_tree):
self.assertEqual(field.get("name"), form_data[index]['label'])
self.assertEqual(field.text, form_data[index]['value'])
60 changes: 60 additions & 0 deletions src/collective/volto/formsupport/tests/test_store_action_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,63 @@ def test_export_csv(self):
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
self.assertTrue(sorted_data[0][-1].startswith(now))
self.assertTrue(sorted_data[1][-1].startswith(now))

def test_data_id_mapping(self):
self.document.blocks = {
"form-id": {
"@type": "form",
"store": True,
"test-field": "renamed-field",
"subblocks": [
{
"field_id": "message",
"label": "Message",
"field_type": "text",
},
{
"field_id": "test-field",
"label": "Test field",
"field_type": "text",
},
],
},
}
transaction.commit()
response = self.submit_form(
data={
"from": "[email protected]",
"data": [
{"field_id": "message", "value": "just want to say hi"},
{"field_id": "test-field", "value": "John"},
],
"subject": "test subject",
"block_id": "form-id",
},
)

response = self.submit_form(
data={
"from": "[email protected]",
"data": [
{"field_id": "message", "value": "bye"},
{"field_id": "test-field", "value": "Sally"},
],
"subject": "test subject",
"block_id": "form-id",
},
)

self.assertEqual(response.status_code, 204)
response = self.export_csv()
data = [*csv.reader(StringIO(response.text), delimiter=",")]
self.assertEqual(len(data), 3)
# Check that 'test-field' got renamed
self.assertEqual(data[0], ["Message", "renamed-field", "date"])
sorted_data = sorted(data[1:])
self.assertEqual(sorted_data[0][:-1], ["bye", "Sally"])
self.assertEqual(sorted_data[1][:-1], ["just want to say hi", "John"])

# check date column. Skip seconds because can change during test
now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M")
self.assertTrue(sorted_data[0][-1].startswith(now))
self.assertTrue(sorted_data[1][-1].startswith(now))

0 comments on commit 15101d6

Please sign in to comment.