Repository: e-valuation/EvaP

Issue:
Export Reward Point Summary
A new button `Export reward points` should be added to the right of the text showing the number of available reward points on the staff reward points redemption events page. Clicking the button should download a CSV file containing a summary of the reward points.

This file should contain the two columns `Email` and `Points` for each user, listing the number of points currently available for that user (grantings minus redemptions) next to the user's email address. A line should only be added for users where this number of available points is not zero.


Discussion:
for the sake of consistency i think this should either result in a xls file or the `Export Redemptions` button should output a csv as well  
It also should have consistent content: `Export Redemptions` gives Last name, First name, Email address and Number of points, not only Email address and points
We talked about this shortly in person and I agree partially: Since both sheets seem to be more on the "just data" than the "this is a spreadsheet with colors and stuff like that" side, I would even say that both should be CSV (as Excel can just open the CSV too). @janno42 What do you think?
@joendter do you want to continue working on this or should we unassign you?
Go ahead and unassign me
> We talked about this shortly in person and I agree partially: Since both sheets seem to be more on the "just data" than the "this is a spreadsheet with colors and stuff like that" side, I would even say that both should be CSV (as Excel can just open the CSV too). @janno42 What do you think?

I'd say CSV and just email and points is sufficient for the use case of this issue. The exported file is used internally for a quick overview of the current reward point status and we don't need the name information because the export is not used for, e.g., email generation. I also prefer CSV over Excel where we don't need formatting.

Resolving patch:
diff --git a/evap/rewards/urls.py b/evap/rewards/urls.py
--- a/evap/rewards/urls.py
+++ b/evap/rewards/urls.py
@@ -7,6 +7,7 @@
 urlpatterns = [
     path("", views.index, name="index"),
 
+    path("reward_points_export", views.reward_points_export, name="reward_points_export"),
     path("reward_point_redemption_events/", views.reward_point_redemption_events, name="reward_point_redemption_events"),
     path("reward_point_redemption_event/create", views.RewardPointRedemptionEventCreateView.as_view(), name="reward_point_redemption_event_create"),
     path("reward_point_redemption_event/<int:event_id>/edit", views.RewardPointRedemptionEventEditView.as_view(), name="reward_point_redemption_event_edit"),
diff --git a/evap/rewards/views.py b/evap/rewards/views.py
--- a/evap/rewards/views.py
+++ b/evap/rewards/views.py
@@ -1,3 +1,4 @@
+import csv
 from datetime import datetime
 
 from django.contrib import messages
@@ -14,7 +15,7 @@
 from django.views.generic import CreateView, UpdateView
 
 from evap.evaluation.auth import manager_required, reward_user_required
-from evap.evaluation.models import Semester
+from evap.evaluation.models import Semester, UserProfile
 from evap.evaluation.tools import AttachmentResponse, get_object_from_dict_pk_entry_or_logged_40x
 from evap.rewards.exporters import RewardsExporter
 from evap.rewards.forms import RewardPointRedemptionEventForm
@@ -158,6 +159,32 @@ def reward_point_redemption_event_export(request, event_id):
     return response
 
 
+@manager_required
+def reward_points_export(request):
+    filename = _("RewardPoints") + f"-{get_language()}.csv"
+    response = AttachmentResponse(filename, content_type="text/csv")
+
+    writer = csv.writer(response, delimiter=";", lineterminator="\n")
+    writer.writerow([_("Email address"), _("Number of points")])
+    profiles_with_points = (
+        UserProfile.objects.annotate(
+            points=Sum("reward_point_grantings__value", default=0) - Sum("reward_point_redemptions__value", default=0)
+        )
+        .filter(points__gt=0)
+        .order_by("-points")
+    )
+
+    for profile in profiles_with_points.all():
+        writer.writerow(
+            [
+                profile.email,
+                profile.points,
+            ]
+        )
+
+    return response
+
+
 @require_POST
 @manager_required
 def semester_activation_edit(request, semester_id):


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
@@ -180,7 +180,7 @@ def test_edit_redemption_event(self):
         self.assertEqual(RewardPointRedemptionEvent.objects.get(pk=self.event.pk).name, "new name")
 
 
-class TestExportView(WebTestStaffModeWith200Check):
+class TestEventExportView(WebTestStaffModeWith200Check):
     @classmethod
     def setUpTestData(cls):
         cls.test_users = [make_manager()]
@@ -189,6 +189,30 @@ def setUpTestData(cls):
         cls.url = f"/rewards/reward_point_redemption_event/{event.pk}/export"
 
 
+class TestPointsExportView(WebTestStaffModeWith200Check):
+    @classmethod
+    def setUpTestData(cls):
+        cls.test_users = [make_manager()]
+        cls.url = reverse("rewards:reward_points_export")
+
+        cls.student = baker.make(UserProfile, email="student@institution.example.com")
+        cls.event = baker.make(RewardPointRedemptionEvent, redeem_end_date=date.today() + timedelta(days=1))
+
+    def test_positive_points(self):
+        baker.make(RewardPointGranting, user_profile=self.student, value=5)
+        baker.make(RewardPointRedemption, user_profile=self.student, event=self.event, value=3)
+
+        response = self.app.get(self.url, user=self.test_users[0], status=200)
+        self.assertIn("student@institution.example.com;2", response)
+
+    def test_zero_points(self):
+        baker.make(RewardPointGranting, user_profile=self.student, value=5)
+        baker.make(RewardPointRedemption, user_profile=self.student, event=self.event, value=5)
+
+        response = self.app.get(self.url, user=self.test_users[0], status=200)
+        self.assertNotIn("student@institution.example.com", response)
+
+
 @override_settings(
     REWARD_POINTS=[
         (1 / 3, 1),