Repository: e-valuation/EvaP

Issue:
Send publish notification to responsible
When sending publish notifications in `send_contributor_publish_notifications` they are currently not sent to the person responsible for the course if the average grade was published (only in the `elif` part the responsible is included). The responsible person should get a notification there as well.


Discussion:

Resolving patch:
diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py
--- a/evap/evaluation/models.py
+++ b/evap/evaluation/models.py
@@ -1599,6 +1599,15 @@ def send_contributor_publish_notifications(cls, evaluations, template=None):
 
         evaluations_per_contributor = defaultdict(set)
         for evaluation in evaluations:
+            # an average grade is published or a general text answer exists
+            relevant_information_published_for_responsibles = (
+                evaluation.can_publish_average_grade
+                or evaluation.textanswer_set.filter(contribution=evaluation.general_contribution).exists()
+            )
+            if relevant_information_published_for_responsibles:
+                for responsible in evaluation.course.responsibles.all():
+                    evaluations_per_contributor[responsible].add(evaluation)
+
             # for evaluations with published averaged grade, all contributors get a notification
             # we don't send a notification if the significance threshold isn't met
             if evaluation.can_publish_average_grade:
@@ -1612,9 +1621,6 @@ def send_contributor_publish_notifications(cls, evaluations, template=None):
                     if textanswer.contribution.contributor:
                         evaluations_per_contributor[textanswer.contribution.contributor].add(evaluation)
 
-                for contributor in evaluation.course.responsibles.all():
-                    evaluations_per_contributor[contributor].add(evaluation)
-
         for contributor, evaluation_set in evaluations_per_contributor.items():
             body_params = {'user': contributor, 'evaluations': list(evaluation_set)}
             template.send_to_user(contributor, {}, body_params, use_cc=True)


Test patch:
diff --git a/evap/evaluation/tests/test_models.py b/evap/evaluation/tests/test_models.py
--- a/evap/evaluation/tests/test_models.py
+++ b/evap/evaluation/tests/test_models.py
@@ -1,5 +1,5 @@
 from datetime import datetime, timedelta, date
-from unittest.mock import patch, Mock
+from unittest.mock import patch, Mock, call
 
 from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
@@ -13,7 +13,7 @@
 from evap.evaluation.models import (Contribution, Course, CourseType, EmailTemplate, Evaluation, NotArchiveable,
                                     Question, Questionnaire, RatingAnswerCounter, Semester, TextAnswer, UserProfile)
 from evap.grades.models import GradeDocument
-from evap.evaluation.tests.tools import let_user_vote_for_evaluation
+from evap.evaluation.tests.tools import let_user_vote_for_evaluation, make_contributor, make_editor
 from evap.results.tools import calculate_average_distribution
 from evap.results.views import get_evaluation_result_template_fragment_cache_key
 
@@ -719,6 +719,58 @@ def test_disable_cc(self):
         self.assertEqual(len(mail.outbox), 1)
         self.assertEqual(set(mail.outbox[0].cc), {self.additional_cc.email})
 
+    @staticmethod
+    def test_send_contributor_publish_notifications():
+        responsible1 = baker.make(UserProfile)
+        responsible2 = baker.make(UserProfile)
+        # use is_single_result to get can_publish_average_grade to become true
+        evaluation1 = baker.make(Evaluation, course__responsibles=[responsible1], is_single_result=True)
+        evaluation2 = baker.make(Evaluation, course__responsibles=[responsible2])
+
+        editor1 = baker.make(UserProfile)
+        contributor1 = baker.make(UserProfile)
+
+        contributor2 = baker.make(UserProfile)
+        editor2 = baker.make(UserProfile)
+        contributor_both = baker.make(UserProfile)
+
+        # Contributions for evaluation1
+        make_contributor(responsible1, evaluation1)
+        make_contributor(contributor1, evaluation1)
+        make_contributor(contributor_both, evaluation1)
+        make_editor(editor1, evaluation1)
+
+        # Contributions for evaluation2
+        make_editor(editor2, evaluation2)
+        contributor_both_contribution = make_contributor(contributor_both, evaluation2)
+        contributor2_contribution = make_contributor(contributor2, evaluation2)
+
+        baker.make(TextAnswer, contribution=contributor_both_contribution)
+        baker.make(TextAnswer, contribution=contributor2_contribution)
+
+        expected_calls = [
+            # these 4 are included since they are contributors for evaluation1 which can publish the average grade
+            call(responsible1, {}, {'user': responsible1, 'evaluations': [evaluation1]}, use_cc=True),
+            call(editor1, {}, {'user': editor1, 'evaluations': [evaluation1]}, use_cc=True),
+            call(contributor1, {}, {'user': contributor1, 'evaluations': [evaluation1]}, use_cc=True),
+            call(contributor_both, {}, {'user': contributor_both, 'evaluations': [evaluation1, evaluation2]}, use_cc=True),
+            # contributor2 has textanswers, so they are notified
+            call(contributor2, {}, {'user': contributor2, 'evaluations': [evaluation2]}, use_cc=True),
+        ]
+
+        with patch('evap.evaluation.models.EmailTemplate.send_to_user') as send_to_user_mock:
+            EmailTemplate.send_contributor_publish_notifications([evaluation1, evaluation2])
+            # Assert that all expected publish notifications are sent to contributors.
+            send_to_user_mock.assert_has_calls(expected_calls, any_order=True)
+
+        # if general textanswers for an evaluation exist, all responsibles should also be notified
+        baker.make(TextAnswer, contribution=evaluation2.general_contribution)
+        expected_calls.append(call(responsible2, {}, {'user': responsible2, 'evaluations': [evaluation2]}, use_cc=True))
+
+        with patch('evap.evaluation.models.EmailTemplate.send_to_user') as send_to_user_mock:
+            EmailTemplate.send_contributor_publish_notifications([evaluation1, evaluation2])
+            send_to_user_mock.assert_has_calls(expected_calls, any_order=True)
+
 
 class TestEmailRecipientList(TestCase):
     def test_recipient_list(self):
diff --git a/evap/evaluation/tests/tools.py b/evap/evaluation/tests/tools.py
--- a/evap/evaluation/tests/tools.py
+++ b/evap/evaluation/tests/tools.py
@@ -101,3 +101,23 @@ def make_manager():
         email='manager@institution.example.com',
         groups=[Group.objects.get(name='Manager')],
     )
+
+
+def make_contributor(user, evaluation):
+    """ Make user a contributor of evaluation. """
+    return baker.make(
+        Contribution,
+        evaluation=evaluation,
+        contributor=user,
+        role=Contribution.Role.CONTRIBUTOR
+    )
+
+
+def make_editor(user, evaluation):
+    """ Make user an editor of evaluation. """
+    return baker.make(
+        Contribution,
+        evaluation=evaluation,
+        contributor=user,
+        role=Contribution.Role.EDITOR,
+    )
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
@@ -1118,21 +1118,21 @@ def test_publish_notifications(self):
         mail.outbox = []
 
         self.helper_publish_evaluation_with_publish_notifications_for(evaluation, contributors=True, participants=False)
-        self.assertEqual(len(mail.outbox), 1)
-        self.assertEqual(mail.outbox[0].to, [contributor1.email])
+        self.assertEqual(len(mail.outbox), 2)
+        self.assertCountEqual([[contributor1.email], [self.responsible.email]], [mail.outbox[0].to, mail.outbox[1].to])
         mail.outbox = []
 
         self.helper_publish_evaluation_with_publish_notifications_for(evaluation, contributors=False, participants=True)
         self.assertEqual(len(mail.outbox), 2)
-        self.assertIn([participant1.email], [mail.outbox[0].to, mail.outbox[1].to])
-        self.assertIn([participant2.email], [mail.outbox[0].to, mail.outbox[1].to])
+        self.assertCountEqual([[participant1.email], [participant2.email]], [mail.outbox[0].to, mail.outbox[1].to])
         mail.outbox = []
 
         self.helper_publish_evaluation_with_publish_notifications_for(evaluation, contributors=True, participants=True)
-        self.assertEqual(len(mail.outbox), 3)
-        self.assertIn([participant1.email], [mail.outbox[0].to, mail.outbox[1].to, mail.outbox[2].to])
-        self.assertIn([participant2.email], [mail.outbox[0].to, mail.outbox[1].to, mail.outbox[2].to])
-        self.assertIn([contributor1.email], [mail.outbox[0].to, mail.outbox[1].to, mail.outbox[2].to])
+        self.assertEqual(len(mail.outbox), 4)
+        self.assertCountEqual(
+            [[participant1.email], [participant2.email], [contributor1.email], [self.responsible.email]],
+            [outbox_entry.to for outbox_entry in mail.outbox]
+        )
         mail.outbox = []
 
     def helper_semester_state_views(self, evaluation, old_state, new_state):
@@ -1153,11 +1153,20 @@ def helper_semester_state_views(self, evaluation, old_state, new_state):
     def test_semester_publish(self):
         participant1 = baker.make(UserProfile, email="foo@example.com")
         participant2 = baker.make(UserProfile, email="bar@example.com")
-        evaluation = baker.make(Evaluation, course=self.course, state='reviewed',
-                                participants=[participant1, participant2], voters=[participant1, participant2])
+        evaluation = baker.make(
+            Evaluation,
+            course=self.course,
+            state='reviewed',
+            participants=[participant1, participant2],
+            voters=[participant1, participant2]
+        )
 
         self.helper_semester_state_views(evaluation, "reviewed", "published")
-        self.assertEqual(len(mail.outbox), 2)
+        self.assertEqual(len(mail.outbox), 3)
+        self.assertCountEqual(
+            [[participant1.email], [participant2.email], [self.responsible.email]],
+            [outbox_entry.to for outbox_entry in mail.outbox],
+        )
 
     def test_semester_reset_1(self):
         evaluation = baker.make(Evaluation, course=self.course, state='prepared')