Repository: e-valuation/EvaP

Issue:
Prioritize evaluations with finished grading for review
In quick text answer review, evaluations where the grading process is finished should be shown first in the list of suggested evaluations for review.
The list is created in `evap.staff.tools.find_unreviewed_evaluations`.


Discussion:

Resolving patch:
diff --git a/evap/staff/tools.py b/evap/staff/tools.py
--- a/evap/staff/tools.py
+++ b/evap/staff/tools.py
@@ -334,15 +334,17 @@ def find_unreviewed_evaluations(semester, excluded):
     if datetime.now().hour < settings.EVALUATION_END_OFFSET_HOURS:
         exclude_date -= timedelta(days=1)
 
-    return (
-        semester.evaluations.exclude(pk__in=excluded)
-        .exclude(state=Evaluation.State.PUBLISHED)
-        .exclude(vote_end_date__gte=exclude_date)
-        .exclude(can_publish_text_results=False)
-        .filter(contributions__textanswer_set__state=TextAnswer.State.NOT_REVIEWED)
-        .annotate(num_unreviewed_textanswers=Count("contributions__textanswer_set"))
-        .order_by("vote_end_date", "-num_unreviewed_textanswers")
-        .all()
+    # Evaluations where the grading process is finished should be shown first, need to be sorted in Python
+    return sorted(
+        (
+            semester.evaluations.exclude(pk__in=excluded)
+            .exclude(state=Evaluation.State.PUBLISHED)
+            .exclude(vote_end_date__gte=exclude_date)
+            .exclude(can_publish_text_results=False)
+            .filter(contributions__textanswer_set__state=TextAnswer.State.NOT_REVIEWED)
+            .annotate(num_unreviewed_textanswers=Count("contributions__textanswer_set"))
+        ),
+        key=lambda e: (-e.grading_process_is_finished, e.vote_end_date, -e.num_unreviewed_textanswers),
     )
 
 


Test patch:
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
@@ -2187,16 +2187,16 @@ class TestEvaluationTextAnswerView(WebTest):
     @classmethod
     def setUpTestData(cls):
         cls.manager = make_manager()
-        semester = baker.make(Semester, pk=1)
-        student1 = baker.make(UserProfile, email="student@institution.example.com")
+        cls.semester = baker.make(Semester, pk=1)
+        cls.student1 = baker.make(UserProfile, email="student@institution.example.com")
         cls.student2 = baker.make(UserProfile, email="student2@example.com")
 
         cls.evaluation = baker.make(
             Evaluation,
             pk=1,
-            course__semester=semester,
-            participants=[student1, cls.student2],
-            voters=[student1],
+            course__semester=cls.semester,
+            participants=[cls.student1, cls.student2],
+            voters=[cls.student1],
             state=Evaluation.State.IN_EVALUATION,
         )
         top_general_questionnaire = baker.make(Questionnaire, type=Questionnaire.Type.TOP)
@@ -2216,9 +2216,9 @@ def setUpTestData(cls):
 
         cls.evaluation2 = baker.make(
             Evaluation,
-            course__semester=semester,
-            participants=[student1],
-            voters=[student1, cls.student2],
+            course__semester=cls.semester,
+            participants=[cls.student1],
+            voters=[cls.student1, cls.student2],
             vote_start_datetime=datetime.datetime.now() - datetime.timedelta(days=5),
             vote_end_date=datetime.date.today() - datetime.timedelta(days=4),
             can_publish_text_results=True,
@@ -2280,6 +2280,51 @@ def test_exclude_unfinished_evaluations(self):
             # unfinished because still in EVALUATION_END_OFFSET_HOURS
             self.assertNotContains(page, self.evaluation2.full_name)
 
+    def test_suggested_evaluation_ordering(self):
+        evaluations = baker.make(
+            Evaluation,
+            course__semester=self.semester,
+            participants=[self.student1, self.student2],
+            voters=[self.student1, self.student2],
+            state=Evaluation.State.IN_EVALUATION,
+            vote_start_datetime=datetime.datetime.now() - datetime.timedelta(days=42),
+            vote_end_date=datetime.date.today() - datetime.timedelta(days=2),
+            can_publish_text_results=True,
+            _quantity=2,
+        )
+
+        for evaluation, answer_count in zip(evaluations, [1, 2]):
+            contribution = baker.make(Contribution, evaluation=evaluation, _fill_optional=["contributor"])
+            baker.make(TextAnswer, contribution=contribution, question__type=Question.TEXT, _quantity=answer_count)
+
+        url = f"/staff/semester/{self.semester.pk}/evaluation/{self.evaluation2.pk}/textanswers"
+
+        with run_in_staff_mode(self):
+            # Since Evaluation 1 has an extra text answer, it should be first
+            page = self.app.get(url, user=self.manager)
+            self.assertIn(
+                f'data-evaluation="{evaluations[1].pk}"',
+                str(page.html.select_one("span[data-next-evaluation-index]")),
+            )
+
+            # Since Evaluation 0 has an earlier end date, it should now be first
+            evaluations[0].vote_end_date = datetime.date.today() - datetime.timedelta(days=4)
+            evaluations[0].save()
+            page = self.app.get(url, user=self.manager)
+            self.assertIn(
+                f'data-evaluation="{evaluations[0].pk}"',
+                str(page.html.select_one("span[data-next-evaluation-index]")),
+            )
+
+            # Since the grading process for Evaluation 1 is finished, it should be first
+            evaluations[1].wait_for_grade_upload_before_publishing = False
+            evaluations[1].save()
+            page = self.app.get(url, user=self.manager)
+            self.assertIn(
+                f'data-evaluation="{evaluations[1].pk}"',
+                str(page.html.select_one("span[data-next-evaluation-index]")),
+            )
+
     def test_num_queries_is_constant(self):
         let_user_vote_for_evaluation(self.student2, self.evaluation)
         with run_in_staff_mode(self):