From 8503118bed2a3539d6dafdbde508201f20ac5dc4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Wed, 8 Apr 2020 11:32:03 +0200
Subject: [PATCH] mail templates: add possibility to pre-set attachments
 (#41319)

---
 tests/test_mail_templates.py | 65 ++++++++++++++++++++++++++++++++++++
 wcs/admin/mail_templates.py  | 13 +++++++-
 wcs/mail_templates.py        |  2 ++
 wcs/qommon/xml_storage.py    | 10 ++++++
 wcs/workflows.py             | 14 +++++---
 5 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/tests/test_mail_templates.py b/tests/test_mail_templates.py
index 6cbbf4b0f..afd814e39 100644
--- a/tests/test_mail_templates.py
+++ b/tests/test_mail_templates.py
@@ -7,10 +7,12 @@ import pytest
 from django.utils.encoding import force_bytes
 from quixote import cleanup
 from wcs.formdef import FormDef
+from wcs.fields import FileField
 from wcs.logged_errors import LoggedError
 from wcs.mail_templates import MailTemplate
 from wcs.workflows import Workflow, SendmailWorkflowStatusItem
 from wcs.qommon.http_request import HTTPRequest
+from wcs.qommon.form import PicklableUpload
 from wcs.qommon.ident.password_accounts import PasswordAccount
 
 from utilities import (get_app, login, create_temporary_pub, clean_temporary_pub)
@@ -98,6 +100,13 @@ def test_mail_templates_basics(pub, superuser, mail_templates_option):
     resp = resp.click('Edit')
     resp.form['subject'] = 'edited mail subject'
     resp.form['body'] = 'edited mail body'
+    resp.form['attachments$element0'] = 'plop'
+    resp = resp.form.submit('submit').follow()
+
+    resp = resp.click('Edit')
+    assert resp.form['subject'].value == 'edited mail subject'
+    assert resp.form['body'].value == 'edited mail body'
+    assert resp.form['attachments$element0'].value == 'plop'
     resp = resp.form.submit('submit').follow()
 
     resp = resp.click('Delete')
@@ -220,3 +229,59 @@ def test_workflow_send_mail_template(pub, superuser, mail_templates_option, emai
     assert LoggedError.count() == 1
     logged_error = LoggedError.select()[0]
     assert logged_error.summary == 'reference to invalid mail template test-mail-template in status Status1'
+
+
+def test_workflow_send_mail_template_attachments(pub, superuser, mail_templates_option, emails):
+    Workflow.wipe()
+    MailTemplate.wipe()
+
+    mail_template = MailTemplate(name='test mail template')
+    mail_template.subject = 'test subject'
+    mail_template.body = 'test body'
+    mail_template.attachments = ['form_var_file1_raw']
+    mail_template.store()
+
+    workflow = Workflow(name='test mail template')
+    st1 = workflow.add_status('Status1')
+    item = SendmailWorkflowStatusItem()
+    item.to = 'xyz@localhost'
+    item.subject = 'Foobar'
+    item.body = 'Hello'
+    item.mail_template = mail_template.slug
+    st1.items.append(item)
+    item.parent = st1
+    workflow.store()
+
+    formdef = FormDef()
+    formdef.name = 'baz'
+    formdef.fields = [
+        FileField(id='1', label='File', type='file', varname='file1'),
+    ]
+    formdef.store()
+
+    upload = PicklableUpload('test.jpeg', 'image/jpeg')
+    jpg = open(os.path.join(os.path.dirname(__file__), 'image-with-gps-data.jpeg'), 'rb').read()
+    upload.receive([jpg])
+    formdata = formdef.data_class()()
+    formdata.data = {'1': upload}
+    formdata.just_created()
+    formdata.store()
+    pub.substitutions.feed(formdata)
+
+    item.perform(formdata)
+    pub.get_request().response.process_after_jobs()
+    assert emails.count() == 1
+    payloads = emails.get('test subject')['msg'].get_payload()
+    assert len(payloads) == 2
+    assert payloads[1].get_content_type() == 'image/jpeg'
+
+    # check two files are sent if mail attachments are also defined on the
+    # action itself.
+    emails.empty()
+    item.attachments = ['form_var_file1_raw']
+    item.perform(formdata)
+    pub.get_request().response.process_after_jobs()
+    payloads = emails.get('test subject')['msg'].get_payload()
+    assert len(payloads) == 3
+    assert payloads[1].get_content_type() == 'image/jpeg'
+    assert payloads[2].get_content_type() == 'image/jpeg'
diff --git a/wcs/admin/mail_templates.py b/wcs/admin/mail_templates.py
index 70716599a..19ed74155 100644
--- a/wcs/admin/mail_templates.py
+++ b/wcs/admin/mail_templates.py
@@ -20,7 +20,8 @@ from quixote.html import TemplateIO, htmltext
 
 from wcs.qommon import _, errors, template
 from wcs.qommon.backoffice.menu import html_top
-from wcs.qommon.form import Form, HtmlWidget, StringWidget, TextWidget, ComputedExpressionWidget
+from wcs.qommon.form import (Form, HtmlWidget, StringWidget, TextWidget,
+        ComputedExpressionWidget, WidgetList)
 from wcs.mail_templates import MailTemplate
 
 
@@ -100,6 +101,15 @@ class MailTemplatePage(Directory):
                     title=_('Identifier'),
                     required=True, advanced=True,
                     )
+
+        form.add(WidgetList, 'attachments',
+                title=_('Attachments (Python expressions)'),
+                element_type=StringWidget,
+                value=self.mail_template.attachments,
+                add_element_label=_('Add attachment'),
+                element_kwargs={'render_br': False, 'size': 50},
+                advanced=True)
+
         form.add_submit('submit', _('Submit'))
         form.add_submit('cancel', _('Cancel'))
         return form
@@ -122,6 +132,7 @@ class MailTemplatePage(Directory):
         self.mail_template.description = form.get_widget('description').parse()
         self.mail_template.subject = form.get_widget('subject').parse()
         self.mail_template.body = form.get_widget('body').parse()
+        self.mail_template.attachments = form.get_widget('attachments').parse()
         if slug_widget:
             self.mail_template.slug = slug
         self.mail_template.store()
diff --git a/wcs/mail_templates.py b/wcs/mail_templates.py
index e593f06f4..4507e7944 100644
--- a/wcs/mail_templates.py
+++ b/wcs/mail_templates.py
@@ -27,6 +27,7 @@ class MailTemplate(XmlStorableObject):
     description = None
     subject = None
     body = None
+    attachments = []
 
     # declarations for serialization
     XML_NODES = [
@@ -35,6 +36,7 @@ class MailTemplate(XmlStorableObject):
             ('description', 'str'),
             ('subject', 'str'),
             ('body', 'str'),
+            ('attachments', 'str_list'),
     ]
 
     def __init__(self, name=None):
diff --git a/wcs/qommon/xml_storage.py b/wcs/qommon/xml_storage.py
index 1dd0705dc..61069a801 100644
--- a/wcs/qommon/xml_storage.py
+++ b/wcs/qommon/xml_storage.py
@@ -67,6 +67,10 @@ class XmlStorableObject(StorableObject):
     def export_datetime_to_xml(self, element, attribute_name, charset):
         element.text = getattr(self, attribute_name).isoformat()
 
+    def export_str_list_to_xml(self, element, attribute_name, charset):
+        for item in getattr(self, attribute_name, None) or []:
+            ET.SubElement(element, 'item').text = item
+
     def export_to_xml_string(self, include_id=False):
         x = self.export_to_xml(include_id=include_id)
         indent_xml(x)
@@ -116,3 +120,9 @@ class XmlStorableObject(StorableObject):
 
     def import_datetime_from_xml(self, element, charset):
         return datetime.datetime.strptime(element.text[:19], '%Y-%m-%dT%H:%M:%S')
+
+    def import_str_list_from_xml(self, element, charset):
+        value = []
+        for item in element.findall('item'):
+            value.append(item.text)
+        return value
diff --git a/wcs/workflows.py b/wcs/workflows.py
index 7100a741a..6cb4a8418 100644
--- a/wcs/workflows.py
+++ b/wcs/workflows.py
@@ -2075,13 +2075,17 @@ class WorkflowStatusItem(XmlSerialisable):
                            (attachment in varnameless))]
         return attachments_options, attachments
 
-    def convert_attachments_to_uploads(self):
+    def convert_attachments_to_uploads(self, extra_attachments=None):
         uploads = []
 
-        if self.attachments:
+        attachments = []
+        attachments.extend(self.attachments or [])
+        attachments.extend(extra_attachments or [])
+
+        if attachments:
             global_eval_dict = get_publisher().get_global_eval_dict()
             local_eval_dict = get_publisher().substitutions.get_context_variables()
-            for attachment in self.attachments:
+            for attachment in attachments:
                 if attachment.startswith('form_fbo') and '-' in attachment:
                     # detect varname-less backoffice fields that were set
                     # before #33366 was fixed, and fix them.
@@ -2563,11 +2567,13 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
         url = formdata.get_url()
         body = self.body
         subject = self.subject
+        extra_attachments = None
         if self.mail_template:
             mail_template = MailTemplate.get_by_slug(self.mail_template)
             if mail_template:
                 body = mail_template.body
                 subject = mail_template.subject
+                extra_attachments = mail_template.attachments
             else:
                 from wcs.logged_errors import LoggedError
                 message = _('reference to invalid mail template %(mail_template)s in status %(status)s') % {
@@ -2647,7 +2653,7 @@ class SendmailWorkflowStatusItem(WorkflowStatusItem):
         if self.custom_from:
             email_from = self.compute(self.custom_from)
 
-        attachments = self.convert_attachments_to_uploads()
+        attachments = self.convert_attachments_to_uploads(extra_attachments)
 
         if len(addresses) > 1:
             emails.email(mail_subject, mail_body, email_rcpt=None,
-- 
2.26.0

