Repository: e-valuation/EvaP

Issue:
Wrong reward point message when removing participants
`grant_reward_points_after_delete` checks for `if grant_reward_points_if_eligible` when collecting user names that should be displayed in a message. This method returns tuples since 2498f15693cd1fabe71e472f57fb57fa5c51fdd8, so the check is always true and all removed participants are shown in the message.

The actual granting is checked correctly, but the displayed message is not necessarily correct.


Discussion:

Resolving patch:
diff --git a/evap/rewards/tools.py b/evap/rewards/tools.py
--- a/evap/rewards/tools.py
+++ b/evap/rewards/tools.py
@@ -64,13 +64,13 @@ def is_semester_activated(semester):
 
 def grant_reward_points_if_eligible(user, semester):
     if not can_user_use_reward_points(user):
-        return 0, False
+        return None, False
     if not is_semester_activated(semester):
-        return 0, False
+        return None, False
     # does the user have at least one required course in this semester?
     required_courses = Course.objects.filter(participants=user, semester=semester, is_rewarded=True)
     if not required_courses.exists():
-        return 0, False
+        return None, False
 
     # How many points have been granted to this user vs how many should they have (this semester)
     granted_points = RewardPointGranting.objects.filter(user_profile=user, semester=semester).aggregate(Sum('value'))['value__sum'] or 0
@@ -79,19 +79,19 @@ def grant_reward_points_if_eligible(user, semester):
     missing_points = target_points - granted_points
 
     if missing_points > 0:
-        RewardPointGranting.objects.create(user_profile=user, semester=semester, value=missing_points)
-        return missing_points, progress >= 1.0
-    return 0, False
+        granting = RewardPointGranting.objects.create(user_profile=user, semester=semester, value=missing_points)
+        return granting, progress >= 1.0
+    return None, False
 
 
 # Signal handlers
 
 @receiver(Course.course_evaluated)
 def grant_reward_points_after_evaluate(request, semester, **_kwargs):
-    points_granted, completed_evaluation = grant_reward_points_if_eligible(request.user, semester)
-    if points_granted:
+    granting, completed_evaluation = grant_reward_points_if_eligible(request.user, semester)
+    if granting:
         message = ngettext("You just earned {count} reward point for this semester.",
-                           "You just earned {count} reward points for this semester.", points_granted).format(count=points_granted)
+                           "You just earned {count} reward points for this semester.", granting.value).format(count=granting.value)
 
         if completed_evaluation:
             message += " " + _("Thank you very much for evaluating all your courses.")
@@ -106,22 +106,24 @@ def grant_reward_points_after_evaluate(request, semester, **_kwargs):
 def grant_reward_points_after_delete(instance, action, reverse, pk_set, **_kwargs):
     # if users do not need to evaluate a course anymore, they may have earned reward points
     if action == 'post_remove':
-        affected = []
+        grantings = []
 
         if reverse:
             # a course got removed from a participant
             user = instance
 
             for semester in Semester.objects.filter(course__pk__in=pk_set):
-                if grant_reward_points_if_eligible(user, semester):
-                    affected = [user]
+                granting, __ = grant_reward_points_if_eligible(user, semester)
+                if granting:
+                    grantings = [granting]
         else:
             # a participant got removed from a course
             course = instance
 
             for user in UserProfile.objects.filter(pk__in=pk_set):
-                if grant_reward_points_if_eligible(user, course.semester):
-                    affected.append(user)
+                granting, __ = grant_reward_points_if_eligible(user, course.semester)
+                if granting:
+                    grantings.append(granting)
 
-        if affected:
-            RewardPointGranting.granted_by_removal.send(sender=RewardPointGranting, users=affected)
+        if grantings:
+            RewardPointGranting.granted_by_removal.send(sender=RewardPointGranting, grantings=grantings)
diff --git a/evap/settings.py b/evap/settings.py
--- a/evap/settings.py
+++ b/evap/settings.py
@@ -45,9 +45,9 @@
 
 # number of reward points a student should have for a semester after evaluating the given fraction of courses.
 REWARD_POINTS = [
-    (1.0/3.0, 1),
-    (2.0/3.0, 2),
-    (3.0/3.0, 3),
+    (1 / 3, 1),
+    (2 / 3, 2),
+    (3 / 3, 3),
 ]
 
 # days before end date to send reminder
diff --git a/evap/staff/views.py b/evap/staff/views.py
--- a/evap/staff/views.py
+++ b/evap/staff/views.py
@@ -16,7 +16,7 @@
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.utils.translation import ugettext as _
-from django.utils.translation import get_language, ungettext
+from django.utils.translation import get_language, ungettext, ngettext
 from django.views.decorators.http import require_POST
 from evap.evaluation.auth import reviewer_required, staff_required
 from evap.evaluation.models import (Contribution, Course, CourseType, Degree, EmailTemplate, FaqQuestion, FaqSection, Question, Questionnaire,
@@ -593,11 +593,15 @@ def course_edit(request, semester_id, course_id):
 @staff_required
 def helper_course_edit(request, semester, course):
     @receiver(RewardPointGranting.granted_by_removal)
-    def notify_reward_points(users, **_kwargs):
-        names = ", ".join('"{}"'.format(user.username) for user in users)
-        messages.info(request, ungettext("The removal of participants has granted {affected} user reward points: {names}.",
-            "The removal of participants has granted {affected} users reward points: {names}.",
-            len(users)).format(affected=len(users), names=names))
+    def notify_reward_points(grantings, **_kwargs):
+        for granting in grantings:
+            messages.info(request,
+                ngettext(
+                    'The removal as participant has granted the user "{granting.user_profile.username}" {granting.value} reward point for the active semester.',
+                    'The removal as participant has granted the user "{granting.user_profile.username}" {granting.value} reward points for the active semester.',
+                    granting.value
+                ).format(granting=granting)
+            )
 
     InlineContributionFormset = inlineformset_factory(Course, Contribution, formset=ContributionFormSet, form=ContributionForm, extra=1)
 
@@ -1187,8 +1191,16 @@ def user_import(request):
 @staff_required
 def user_edit(request, user_id):
     @receiver(RewardPointGranting.granted_by_removal)
-    def notify_reward_points(users, **_kwargs):
-        messages.info(request, _('The removal of courses has granted the user "{}" reward points for the active semester.'.format(users[0].username)))
+    def notify_reward_points(grantings, **_kwargs):
+        assert len(grantings) == 1
+
+        messages.info(request,
+            ngettext(
+                'The removal of courses has granted the user "{granting.user_profile.username}" {granting.value} reward point for the active semester.',
+                'The removal of courses has granted the user "{granting.user_profile.username}" {granting.value} reward points for the active semester.',
+                grantings[0].value
+            ).format(granting=grantings[0])
+        )
 
     user = get_object_or_404(UserProfile, id=user_id)
     form = UserForm(request.POST or None, request.FILES or None, instance=user)


Test patch:
diff --git a/evap/rewards/tests/test_tools.py b/evap/rewards/tests/test_tools.py
--- a/evap/rewards/tests/test_tools.py
+++ b/evap/rewards/tests/test_tools.py
@@ -11,9 +11,9 @@
 
 
 @override_settings(REWARD_POINTS=[
-    (1.0/3.0, 1),
-    (2.0/3.0, 2),
-    (3.0/3.0, 3),
+    (1 / 3, 1),
+    (2 / 3, 2),
+    (3 / 3, 3),
 ])
 class TestGrantRewardPoints(WebTest):
     csrf_checks = False
@@ -66,9 +66,9 @@ def test_already_got_enough_points(self):
 
 
 @override_settings(REWARD_POINTS=[
-    (1.0/3.0, 1),
-    (2.0/3.0, 2),
-    (3.0/3.0, 3),
+    (1 / 3, 1),
+    (2 / 3, 2),
+    (3 / 3, 3),
 ])
 class TestGrantRewardPointsParticipationChange(TestCase):
     @classmethod
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
@@ -6,6 +6,7 @@
 from django.contrib.auth.models import Group
 from django.core import mail
 from django.urls import reverse
+from django.test import override_settings
 from model_mommy import mommy
 import xlrd
 
@@ -124,6 +125,11 @@ def test_user_is_created(self):
         self.assertEqual(UserProfile.objects.order_by("pk").last().username, "mflkd862xmnbo5")
 
 
+@override_settings(REWARD_POINTS=[
+    (1 / 3, 1),
+    (2 / 3, 2),
+    (3 / 3, 3),
+])
 class TestUserEditView(ViewTest):
     url = "/staff/user/3/edit"
     test_users = ['staff']
@@ -156,7 +162,7 @@ def test_reward_points_granting_message(self):
         student.refresh_from_db()
 
         self.assertIn("Successfully updated user.", page)
-        self.assertIn("The removal of courses has granted the user &quot;{}&quot; reward points for the active semester.".format(student.username), page)
+        self.assertIn("The removal of courses has granted the user &quot;{}&quot; 3 reward points for the active semester.".format(student.username), page)
 
 
 class TestUserMergeSelectionView(ViewTest):
@@ -872,6 +878,11 @@ def test_course_create(self):
         self.assertEqual(Course.objects.get().name_de, "lfo9e7bmxp1xi")
 
 
+@override_settings(REWARD_POINTS=[
+    (1 / 3, 1),
+    (2 / 3, 2),
+    (3 / 3, 3),
+])
 class TestCourseEditView(ViewTest):
     url = '/staff/semester/1/course/1/edit'
     test_users = ['staff']
@@ -925,7 +936,7 @@ def test_participant_removal_reward_point_granting_message(self):
         form['participants'] = [other.pk]
         page = form.submit('operation', value='save').follow()
 
-        self.assertIn("The removal of participants has granted 1 user reward points: &quot;{}&quot;".format(student.username), page)
+        self.assertIn("The removal as participant has granted the user &quot;{}&quot; 3 reward points for the active semester.".format(student.username), page)
 
     def test_remove_participants(self):
         already_evaluated = mommy.make(Course, semester=self.course.semester)
@@ -934,7 +945,7 @@ def test_remove_participants(self):
 
         for name in ["a", "b", "c", "d", "e"]:
             mommy.make(UserProfile, username=name, email="{}@institution.example.com".format(name),
-            courses_participating_in=[self.course, already_evaluated], courses_voted_for=[already_evaluated])
+                courses_participating_in=[self.course, already_evaluated], courses_voted_for=[already_evaluated])
 
         page = self.app.get(reverse('staff:course_edit', args=[self.course.semester.pk, self.course.pk]), user='staff')
 
@@ -943,7 +954,30 @@ def test_remove_participants(self):
         form['participants'] = [student.pk]
         page = form.submit('operation', value='save').follow()
 
-        self.assertIn("The removal of participants has granted 5 users reward points: &quot;a&quot;, &quot;b&quot;, &quot;c&quot;, &quot;d&quot;, &quot;e&quot;.", page)
+        for name in ["a", "b", "c", "d", "e"]:
+            self.assertIn("The removal as participant has granted the user &quot;{}&quot; 3 reward points for the active semester.".format(name), page)
+
+    def test_remove_participants_proportional_reward_points(self):
+        already_evaluated = mommy.make(Course, semester=self.course.semester)
+        SemesterActivation.objects.create(semester=self.course.semester, is_active=True)
+        student = mommy.make(UserProfile, courses_participating_in=[self.course])
+
+        for name, points_granted in [("a", 0), ("b", 1), ("c", 2), ("d", 3)]:
+            user = mommy.make(UserProfile, username=name, email="{}@institution.example.com".format(name),
+                courses_participating_in=[self.course, already_evaluated], courses_voted_for=[already_evaluated])
+            RewardPointGranting.objects.create(user_profile=user, semester=self.course.semester, value=points_granted)
+
+        page = self.app.get(reverse('staff:course_edit', args=[self.course.semester.pk, self.course.pk]), user='staff')
+
+        # remove four participants
+        form = page.forms['course-form']
+        form['participants'] = [student.pk]
+        page = form.submit('operation', value='save').follow()
+
+        self.assertIn("The removal as participant has granted the user &quot;a&quot; 3 reward points for the active semester.", page)
+        self.assertIn("The removal as participant has granted the user &quot;b&quot; 2 reward points for the active semester.", page)
+        self.assertIn("The removal as participant has granted the user &quot;c&quot; 1 reward point for the active semester.", page)
+        self.assertNotIn("The removal as participant has granted the user &quot;d&quot;", page)
 
     def test_last_modified_user(self):
         """