Repository: e-valuation/EvaP

Issue:
Allow redeeming points only for open events
Currently there is no check that the event for which points should be redeemed is still available for redemptions. If the `redeem_end_date` is reached, the event can't be selected on the rewards index page anymore, but a correct post request can still redeem points of a user for this event.
This should not be possible.



Discussion:

Resolving patch:
diff --git a/evap/rewards/models.py b/evap/rewards/models.py
--- a/evap/rewards/models.py
+++ b/evap/rewards/models.py
@@ -14,6 +14,11 @@ class NotEnoughPoints(Exception):
     pass
 
 
+class RedemptionEventExpired(Exception):
+    """An attempt has been made to redeem more points for an event whose redeem_end_date lies in the past."""
+    pass
+
+
 class RewardPointRedemptionEvent(models.Model):
     name = models.CharField(max_length=1024, verbose_name=_("event name"))
     date = models.DateField(verbose_name=_("event date"))
diff --git a/evap/rewards/tools.py b/evap/rewards/tools.py
--- a/evap/rewards/tools.py
+++ b/evap/rewards/tools.py
@@ -1,3 +1,5 @@
+from datetime import date
+
 from django.conf import settings
 from django.contrib import messages
 from django.db import transaction
@@ -7,7 +9,8 @@
 from django.contrib.auth.decorators import login_required
 from evap.evaluation.models import Course
 
-from evap.rewards.models import RewardPointGranting, RewardPointRedemption, RewardPointRedemptionEvent, SemesterActivation, NoPointsSelected, NotEnoughPoints
+from evap.rewards.models import RewardPointGranting, RewardPointRedemption, RewardPointRedemptionEvent, \
+                                SemesterActivation, NoPointsSelected, NotEnoughPoints, RedemptionEventExpired
 
 
 @login_required
@@ -24,10 +27,14 @@ def save_redemptions(request, redemptions):
 
     for event_id in redemptions:
         if redemptions[event_id] > 0:
+            event = RewardPointRedemptionEvent.objects.get(id=event_id)
+            if event.redeem_end_date < date.today():
+                raise RedemptionEventExpired(_("Sorry, the deadline for this event expired already."))
+
             RewardPointRedemption.objects.create(
                 user_profile=request.user,
                 value=redemptions[event_id],
-                event=RewardPointRedemptionEvent.objects.get(id=event_id)
+                event=event
             )
 
 
@@ -36,13 +43,10 @@ def can_user_use_reward_points(user):
 
 
 def reward_points_of_user(user):
-    reward_point_grantings = RewardPointGranting.objects.filter(user_profile=user)
-    reward_point_redemptions = RewardPointRedemption.objects.filter(user_profile=user)
-
     count = 0
-    for granting in reward_point_grantings:
+    for granting in user.reward_point_grantings.all():
         count += granting.value
-    for redemption in reward_point_redemptions:
+    for redemption in user.reward_point_redemptions.all():
         count -= redemption.value
 
     return count
@@ -75,8 +79,4 @@ def grant_reward_points(sender, **kwargs):
 
 
 def is_semester_activated(semester):
-    try:
-        activation = SemesterActivation.objects.get(semester=semester)
-        return activation.is_active
-    except SemesterActivation.DoesNotExist:
-        return False
+    return SemesterActivation.objects.filter(semester=semester, is_active=True).exists()
diff --git a/evap/rewards/views.py b/evap/rewards/views.py
--- a/evap/rewards/views.py
+++ b/evap/rewards/views.py
@@ -13,7 +13,8 @@
 
 from evap.staff.views import semester_view
 
-from evap.rewards.models import RewardPointGranting, RewardPointRedemption, RewardPointRedemptionEvent, SemesterActivation, NoPointsSelected, NotEnoughPoints
+from evap.rewards.models import RewardPointGranting, RewardPointRedemption, RewardPointRedemptionEvent, \
+                                SemesterActivation, NoPointsSelected, NotEnoughPoints, RedemptionEventExpired
 from evap.rewards.tools import save_redemptions, reward_points_of_user
 from evap.rewards.forms import RewardPointRedemptionEventForm
 from evap.rewards.exporters import ExcelExporter
@@ -31,14 +32,13 @@ def index(request):
         try:
             save_redemptions(request, redemptions)
             messages.success(request, _("You successfully redeemed your points."))
-        except (NoPointsSelected, NotEnoughPoints) as error:
+        except (NoPointsSelected, NotEnoughPoints, RedemptionEventExpired) as error:
             messages.warning(request, error)
 
     total_points_available = reward_points_of_user(request.user)
     reward_point_grantings = RewardPointGranting.objects.filter(user_profile=request.user)
     reward_point_redemptions = RewardPointRedemption.objects.filter(user_profile=request.user)
-    events = RewardPointRedemptionEvent.objects.filter(redeem_end_date__gte=datetime.now())
-    events = sorted(events, key=lambda event: event.date)
+    events = RewardPointRedemptionEvent.objects.filter(redeem_end_date__gte=datetime.now()).order_by('date')
 
     reward_point_actions = []
     for granting in reward_point_grantings:


Test patch:
diff --git a/evap/rewards/tests/test_views.py b/evap/rewards/tests/test_views.py
--- a/evap/rewards/tests/test_views.py
+++ b/evap/rewards/tests/test_views.py
@@ -1,3 +1,5 @@
+from datetime import date, timedelta
+
 from django.contrib.auth.models import Group
 from django.core.urlresolvers import reverse
 
@@ -43,7 +45,7 @@ def setUpTestData(cls):
         cls.student = mommy.make(UserProfile, username='student', email='foo@hpi.de')
         mommy.make(Course, participants=[cls.student])
         mommy.make(RewardPointGranting, user_profile=cls.student, value=5)
-        mommy.make(RewardPointRedemptionEvent, _quantity=2)
+        mommy.make(RewardPointRedemptionEvent, _quantity=2, redeem_end_date=date.today() + timedelta(days=1))
 
     def test_redeem_all_points(self):
         response = self.app.get(reverse('rewards:index'), user='student')
@@ -56,13 +58,22 @@ def test_redeem_all_points(self):
         self.assertEqual(0, reward_points_of_user(self.student))
 
     def test_redeem_too_many_points(self):
-        mommy.make(RewardPointRedemptionEvent)
         response = self.app.get(reverse('rewards:index'), user='student')
         form = response.forms['reward-redemption-form']
         form.set('points-1', 3)
         form.set('points-2', 3)
         response = form.submit()
-        self.assertIn(b"have enough reward points.", response.body)
+        self.assertContains(response, "have enough reward points.")
+        self.assertEqual(5, reward_points_of_user(self.student))
+
+    def test_redeem_points_for_expired_event(self):
+        """ Regression test for #846 """
+        response = self.app.get(reverse('rewards:index'), user='student')
+        form = response.forms['reward-redemption-form']
+        form.set('points-2', 1)
+        RewardPointRedemptionEvent.objects.update(redeem_end_date=date.today() - timedelta(days=1))
+        response = form.submit()
+        self.assertContains(response, "event expired already.")
         self.assertEqual(5, reward_points_of_user(self.student))