Repository: e-valuation/EvaP

Issue:
Duplicate contributor error message
When trying to add the same contributor to a course twice, an error message is shown. This message differs based on whether a questionnaire was assigned or not. The error message should in both cases be the one currently shown when no questionnaire was selected.

**Current messages:**
Questionnaire selected: `Please correct the duplicate data for contributor. Please correct the duplicate values below.`
No questionnaire selected: `Duplicate contributor found. Each contributor should only be used once.`


Discussion:
Identified Cause:

The `Please correct the duplicate data for contributor. Please correct the duplicate values below.` message originates from Django internal unique checks.
The `Duplicate contributor found. Each contributor should only be used once.` message originates from our own checks.

Django internal unique checks only apply to all valid Forms of a FormSet. Hence, any invalid ContributionForm is not included in the unique check. A newly added contributor is invalid (set to contributes to evaluation without any questionnaires).

To resolve this, either we have to change the unique message of django internal unique checks or somehow exclude this unique check.
@Pottiman do you still want to work on this?
Django code line generating the error message:
https://github.com/django/django/blob/ba42569d5c01b58b2999e393f097b530e538ec41/django/forms/models.py#L722

Django's unique validation there is kind of weird. Django does not do unique checks on the formset unless every single form is valid (I guess that's why we added our manual check?). If it does, the error message listed above in the issue is generated by two seperate parts, see my linked code part. One of these parts could be changed by overwriting a django method, the other part would have to be overwritten in a different method, so the message @janno42 listed above are actually two seperate messages.

I'm also confused about one of the errors being raised as a `ValidationError` while the other is just put in `form._errors` as a non field error.

Maybe @karyon knows more about this handling / has done anything with it before?

Resolving patch:
diff --git a/evap/staff/forms.py b/evap/staff/forms.py
--- a/evap/staff/forms.py
+++ b/evap/staff/forms.py
@@ -618,9 +618,6 @@ def handle_moved_contributors(data, **kwargs):
 
     def clean(self):
         self.handle_deleted_and_added_contributions()
-
-        super().clean()
-
         found_contributor = set()
         for form in self.forms:
             if not form.cleaned_data or form.cleaned_data.get('DELETE'):
@@ -629,9 +626,10 @@ def clean(self):
             if contributor is None:
                 raise forms.ValidationError(_('Please select the name of each added contributor. Remove empty rows if necessary.'))
             if contributor and contributor in found_contributor:
-                raise forms.ValidationError(_('Duplicate contributor found. Each contributor should only be used once.'))
+                raise forms.ValidationError(_('Duplicate contributor ({}) found. Each contributor should only be used once.').format(contributor.full_name))
             if contributor:
                 found_contributor.add(contributor)
+        super().clean()
 
 
 class ContributionCopyFormSet(ContributionFormSet):


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
@@ -231,7 +231,7 @@ def test_contribution_form_set(self):
             Tests the ContributionFormset with various input data sets.
         """
         evaluation = baker.make(Evaluation)
-        user1 = baker.make(UserProfile)
+        user1 = baker.make(UserProfile, _fill_optional=['first_name', 'last_name'])
         user2 = baker.make(UserProfile)
         baker.make(UserProfile)
         questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.CONTRIBUTOR)
@@ -257,13 +257,22 @@ def test_contribution_form_set(self):
         data['contributions-TOTAL_FORMS'] = 2
         data['contributions-1-contributor'] = user1.pk
         data['contributions-1-evaluation'] = evaluation.pk
-        data['contributions-1-questionnaires'] = questionnaire.pk
         data['contributions-1-order'] = 1
         data['contributions-1-textanswer_visibility'] = Contribution.TextAnswerVisibility.GENERAL_TEXTANSWERS
-        self.assertFalse(ContributionFormset(instance=evaluation, form_kwargs={'evaluation': evaluation}, data=data).is_valid())
+        data['contributions-1-role'] = Contribution.Role.EDITOR
+        formset = ContributionFormset(instance=evaluation, form_kwargs={'evaluation': evaluation}, data=data)
+        self.assertFalse(formset.is_valid())
+        # regression for https://github.com/e-valuation/EvaP/issues/1082
+        # assert same error message with and without questionnaire
+        self.assertEqual(formset.non_form_errors(), [('Duplicate contributor ({}) found. Each contributor should only be used once.').format(user1.full_name)])
+
+        data['contributions-1-questionnaires'] = questionnaire.pk
+        formset = ContributionFormset(instance=evaluation, form_kwargs={'evaluation': evaluation}, data=data)
+        self.assertFalse(formset.is_valid())
+        self.assertEqual(formset.non_form_errors(), [('Duplicate contributor ({}) found. Each contributor should only be used once.').format(user1.full_name)])
+
         # two contributors
         data['contributions-1-contributor'] = user2.pk
-        data['contributions-1-role'] = Contribution.Role.EDITOR
         self.assertTrue(ContributionFormset(instance=evaluation, form_kwargs={'evaluation': evaluation}, data=data).is_valid())
 
     def test_dont_validate_deleted_contributions(self):