Repository: carpentries/amy

Issue:
Auto-response to workshop request
When someone submits a [workshop request form](https://amy.carpentries.org/forms/workshop/) 
they should get an email back with the following text:

> Thank you for requesting a Carpentries workshop!  Your request has been received, and a member of our team will get back to you within three business days. If you have any immediate questions, please contact us at team@carpentries.org.  A copy of your request is included below for your reference.  

(and then include the request content)

This is in addition to the emails that go to the admin-xx@carpentries.org based on geography.

The emails that go to admin-xx@carpentries.org have a link to view the request in AMY; these should be no such link for the email that goes to the original requester.  



Discussion:

Resolving patch:
diff --git a/amy/extforms/views.py b/amy/extforms/views.py
--- a/amy/extforms/views.py
+++ b/amy/extforms/views.py
@@ -35,8 +35,10 @@ class TrainingRequestCreate(
     form_class = TrainingRequestForm
     template_name = 'forms/trainingrequest.html'
     success_url = reverse_lazy('training_request_confirm')
-    email_subject = 'Thank you for your application'
-    email_body_template = 'mailing/training_request.txt'
+    autoresponder_subject = 'Thank you for your application'
+    autoresponder_body_template_txt = 'mailing/training_request.txt'
+    autoresponder_body_template_html = 'mailing/training_request.html'
+    autoresponder_form_field = 'email'
 
     def get_success_message(self, *args, **kwargs):
         """Don't display a success message."""
@@ -65,6 +67,7 @@ def get_context_data(self, **kwargs):
 class WorkshopRequestCreate(
     LoginNotRequiredMixin,
     EmailSendMixin,
+    AutoresponderMixin,
     AMYCreateView,
 ):
     model = WorkshopRequest
@@ -74,6 +77,14 @@ class WorkshopRequestCreate(
     success_url = reverse_lazy('workshop_request_confirm')
     email_fail_silently = False
 
+    autoresponder_subject = 'Workshop request confirmation'
+    autoresponder_body_template_txt = 'mailing/workshoprequest.txt'
+    autoresponder_body_template_html = 'mailing/workshoprequest.html'
+    autoresponder_form_field = 'email'
+
+    def autoresponder_email_context(self, form):
+        return dict(object=self.object)
+
     def get_success_message(self, *args, **kwargs):
         """Don't display a success message."""
         return ''
@@ -108,7 +119,7 @@ def get_body(self):
         link_domain = 'https://{}'.format(get_current_site(self.request))
 
         body_txt = get_template(
-            'mailing/workshoprequest.txt'
+            'mailing/workshoprequest_admin.txt'
         ).render({
             'object': self.object,
             'link': link,
@@ -116,7 +127,7 @@ def get_body(self):
         })
 
         body_html = get_template(
-            'mailing/workshoprequest.html'
+            'mailing/workshoprequest_admin.html'
         ).render({
             'object': self.object,
             'link': link,
diff --git a/amy/workshops/base_views.py b/amy/workshops/base_views.py
--- a/amy/workshops/base_views.py
+++ b/amy/workshops/base_views.py
@@ -266,36 +266,77 @@ def get_form(self, *args, **kwargs):
 
 
 class AutoresponderMixin:
-    """Automatically emails the sender."""
+    """Automatically emails the form sender."""
 
     @property
-    def email_subject(self):
+    def autoresponder_subject(self):
+        """Autoresponder email subject."""
         raise NotImplementedError
 
     @property
-    def email_body_template(self):
+    def autoresponder_body_template_txt(self):
+        """Autoresponder email body template (TXT)."""
         raise NotImplementedError
 
-    def form_valid(self, form):
-        """Send email to form sender if the form is valid."""
+    @property
+    def autoresponder_body_template_html(self):
+        """Autoresponder email body template (HTML)."""
+        raise NotImplementedError
 
-        retval = super().form_valid(form)
+    @property
+    def autoresponder_form_field(self):
+        """Form field's name that contains autoresponder recipient email."""
+        return 'email'
+
+    def autoresponder_email_context(self, form):
+        """Context for """
+        # list of fields allowed to show to the user
+        whitelist = []
+        form_data = [v for k, v in form.cleaned_data.items() if k in whitelist]
+        return dict(form_data=form_data)
+
+    def autoresponder_kwargs(self, form):
+        """Arguments passed to EmailMultiAlternatives."""
+        recipient = (
+            form.cleaned_data.get(self.autoresponder_form_field, None) or ""
+        )
+        return dict(to=[recipient])
 
-        body_template = get_template(self.email_body_template)
-        email_body = body_template.render({})
-        recipient = form.cleaned_data['email']
+    def autoresponder_prepare_email(self, form):
+        """Prepare EmailMultiAlternatives object with message."""
+        # get message subject
+        subject = self.autoresponder_subject
 
-        email = EmailMessage(
-            subject=self.email_subject,
-            body=email_body,
-            to=[recipient],
-        )
+        # get message body templates
+        body_txt_tpl = get_template(self.autoresponder_body_template_txt)
+        body_html_tpl = get_template(self.autoresponder_body_template_html)
+
+        # get message body (both in text and in HTML)
+        context = self.autoresponder_email_context(form)
+        body_txt = body_txt_tpl.render(context)
+        body_html = body_html_tpl.render(context)
+
+        # additional arguments, including recipients
+        kwargs = self.autoresponder_kwargs(form)
+
+        email = EmailMultiAlternatives(subject, body_txt, **kwargs)
+        email.attach_alternative(body_html, 'text/html')
+        return email
+
+    def autoresponder(self, form, fail_silently=True):
+        """Get email from `self.autoresponder_prepare_email`, then send it."""
+        email = self.autoresponder_prepare_email(form)
 
         try:
             email.send()
         except SMTPException as e:
-            pass  # fail silently
+            if not fail_silently:
+                raise e
 
+    def form_valid(self, form):
+        """Send email to form sender if the form is valid."""
+        retval = super().form_valid(form)
+        self.autoresponder(form, fail_silently=True)
         return retval
 
 


Test patch:
diff --git a/amy/extforms/tests/test_training_request_form.py b/amy/extforms/tests/test_training_request_form.py
--- a/amy/extforms/tests/test_training_request_form.py
+++ b/amy/extforms/tests/test_training_request_form.py
@@ -1,6 +1,8 @@
 from django.core import mail
 from django.urls import reverse
 
+from extforms.forms import TrainingRequestForm
+from extforms.views import TrainingRequestCreate
 from workshops.models import Role, TrainingRequest
 from workshops.tests.base import TestBase
 
@@ -59,6 +61,10 @@ def test_request_added(self):
         self.assertEqual(len(mail.outbox), 1)
         msg = mail.outbox[0]
         self.assertEqual(msg.to, [email])
+        self.assertEqual(msg.subject,
+                         TrainingRequestCreate.autoresponder_subject)
+        # with open('email.eml', 'wb') as f:
+        #     f.write(msg.message().as_bytes())
 
 
 class GroupNameFieldTestsBase(TestBase):
diff --git a/amy/extforms/tests/test_workshop_request_form.py b/amy/extforms/tests/test_workshop_request_form.py
--- a/amy/extforms/tests/test_workshop_request_form.py
+++ b/amy/extforms/tests/test_workshop_request_form.py
@@ -1,4 +1,5 @@
 from django.core import mail
+from django.conf import settings
 from django.urls import reverse
 
 from extforms.forms import WorkshopRequestExternalForm
@@ -81,10 +82,31 @@ def test_request_added(self):
         self.assertIn('Thank you for requesting a workshop', content)
         self.assertEqual(WorkshopRequest.objects.all().count(), 1)
         self.assertEqual(WorkshopRequest.objects.all()[0].state, 'p')
-        self.assertEqual(len(mail.outbox), 1)
+
+        # 1 email for autoresponder, 1 email for admins
+        self.assertEqual(len(mail.outbox), 2)
+
+        # save the email messages for test debuggig
+        # with open('email0.eml', 'wb') as f:
+        #     f.write(mail.outbox[0].message().as_bytes())
+        # with open('email1.eml', 'wb') as f:
+        #     f.write(mail.outbox[1].message().as_bytes())
+
+        # before tests, check if the template invalid string exists
+        self.assertTrue(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'])
+
+        # test autoresponder email
         msg = mail.outbox[0]
+        self.assertEqual(msg.subject, 'Workshop request confirmation')
+        self.assertEqual(msg.recipients(), ['hpotter@magic.gov'])
+        self.assertNotIn(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'],
+                         msg.body)
+        # test email for admins
+        msg = mail.outbox[1]
         self.assertEqual(
             msg.subject,
             'New workshop request: Ministry of Magic, 03-04 November, 2018',
         )
         self.assertEqual(msg.recipients(), ['admin-uk@carpentries.org'])
+        self.assertNotIn(settings.TEMPLATES[0]['OPTIONS']['string_if_invalid'],
+                         msg.body)