Repository: carpentries/amy

Issue:
Email senders of training request form
Thank you for your application for Software Carpentry/Data Carpentry instructor training.  Due to high demand, the waiting time for open instructor training events is currently 8-12 months as we try to clear our backlog.  If you are affiliated with one of our institutional partners (listed at http://software-carpentry.org/scf/partners), we will do our best to prioritize your application; if you or your institution would like to discuss partnership, we would welcome an opportunity to speak with you.
Email senders of training request form
Thank you for your application for Software Carpentry/Data Carpentry instructor training.  Due to high demand, the waiting time for open instructor training events is currently 8-12 months as we try to clear our backlog.  If you are affiliated with one of our institutional partners (listed at http://software-carpentry.org/scf/partners), we will do our best to prioritize your application; if you or your institution would like to discuss partnership, we would welcome an opportunity to speak with you.


Discussion:
Broader than this - I'd like an autoresponder to any form (instructor application, workshop request, etc.)
Broader than this - I'd like an autoresponder to any form (instructor application, workshop request, etc.)

Resolving patch:
diff --git a/extforms/urls.py b/extforms/urls.py
--- a/extforms/urls.py
+++ b/extforms/urls.py
@@ -13,5 +13,6 @@
     # disabled as per @maneesha's request
     # url(r'^submit/confirm/$', views.EventSubmissionConfirm.as_view(), name='event_submission_confirm'),
     url(r'^update_profile/$', views.profileupdaterequest_create, name='profileupdate_request'),
-    url(r'^request_training/$', views.trainingrequest_create, name='training_request'),
+    url(r'^request_training/$', views.TrainingRequestCreate.as_view(), name='training_request'),
+    url(r'^request_training/confirm/$', views.TrainingRequestConfirm.as_view(), name='training_request_confirm'),
 ]
diff --git a/extforms/views.py b/extforms/views.py
--- a/extforms/views.py
+++ b/extforms/views.py
@@ -17,6 +17,7 @@
     EventRequest,
     EventSubmission as EventSubmissionModel,
     DCSelfOrganizedEventRequest as DCSelfOrganizedEventRequestModel,
+    TrainingRequest,
 )
 from workshops.util import (
     login_not_required,
@@ -25,6 +26,7 @@
 from workshops.base_views import (
     AMYCreateView,
     EmailSendMixin,
+    AutoresponderMixin,
 )
 
 
@@ -254,35 +256,33 @@ def get_context_data(self, **kwargs):
         return context
 
 
-@login_not_required
-def trainingrequest_create(request):
-    """A form to let all users (no login required) to request Instructor
-    Training."""
+class TrainingRequestCreate(
+    LoginNotRequiredMixin,
+    AutoresponderMixin,
+    AMYCreateView,
+):
+    model = TrainingRequest
+    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'
 
-    group_name = request.GET.get('group', None) or None  # replace empty string with None
-    form = TrainingRequestForm(initial_group_name=group_name)
+    def get_success_message(self, *args, **kwargs):
+        """Don't display a success message."""
+        return ''
 
-    page_title = 'Apply for Instructor Training'
+    def get_form_kwargs(self):
+        kwargs = super().get_form_kwargs()
+        group_name = self.request.GET.get('group', None) or None  # replace empty string with None
+        kwargs['initial_group_name'] = group_name
+        return kwargs
 
-    if request.method == 'POST':
-        form = TrainingRequestForm(request.POST, initial_group_name=group_name)
 
-        if form.is_valid():
-            form.save()
-
-            # TODO: email notification?
+class TrainingRequestConfirm(LoginNotRequiredMixin, TemplateView):
+    template_name = 'forms/trainingrequest_confirm.html'
 
-            context = {
-                'title': 'Thank you for applying for an instructor training.',
-            }
-            return render(request,
-                          'forms/trainingrequest_confirm.html',
-                          context)
-        else:
-            messages.error(request, 'Fix errors below.')
-
-    context = {
-        'title': page_title,
-        'form': form,
-    }
-    return render(request, 'forms/trainingrequest.html', context)
+    def get_context_data(self, **kwargs):
+        context = super().get_context_data(**kwargs)
+        context['title'] = 'Thank you for applying for an instructor training'
+        return context
diff --git a/workshops/base_views.py b/workshops/base_views.py
--- a/workshops/base_views.py
+++ b/workshops/base_views.py
@@ -1,7 +1,9 @@
+from smtplib import SMTPException
+
 from django.contrib import messages
 from django.contrib.messages.views import SuccessMessageMixin
 from django.core.exceptions import ImproperlyConfigured
-from django.core.mail import EmailMultiAlternatives
+from django.core.mail import EmailMultiAlternatives, EmailMessage
 from django.db.models import Model, ProtectedError
 from django.http import HttpResponseRedirect
 from django.utils.http import is_safe_url
@@ -13,6 +15,7 @@
     ListView,
     DetailView,
 )
+from django.template.loader import get_template
 
 from workshops.forms import BootstrapHelper
 from workshops.util import failed_to_delete, Paginator, get_pagination_items
@@ -219,3 +222,37 @@ def get_form(self, *args, **kwargs):
             if field in self.request.GET:
                 form.fields[field].disabled = True
         return form
+
+
+class AutoresponderMixin:
+    """Automatically emails the sender."""
+
+    @property
+    def email_subject(self):
+        raise NotImplementedError
+
+    @property
+    def email_body_template(self):
+        raise NotImplementedError
+
+    def form_valid(self, form):
+        """Send email to form sender if the form is valid."""
+
+        retval = super().form_valid(form)
+
+        body_template = get_template(self.email_body_template)
+        email_body = body_template.render({})
+        recipient = form.cleaned_data['email']
+
+        email = EmailMessage(
+            subject=self.email_subject, 
+            body=email_body,
+            to=[recipient],
+        )
+
+        try:
+            email.send()
+        except SMTPException as e:
+            pass  # fail silently
+
+        return retval


Test patch:
diff --git a/extforms/test/test_training_request_form.py b/extforms/test/test_training_request_form.py
--- a/extforms/test/test_training_request_form.py
+++ b/extforms/test/test_training_request_form.py
@@ -82,7 +82,7 @@ def fillin_and_submit(self):
         self.form['recaptcha_response_field'] = 'PASSED'
 
         # submit the form
-        self.rs = self.form.submit()
+        self.rs = self.form.submit().maybe_follow()
 
     def assertSubmissionIsRecorded(self, with_group_name):
         # the form should be successfully submitted
diff --git a/workshops/test/test_security.py b/workshops/test/test_security.py
--- a/workshops/test/test_security.py
+++ b/workshops/test/test_security.py
@@ -143,7 +143,7 @@ def test_function_based_view_accessible_to_unauthorized_users(self):
         Test that a view decorated with @login_not_required is accessible to
         everyone.
         """
-        view_name = 'training_request'
+        view_name = 'profileupdate_request'
         view = get_view_by_name(view_name)
         assert view._access_control_list == [login_not_required]
         url = reverse(view_name)
diff --git a/workshops/test/test_training_request.py b/workshops/test/test_training_request.py
--- a/workshops/test/test_training_request.py
+++ b/workshops/test/test_training_request.py
@@ -1,3 +1,4 @@
+from django.core import mail
 from django.core.exceptions import ValidationError
 from django.core.urlresolvers import reverse
 from django.template import Context
@@ -15,6 +16,63 @@
 )
 
 
+class TestTrainingRequestForm(TestBase):
+    def setUp(self):
+        self._setUpUsersAndLogin()
+        self._setUpRoles()
+
+    def test_request_added(self):
+        email = 'john@smith.com'
+        data = {
+            'group': 'coolprogrammers',
+            'personal': 'John',
+            'family': 'Smith',
+            'email': email,
+            'github': '',
+            'occupation': '',
+            'occupation_other': 'unemployed',
+            'affiliation': 'AGH University of Science and Technology',
+            'location': 'Cracow',
+            'country': 'PL',
+            'domains': [1, 2],
+            'domains_other': '',
+            'gender': Person.MALE,
+            'gender_other': '',
+            'previous_involvement': [Role.objects.get(name='host').id],
+            'previous_training': 'none',
+            'previous_training_other': '',
+            'previous_training_explanation': '',
+            'previous_experience': 'none',
+            'previous_experience_other': '',
+            'previous_experience_explanation': '',
+            'programming_language_usage_frequency': 'daily',
+            'reason': 'Just for fun.',
+            'teaching_frequency_expectation': 'often',
+            'teaching_frequency_expectation_other': '',
+            'max_travelling_frequency': 'yearly',
+            'max_travelling_frequency_other': '',
+            'addition_skills': '',
+            'comment': '',
+            'agreed_to_code_of_conduct': 'on',
+            'agreed_to_complete_training': 'on',
+            'agreed_to_teach_workshops': 'on',
+            'privacy_consent': True,
+            'recaptcha_response_field': 'PASSED',
+        }
+        rv = self.client.post(reverse('training_request'), data,
+                              follow=True)
+        self.assertEqual(rv.status_code, 200)
+        content = rv.content.decode('utf-8')
+        self._save_html(content)
+        self.assertNotIn('Fix errors below', content)
+        self.assertEqual(TrainingRequest.objects.all().count(), 1)
+
+        # Test that the sender was emailed
+        self.assertEqual(len(mail.outbox), 1)
+        msg = mail.outbox[0]
+        self.assertEqual(msg.to, [email])
+
+
 def create_training_request(state, person):
     return TrainingRequest.objects.create(
         personal='John',