Repository: carpentries/amy

Issue:
Indicate successful operation on forms
Right now (2504e4aee6c, 2015-06-04) when we give someone a badge, the form is redisplayed with all the fields still filled in and no indication that the badge has been issued - so if you blink, you're not aware the form has been submitted, and you're all too likely to click 'submit' again.  We should indicate successful submission somehow (an acknowledgment message on the form).  This should also be done on other forms.



Discussion:

Resolving patch:
diff --git a/workshops/forms.py b/workshops/forms.py
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -32,11 +32,14 @@ def __init__(self, form=None):
         self.inputs.append(Submit('submit', 'Submit'))
 
 
-class BootstrapHelperWithoutForm(BootstrapHelper):
-    form_tag = False
+class BootstrapHelperWithAdd(BootstrapHelper):
+    def __init__(self, form=None):
+        super().__init__(form)
+
+        self.inputs[-1] = Submit('submit', 'Add')
 
 bootstrap_helper = BootstrapHelper()
-bootstrap_helper_without_form = BootstrapHelperWithoutForm()
+bootstrap_helper_with_add = BootstrapHelperWithAdd()
 
 
 class InstructorsForm(forms.Form):
@@ -200,15 +203,10 @@ class TaskForm(forms.ModelForm):
         widget=selectable.AutoComboboxSelectWidget,
     )
 
-    def __init__(self, *args, **kwargs):
-        event = kwargs.pop('event', None)
-        super().__init__(*args, **kwargs)
-        if event:
-            self.instance.event = event
-
     class Meta:
         model = Task
-        exclude = ('event', 'deleted')
+        exclude = ('deleted', )
+        widgets = {'event': HiddenInput}
 
 
 class TaskFullForm(TaskForm):
diff --git a/workshops/views.py b/workshops/views.py
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -6,6 +6,7 @@
 import requests
 
 from django.contrib import messages
+from django.contrib.messages.views import SuccessMessageMixin
 from django.contrib.auth import update_session_auth_hash
 from django.contrib.auth.forms import SetPasswordForm, PasswordChangeForm
 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
@@ -35,7 +36,7 @@
 from workshops.forms import (
     SearchForm, DebriefForm, InstructorsForm, PersonForm, PersonBulkAddForm,
     EventForm, TaskForm, TaskFullForm, bootstrap_helper,
-    bootstrap_helper_without_form, BadgeAwardForm, PersonAwardForm,
+    bootstrap_helper_with_add, BadgeAwardForm, PersonAwardForm,
     PersonPermissionsForm
 )
 from workshops.util import (
@@ -50,11 +51,12 @@
 #------------------------------------------------------------
 
 
-class CreateViewContext(CreateView):
+class CreateViewContext(SuccessMessageMixin, CreateView):
     """
     Class-based view for creating objects that extends default template context
     by adding model class used in objects creation.
     """
+    success_message = '{name} was created successfully.'
 
     def get_context_data(self, **kwargs):
         context = super(CreateViewContext, self).get_context_data(**kwargs)
@@ -71,12 +73,17 @@ def get_context_data(self, **kwargs):
         context['form_helper'] = bootstrap_helper
         return context
 
+    def get_success_message(self, cleaned_data):
+        "Format self.success_message, used by messages framework from Django."
+        return self.success_message.format(cleaned_data, name=str(self.object))
 
-class UpdateViewContext(UpdateView):
+
+class UpdateViewContext(SuccessMessageMixin, UpdateView):
     """
     Class-based view for updating objects that extends default template context
     by adding proper page title.
     """
+    success_message = '{name} was updated successfully.'
 
     def get_context_data(self, **kwargs):
         context = super(UpdateViewContext, self).get_context_data(**kwargs)
@@ -94,6 +101,11 @@ def get_context_data(self, **kwargs):
         context['form_helper'] = bootstrap_helper
         return context
 
+    def get_success_message(self, cleaned_data):
+        "Format self.success_message, used by messages framework from Django."
+        return self.success_message.format(cleaned_data, name=str(self.object))
+
+
 
 class LoginRequiredMixin(object):
     """
@@ -207,13 +219,6 @@ class AirportUpdate(LoginRequiredMixin, UpdateViewContext):
 #------------------------------------------------------------
 
 
-PERSON_FIELDS = [
-        field.name for field in Person._meta.fields
-    ] + [
-        'user_permissions',
-    ]
-
-
 @login_required
 def all_persons(request):
     '''List all persons.'''
@@ -426,18 +431,40 @@ def person_edit(request, person_id):
             award_form = PersonAwardForm(request.POST, prefix='award')
 
             if award_form.is_valid():
-                award_form.save()
+                award = award_form.save()
+
+                messages.success(
+                    request,
+                    '{person} was awarded {badge} badge.'.format(
+                        person=str(person),
+                        badge=award.badge.title,
+                    ),
+                )
 
                 # to reset the form values
                 return redirect(request.path)
 
+            else:
+                messages.error(request, 'Fix errors below.')
+
         else:
             person_form = PersonForm(request.POST, prefix='person',
                                      instance=person)
             if person_form.is_valid():
-                person_form.save()
+                person = person_form.save()
+
+                messages.success(
+                    request,
+                    '{name} was updated successfully.'.format(
+                        name=str(person),
+                    ),
+                )
+
                 return redirect(person)
 
+            else:
+                messages.error(request, 'Fix errors below.')
+
     # two separate forms on one page
     context = {'title': 'Edit Person {0}'.format(str(person)),
                'person_form': person_form,
@@ -445,7 +472,9 @@ def person_edit(request, person_id):
                'model': Person,
                'awards': awards,
                'award_form': award_form,
-               'form_helper': bootstrap_helper}
+               'form_helper': bootstrap_helper,
+               'form_helper_with_add': bootstrap_helper_with_add,
+               }
     return render(request, 'workshops/person_edit_form.html', context)
 
 
@@ -474,7 +503,12 @@ def person_password(request, person_id):
 
             update_session_auth_hash(request, form.user)
 
+            messages.success(request, 'Password was changed successfully.')
+
             return redirect(reverse('person_details', args=[user.id]))
+
+        else:
+            messages.error(request, 'Fix errors below.')
     else:
         form = Form(user)
 
@@ -553,20 +587,51 @@ def event_edit(request, event_ident):
     except ObjectDoesNotExist:
         raise Http404("No event found matching the query.")
 
-    if request.method == 'GET':
-        event_form = EventForm(prefix='event', instance=event)
-        task_form = TaskForm(prefix='task', event=event)
+    event_form = EventForm(prefix='event', instance=event)
+    task_form = TaskForm(prefix='task', initial={
+        'event': event,
+    })
 
-    elif request.method == 'POST':
-        event_form = EventForm(request.POST, prefix='event', instance=event)
-        task_form = TaskForm(request.POST, prefix='task', event=event)
+    if request.method == 'POST':
+        # check which form was submitted
+        if "task-role" in request.POST:
+            task_form = TaskForm(request.POST, prefix='task')
+
+            if task_form.is_valid():
+                task = task_form.save()
 
-        if "submit" in request.POST and event_form.is_valid():
-            event_form.save()
-            return redirect(event)
+                messages.success(
+                    request,
+                    '{event} was added a new task "{task}".'.format(
+                        event=str(event),
+                        task=str(task),
+                    ),
+                )
+
+                # to reset the form values
+                return redirect(request.path)
+
+            else:
+                messages.error(request, 'Fix errors below.')
+
+        else:
+            event_form = EventForm(request.POST, prefix='event',
+                                   instance=event)
+            if event_form.is_valid():
+                event = event_form.save()
+
+                messages.success(
+                    request,
+                    '{name} was updated successfully.'.format(
+                        name=str(event),
+                    ),
+                )
+
+                return redirect(event)
+
+            else:
+                messages.error(request, 'Fix errors below.')
 
-        if "add" in request.POST and task_form.is_valid():
-            task_form.save()
 
     context = {'title': 'Edit Event {0}'.format(event.get_ident()),
                'event_form': event_form,
@@ -574,7 +639,9 @@ def event_edit(request, event_ident):
                'model': Event,
                'tasks': tasks,
                'task_form': task_form,
-               'form_helper': bootstrap_helper_without_form}
+               'form_helper': bootstrap_helper,
+               'form_helper_with_add': bootstrap_helper_with_add,
+               }
     return render(request, 'workshops/event_edit_form.html', context)
 
 
@@ -592,6 +659,7 @@ def event_delete(request, event_ident):
     tasks.update(deleted=True)
     event.deleted = True
     event.save()
+    messages.success(request, 'Event was deleted successfully.')
     return redirect(reverse('all_events'))
 
 #------------------------------------------------------------
@@ -624,6 +692,7 @@ def task_delete(request, task_id):
     t = Task.objects.get(pk=task_id)
     t.deleted = True
     t.save()
+    messages.success(request, 'Task was deleted successfully.')
     return redirect(event_edit, t.event.id)
 
 


Test patch:
diff --git a/workshops/test/test_event.py b/workshops/test/test_event.py
--- a/workshops/test/test_event.py
+++ b/workshops/test/test_event.py
@@ -150,7 +150,7 @@ def test_edit_event(self):
         event = Event.objects.all()[0]
         tag = Tag.objects.get(name='Test Tag')
         role = Role.objects.get(name='Test Role')
-        url, values = self._get_initial_form('event_edit', event.id)
+        url, values = self._get_initial_form_index(0, 'event_edit', event.id)
         assert len(values) > 0
         values['event-end'] = ''
         values['event-tags'] = tag.id
@@ -160,6 +160,11 @@ def test_edit_event(self):
         assert event.reg_key != new_reg_key, \
             'Would be unable to tell if reg_key had changed'
         values['event-reg_key'] = new_reg_key
+        response = self.client.post(url, values, follow=True)
+        content = response.content.decode('utf-8')
+        assert new_reg_key in content
+
+        url, values = self._get_initial_form_index(1, 'event_edit', event.id)
         assert "task-person_0" in values, \
             'No person select in initial form'
 
@@ -167,14 +172,8 @@ def test_edit_event(self):
         values['task-person_1'] = person.id
         values['task-role'] = role.id
 
-        # Add superuser as a test role
-        values['add'] = 'yes'
-
-        response = self.client.post(url, values, follow=True) # Submit, following redirect
-        _, params = cgi.parse_header(response['content-type'])
-        charset = params['charset']
-        content = response.content.decode(charset)
-        assert new_reg_key in content
+        response = self.client.post(url, values, follow=True)
+        content = response.content.decode('utf-8')
         assert "/workshops/person/1" in content
         assert "Test Role" in content