Repository: e-valuation/EvaP

Issue:
Evaluation weight doesn't add up to 100%
Split out from #1621. See screenshot:
![image](https://user-images.githubusercontent.com/1891915/147852750-2dd6c5c2-a500-42a1-b997-ec2e74039da8.png)

Evaluation weight doesn't add up to 100%
Split out from #1621. See screenshot:
![image](https://user-images.githubusercontent.com/1891915/147852750-2dd6c5c2-a500-42a1-b997-ec2e74039da8.png)



Discussion:
@janno42 if there's an evaluation with some non-zero weight that's not shown (evaluation hasn't started yet for staff users, evaluation is not published for all other users), should the shown weights still add up to 100% or to 100% minus the percentage of the hidden evaluation? what if the evaluation is shown here but the grade (and thus the percentage) is not shown?
The percentage values should reflect the total weighting (from a global permission-independent view) even if single evaluations are not shown. So the values may add up to less than 100%.
If an evaluation is shown but the grade is not yet displayed, we should optimally already show the percentage.
The problem is that [this annotate](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/tools.py#L301) operates on the filtered evaluations. Swapping the filter and the annotate should work [according to the docs](https://docs.djangoproject.com/en/4.0/topics/db/aggregation/#order-of-annotate-and-filter-clauses), but it gives me the sum multiplied by the number of evaluations per course. This might be a variant of [this documented bug](https://docs.djangoproject.com/en/2.2/topics/db/aggregation/#combining-multiple-aggregations). Fetching the courses separately like below should work.

```python
    course_id_evaluation_weight_sum_pairs = (
        Course.objects
        .filter(pk__in=Course.objects.filter(evaluations__in=evaluations))
        .annotate(Sum("evaluations__weight"))
        .values_list("id", "evaluations__weight__sum")
    ) 
```

An additional weirdness of this view is that first the correct value is annotated [here](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/views.py#L195) (it is correct because the list of evaluations contains all evaluations of the course), but then overwritten with an incorrect annotation [here](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/views.py#L210).
Generally, the filter-then-annotate approach in results/views.py:210 is wrong, so I think fixing that method in itself is an okay solution for this issue.

Analyzing why the annotation happens twice is maybe something for another issue.
@janno42 if there's an evaluation with some non-zero weight that's not shown (evaluation hasn't started yet for staff users, evaluation is not published for all other users), should the shown weights still add up to 100% or to 100% minus the percentage of the hidden evaluation? what if the evaluation is shown here but the grade (and thus the percentage) is not shown?
The percentage values should reflect the total weighting (from a global permission-independent view) even if single evaluations are not shown. So the values may add up to less than 100%.
If an evaluation is shown but the grade is not yet displayed, we should optimally already show the percentage.
The problem is that [this annotate](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/tools.py#L301) operates on the filtered evaluations. Swapping the filter and the annotate should work [according to the docs](https://docs.djangoproject.com/en/4.0/topics/db/aggregation/#order-of-annotate-and-filter-clauses), but it gives me the sum multiplied by the number of evaluations per course. This might be a variant of [this documented bug](https://docs.djangoproject.com/en/2.2/topics/db/aggregation/#combining-multiple-aggregations). Fetching the courses separately like below should work.

```python
    course_id_evaluation_weight_sum_pairs = (
        Course.objects
        .filter(pk__in=Course.objects.filter(evaluations__in=evaluations))
        .annotate(Sum("evaluations__weight"))
        .values_list("id", "evaluations__weight__sum")
    ) 
```

An additional weirdness of this view is that first the correct value is annotated [here](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/views.py#L195) (it is correct because the list of evaluations contains all evaluations of the course), but then overwritten with an incorrect annotation [here](https://github.com/e-valuation/EvaP/blob/1427e47d4eddec45d443bc441d61d4298219dbd8/evap/results/views.py#L210).
Generally, the filter-then-annotate approach in results/views.py:210 is wrong, so I think fixing that method in itself is an okay solution for this issue.

Analyzing why the annotation happens twice is maybe something for another issue.

Resolving patch:
diff --git a/evap/results/tools.py b/evap/results/tools.py
--- a/evap/results/tools.py
+++ b/evap/results/tools.py
@@ -298,8 +298,8 @@ def get_evaluations_with_course_result_attributes(evaluations):
     )
 
     course_id_evaluation_weight_sum_pairs = (
-        Course.objects.filter(evaluations__in=evaluations)
-        .annotate(Sum("evaluations__weight"))
+        Course.objects.annotate(Sum("evaluations__weight"))
+        .filter(pk__in=Course.objects.filter(evaluations__in=evaluations))  # is needed, see #1691
         .values_list("id", "evaluations__weight__sum")
     )
 


Test patch:
diff --git a/evap/results/tests/test_views.py b/evap/results/tests/test_views.py
--- a/evap/results/tests/test_views.py
+++ b/evap/results/tests/test_views.py
@@ -20,6 +20,7 @@
     Evaluation,
     Question,
     Questionnaire,
+    RatingAnswerCounter,
     Semester,
     UserProfile,
 )
@@ -31,7 +32,7 @@
 )
 from evap.results.exporters import TextAnswerExporter
 from evap.results.tools import cache_results
-from evap.results.views import get_evaluations_with_prefetched_data
+from evap.results.views import get_evaluations_with_prefetched_data, warm_up_template_cache
 from evap.staff.tests.utils import WebTestStaffMode, helper_exit_staff_mode, run_in_staff_mode
 
 
@@ -264,6 +265,32 @@ def make_course_with_evaluations(unique_suffix):
         caches["sessions"].clear()
         caches["results"].clear()
 
+    def test_evaluation_weight_sums(self):
+        """Regression test for #1691"""
+        student = baker.make(UserProfile, email="student@institution.example.com")
+        course = baker.make(Course)
+
+        published = baker.make(
+            Evaluation,
+            course=course,
+            name_en=iter(["ev1", "ev2", "ev3"]),
+            name_de=iter(["ev1", "ev2", "ev3"]),
+            state=iter([Evaluation.State.NEW, Evaluation.State.PUBLISHED, Evaluation.State.PUBLISHED]),
+            weight=iter([8, 3, 4]),
+            is_single_result=True,
+            _quantity=3,
+        )[1:]
+
+        contributions = [e.general_contribution for e in published]
+        baker.make(RatingAnswerCounter, contribution=iter(contributions), answer=2, count=2, _quantity=len(published))
+        warm_up_template_cache(published)
+
+        page = self.app.get(self.url, user=student)
+        decoded = page.body.decode()
+
+        self.assertTrue(decoded.index("ev2") < decoded.index(" 20% ") < decoded.index("ev3") < decoded.index(" 26% "))
+        self.assertNotContains(page, " 53% ")
+
 
 class TestGetEvaluationsWithPrefetchedData(TestCase):
     def test_returns_correct_participant_count(self):