Repository: e-valuation/EvaP

Issue:
Replace option for participant adding
When copying or importing participants, there should be an option to replace the current participants instead of just adding the new ones.


Discussion:

Resolving patch:
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -16,6 +16,7 @@
 from django.http import HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
+from django.utils.html import format_html
 from django.utils.translation import gettext as _, gettext_lazy
 from django.utils.translation import get_language, ngettext
 from django.views.decorators.http import require_POST
@@ -974,11 +975,24 @@ def evaluation_email(request, semester_id, evaluation_id):
     return render(request, "staff_evaluation_email.html", dict(semester=semester, evaluation=evaluation, form=form))
 
 
+def helper_delete_users_from_evaluation(evaluation, operation):
+    if 'participants' in operation:
+        deleted_person_count = evaluation.participants.count()
+        deletion_message = _("{} participants were deleted from evaluation {}")
+        evaluation.participants.clear()
+    elif 'contributors' in operation:
+        deleted_person_count = evaluation.contributions.exclude(contributor=None).count()
+        deletion_message = _("{} contributors were deleted from evaluation {}")
+        evaluation.contributions.exclude(contributor=None).delete()
+
+    return deleted_person_count, deletion_message
+
 @manager_required
+@transaction.atomic
 def evaluation_person_management(request, semester_id, evaluation_id):
     # This view indeed handles 4 tasks. However, they are tightly coupled, splitting them up
     # would lead to more code duplication. Thus, we decided to leave it as is for now
-    # pylint: disable=too-many-locals
+    # pylint: disable=too-many-locals, too-many-branches
     semester = get_object_or_404(Semester, id=semester_id)
     evaluation = get_object_or_404(Evaluation, id=evaluation_id, course__semester=semester)
     if evaluation.participations_are_archived:
@@ -996,8 +1010,8 @@ def evaluation_person_management(request, semester_id, evaluation_id):
 
     if request.method == "POST":
         operation = request.POST.get('operation')
-        if operation not in ('test-participants', 'import-participants', 'copy-participants',
-                             'test-contributors', 'import-contributors', 'copy-contributors'):
+        if operation not in ('test-participants', 'import-participants', 'copy-participants', 'import-replace-participants', 'copy-replace-participants',
+                             'test-contributors', 'import-contributors', 'copy-contributors', 'import-replace-contributors', 'copy-replace-contributors'):
             raise SuspiciousOperation("Invalid POST operation")
 
         import_type = ImportType.PARTICIPANT if 'participants' in operation else ImportType.CONTRIBUTOR
@@ -1014,21 +1028,28 @@ def evaluation_person_management(request, semester_id, evaluation_id):
                 if not errors:
                     save_import_file(excel_file, request.user.id, import_type)
 
-        elif 'import' in operation:
-            file_content = get_import_file_content_or_raise(request.user.id, import_type)
-            success_messages, warnings, __ = PersonImporter.process_file_content(import_type, evaluation, test_run=False, file_content=file_content)
-            delete_import_file(request.user.id, import_type)
+        else:
+            additional_messages = []
+            if 'replace' in operation:
+                deleted_person_count, deletion_message = helper_delete_users_from_evaluation(evaluation, operation)
+                additional_messages = format_html(deletion_message, deleted_person_count, evaluation.full_name)
+
+            if 'import' in operation:
+                file_content = get_import_file_content_or_raise(request.user.id, import_type)
+                success_messages, warnings, __ = PersonImporter.process_file_content(import_type, evaluation, test_run=False, file_content=file_content)
+                delete_import_file(request.user.id, import_type)
+
+            elif 'copy' in operation:
+                copy_form.evaluation_selection_required = True
+                if copy_form.is_valid():
+                    import_evaluation = copy_form.cleaned_data['evaluation']
+                    success_messages, warnings, errors = PersonImporter.process_source_evaluation(import_type, evaluation, test_run=False, source_evaluation=import_evaluation)
+
+            success_messages.insert(0, additional_messages)
+
             forward_messages(request, success_messages, warnings)
             return redirect('staff:semester_view', semester_id)
 
-        elif 'copy' in operation:
-            copy_form.evaluation_selection_required = True
-            if copy_form.is_valid():
-                import_evaluation = copy_form.cleaned_data['evaluation']
-                success_messages, warnings, errors = PersonImporter.process_source_evaluation(import_type, evaluation, test_run=False, source_evaluation=import_evaluation)
-                forward_messages(request, success_messages, warnings)
-                return redirect('staff:semester_view', semester_id)
-
     participant_test_passed = import_file_exists(request.user.id, ImportType.PARTICIPANT)
     contributor_test_passed = import_file_exists(request.user.id, ImportType.CONTRIBUTOR)
     # casting warnings to a normal dict is necessary for the template to iterate over it.


Test patch:
diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py
--- a/evap/staff/tests/test_views.py
+++ b/evap/staff/tests/test_views.py
@@ -1459,16 +1459,15 @@ def test_evaluation_create(self):
 
 
 class TestEvaluationCopyView(WebTestStaffMode):
-    url = '/staff/semester/1/evaluation/1/copy'
+
 
     @classmethod
     def setUpTestData(cls):
         cls.manager = make_manager()
-        cls.semester = baker.make(Semester, pk=1)
+        cls.semester = baker.make(Semester)
         cls.course = baker.make(Course, semester=cls.semester)
         cls.evaluation = baker.make(
             Evaluation,
-            pk=1,
             course=cls.course,
             name_de="Das Original",
             name_en="The Original",
@@ -1481,6 +1480,7 @@ def setUpTestData(cls):
                 evaluation=cls.evaluation,
                 contributor=baker.make(UserProfile),
             )
+        cls.url = f'/staff/semester/{cls.semester.id}/evaluation/{cls.evaluation.id}/copy'
 
     def test_copy_forms_are_used(self):
         response = self.app.get(self.url, user=self.manager, status=200)
@@ -1686,6 +1686,7 @@ def setUpTestData(cls):
 
 class TestEvaluationImportPersonsView(WebTestStaffMode):
     url = "/staff/semester/1/evaluation/1/person_management"
+    url2 = "/staff/semester/1/evaluation/2/person_management"
     filename_valid = os.path.join(settings.BASE_DIR, "staff/fixtures/valid_user_import.xls")
     filename_invalid = os.path.join(settings.BASE_DIR, "staff/fixtures/invalid_user_import.xls")
     filename_random = os.path.join(settings.BASE_DIR, "staff/fixtures/random.random")
@@ -1694,9 +1695,11 @@ class TestEvaluationImportPersonsView(WebTestStaffMode):
     def setUpTestData(cls):
         semester = baker.make(Semester, pk=1)
         cls.manager = make_manager()
-        cls.evaluation = baker.make(Evaluation, pk=1, course=baker.make(Course, semester=semester))
-        profiles = baker.make(UserProfile, _quantity=42)
-        cls.evaluation2 = baker.make(Evaluation, pk=2, course=baker.make(Course, semester=semester), participants=profiles)
+        profiles1 = baker.make(UserProfile, _quantity=31)
+        cls.evaluation = baker.make(Evaluation, pk=1, course=baker.make(Course, semester=semester), participants=profiles1)
+        profiles2 = baker.make(UserProfile, _quantity=42)
+        cls.evaluation2 = baker.make(Evaluation, pk=2, course=baker.make(Course, semester=semester), participants=profiles2)
+        cls.contribution2 = baker.make(Contribution, evaluation=cls.evaluation2, contributor=baker.make(UserProfile))
 
     @classmethod
     def tearDown(cls):
@@ -1722,6 +1725,22 @@ def test_import_valid_participants_file(self):
         page = self.app.get(self.url, user=self.manager)
         self.assertNotContains(page, 'Import previously uploaded file')
 
+    def test_replace_valid_participants_file(self):
+        page = self.app.get(self.url2, user=self.manager)
+
+        form = page.forms["participant-import-form"]
+        form["excel_file"] = (self.filename_valid,)
+        page = form.submit(name="operation", value="test-participants")
+
+        self.assertNotEqual(self.evaluation2.participants.count(), 2)
+
+        form = page.forms["participant-import-form"]
+        form.submit(name="operation", value="import-replace-participants")
+        self.assertEqual(self.evaluation2.participants.count(), 2)
+
+        page = self.app.get(self.url2, user=self.manager)
+        self.assertNotContains(page, 'Import previously uploaded file')
+
     def test_copy_participants(self):
         page = self.app.get(self.url, user=self.manager)
 
@@ -1733,6 +1752,17 @@ def test_copy_participants(self):
 
         self.assertEqual(self.evaluation.participants.count(), original_participant_count + self.evaluation2.participants.count())
 
+    def test_replace_copy_participants(self):
+        page = self.app.get(self.url, user=self.manager)
+
+        self.assertNotEqual(self.evaluation.participants.count(), self.evaluation2.participants.count())
+
+        form = page.forms["participant-copy-form"]
+        form["evaluation"] = str(self.evaluation2.pk)
+        page = form.submit(name="operation", value="copy-replace-participants")
+
+        self.assertEqual(self.evaluation.participants.count(), self.evaluation2.participants.count())
+
     def test_import_valid_contributors_file(self):
         page = self.app.get(self.url, user=self.manager)
 
@@ -1752,6 +1782,22 @@ def test_import_valid_contributors_file(self):
         page = self.app.get(self.url, user=self.manager)
         self.assertNotContains(page, 'Import previously uploaded file')
 
+    def test_replace_valid_contributors_file(self):
+        page = self.app.get(self.url2, user=self.manager)
+
+        form = page.forms["contributor-import-form"]
+        form["excel_file"] = (self.filename_valid,)
+        page = form.submit(name="operation", value="test-contributors")
+
+        self.assertNotEqual(UserProfile.objects.filter(contributions__evaluation=self.evaluation2).count(), 2)
+
+        form = page.forms["contributor-import-form"]
+        form.submit(name="operation", value="import-replace-contributors")
+        self.assertEqual(UserProfile.objects.filter(contributions__evaluation=self.evaluation2).count(), 2)
+
+        page = self.app.get(self.url, user=self.manager)
+        self.assertNotContains(page, 'Import previously uploaded file')
+
     def test_copy_contributors(self):
         page = self.app.get(self.url, user=self.manager)
 
@@ -1764,6 +1810,19 @@ def test_copy_contributors(self):
         new_contributor_count = UserProfile.objects.filter(contributions__evaluation=self.evaluation).count()
         self.assertEqual(new_contributor_count, original_contributor_count + UserProfile.objects.filter(contributions__evaluation=self.evaluation2).count())
 
+    def test_copy_replace_contributors(self):
+        page = self.app.get(self.url, user=self.manager)
+
+        old_contributor_count = UserProfile.objects.filter(contributions__evaluation=self.evaluation).count()
+        self.assertNotEqual(old_contributor_count, UserProfile.objects.filter(contributions__evaluation=self.evaluation2).count())
+
+        form = page.forms["contributor-copy-form"]
+        form["evaluation"] = str(self.evaluation2.pk)
+        page = form.submit(name="operation", value="copy-replace-contributors")
+
+        new_contributor_count = UserProfile.objects.filter(contributions__evaluation=self.evaluation).count()
+        self.assertEqual(new_contributor_count, UserProfile.objects.filter(contributions__evaluation=self.evaluation2).count())
+
     def test_import_participants_error_handling(self):
         """
         Tests whether errors given from the importer are displayed