Repository: e-valuation/EvaP

Issue:
Removing empty contributor field throws KeyError
When removing the empty contributor field at the end of a course form (as an editor or staff member) and saving the course, a `KeyError` is thrown.


Discussion:

Resolving patch:
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -422,8 +422,10 @@ def handle_deleted_and_added_contributions(self):
         for form_with_errors in self.forms:
             if not form_with_errors.errors:
                 continue
+            if 'contributor' not in form_with_errors.cleaned_data:
+                continue
             for deleted_form in self.forms:
-                if not deleted_form.cleaned_data or not deleted_form.cleaned_data.get('DELETE'):
+                if not deleted_form.cleaned_data or 'contributor' not in deleted_form.cleaned_data or not deleted_form.cleaned_data.get('DELETE'):
                     continue
                 if not deleted_form.cleaned_data['contributor'] == form_with_errors.cleaned_data['contributor']:
                     continue


Test patch:
diff --git a/evap/staff/tests/test_forms.py b/evap/staff/tests/test_forms.py
--- a/evap/staff/tests/test_forms.py
+++ b/evap/staff/tests/test_forms.py
@@ -230,6 +230,48 @@ def test_dont_validate_deleted_contributions(self):
         formset = contribution_formset(instance=course, form_kwargs={'course': course}, data=data)
         self.assertTrue(formset.is_valid())
 
+    def test_deleted_empty_contribution_does_not_crash(self):
+        """
+            When removing the empty extra contribution formset, validating the form should not crash.
+            Similarly, when removing the contribution formset of an existing contributor, and entering some data in the extra formset, it should not crash.
+            Regression test for #1057
+        """
+        course = mommy.make(Course)
+        user1 = mommy.make(UserProfile)
+        questionnaire = mommy.make(Questionnaire, is_for_contributors=True)
+
+        contribution_formset = inlineformset_factory(Course, Contribution, formset=ContributionFormSet, form=ContributionForm, extra=0)
+
+        data = to_querydict({
+            'contributions-TOTAL_FORMS': 2,
+            'contributions-INITIAL_FORMS': 0,
+            'contributions-MAX_NUM_FORMS': 5,
+            'contributions-0-course': course.pk,
+            'contributions-0-questionnaires': questionnaire.pk,
+            'contributions-0-order': 0,
+            'contributions-0-responsibility': "RESPONSIBLE",
+            'contributions-0-comment_visibility': "ALL",
+            'contributions-0-contributor': user1.pk,
+            'contributions-1-course': course.pk,
+            'contributions-1-questionnaires': "",
+            'contributions-1-order': -1,
+            'contributions-1-responsibility': "CONTRIBUTOR",
+            'contributions-1-comment_visibility': "OWN",
+            'contributions-1-contributor': "",
+        })
+
+        # delete extra formset
+        data['contributions-1-DELETE'] = 'on'
+        formset = contribution_formset(instance=course, form_kwargs={'course': course}, data=data)
+        formset.is_valid()
+        data['contributions-1-DELETE'] = ''
+
+        # delete first, change data in extra formset
+        data['contributions-0-DELETE'] = 'on'
+        data['contributions-1-responsibility'] = 'RESPONSIBLE'
+        formset = contribution_formset(instance=course, form_kwargs={'course': course}, data=data)
+        formset.is_valid()
+
     def test_take_deleted_contributions_into_account(self):
         """
             Tests whether contributions marked for deletion are properly taken into account
@@ -364,7 +406,7 @@ def test_extra_form(self):
         # make sure nothing crashes when an extra form is present.
         self.data['contributions-0-contributor'] = self.user2.pk
         self.data['contributions-1-contributor'] = self.user1.pk
-        self.data['contributions-2-TOTAL_FORMS'] = 3
+        self.data['contributions-TOTAL_FORMS'] = 3
         self.data['contributions-2-id'] = ""
         self.data['contributions-2-order'] = -1
         self.data['contributions-2-responsibility'] = "CONTRIBUTOR"