Repository: e-valuation/EvaP

Issue:
Prevent deleting contributors voted for in the form
Staff users can delete contributors in the evaluation form even if answers to questions of their contribution have already been given. However, this correctly results in a `ProtectedError` when saving the form:
```
Cannot delete some instances of model 'Contribution' because they are referenced through a protected foreign key
```
Deleting these contributors should not be possible in the form.


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
@@ -517,6 +517,16 @@ def clean(self):
         if not self.cleaned_data.get("does_not_contribute") and not self.cleaned_data.get("questionnaires"):
             self.add_error("does_not_contribute", _("Select either this option or at least one questionnaire!"))
 
+    @property
+    def show_delete_button(self):
+        if self.instance.pk is None:
+            return True  # not stored in the DB. Required so temporary instances in the formset can be deleted.
+
+        if not self.instance.ratinganswercounter_set.exists() and not self.instance.textanswer_set.exists():
+            return True
+
+        return False
+
 
 class ContributionCopyForm(ContributionForm):
     def __init__(self, data=None, instance=None, evaluation=None, **kwargs):


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
@@ -6,6 +6,7 @@
 
 from evap.contributor.forms import EvaluationForm as ContributorEvaluationForm
 from evap.evaluation.models import (
+    Answer,
     Contribution,
     Course,
     Degree,
@@ -13,7 +14,9 @@
     Evaluation,
     Question,
     Questionnaire,
+    RatingAnswerCounter,
     Semester,
+    TextAnswer,
     UserProfile,
 )
 from evap.evaluation.tests.tools import (
@@ -554,6 +557,36 @@ def test_staff_can_select_proxy_user(self):
         form = CourseForm(instance=course)
         self.assertIn(proxy_user, form.fields["responsibles"].queryset)
 
+    def test_prevent_contribution_deletion_with_answers(self):
+        """
+        When answers for a contribution already exist, it should not be possible to remove that contribution.
+        """
+        self.assertEqual(
+            set(Answer.__subclasses__()),
+            {RatingAnswerCounter, TextAnswer},
+            "This requires an update if a new answer type is added",
+        )
+        evaluation = baker.make(Evaluation)
+        contribution = baker.make(
+            Contribution,
+            evaluation=evaluation,
+            role=Contribution.Role.EDITOR,
+            textanswer_visibility=Contribution.TextAnswerVisibility.GENERAL_TEXTANSWERS,
+            _fill_optional=["contributor"],
+        )
+
+        contribution_formset = inlineformset_factory(
+            Evaluation, Contribution, formset=ContributionFormSet, form=ContributionForm, extra=1
+        )
+        formset = contribution_formset(instance=evaluation, form_kwargs={"evaluation": evaluation})
+        self.assertTrue(formset.forms[0].show_delete_button)
+        self.assertTrue(formset.forms[1].show_delete_button)
+
+        baker.make(RatingAnswerCounter, contribution=contribution)
+
+        self.assertFalse(formset.forms[0].show_delete_button)
+        self.assertTrue(formset.forms[1].show_delete_button)
+
 
 class ContributionFormset775RegressionTests(TestCase):
     """