Repository: apluslms/a-plus

Issue:
The next submitter link for manual assessment
After merging the PR #1167 for the issue #1110, we found bugs in the new implementation. The pull request #1167 includes more comments about the details.


* The selected next submitter may be such a student who has already received manual grading on a submission other than the latest one. A student should be considered manually graded once any one of his/her submissions has been manually graded. The "next submitter" link should not repeat a student who has been manually graded.
* The "next submitter" link may repeat the same student who was already being inspected. This happens when the student's earlier submission (but not the latest) has been manually graded even though other students have not received any manual grading.
* When all students have received manual grading, the "next submitter" link must stop cycling between students like the old implementation had done. The old code has an if statement with the comment "There are no more unassessed submitters". It redirects back to the submission list.

_Originally posted by @Mankro in https://github.com/apluslms/a-plus/pull/1167#pullrequestreview-1474236719_
            


Discussion:

Resolving patch:
diff --git a/exercise/staff_views.py b/exercise/staff_views.py
--- a/exercise/staff_views.py
+++ b/exercise/staff_views.py
@@ -5,7 +5,7 @@
 from django.contrib.auth.models import User
 from django.core.exceptions import PermissionDenied, ValidationError
 from django.core.validators import URLValidator
-from django.db.models import Count, Max, Prefetch, Q
+from django.db.models import Count, Max, Min, Prefetch, Q
 from django.http.request import HttpRequest
 from django.http.response import HttpResponse, JsonResponse, Http404
 from django.shortcuts import get_object_or_404
@@ -350,17 +350,24 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
                     'submissions__id',
                     filter=(Q(submissions__grader__isnull=False)),
                 ),
+                earliest_submission=Min('submissions__submission_time'),
             )
-            .filter(count_assessed=0)
-            .order_by('id'))
-
+            .order_by('earliest_submission'))
         previous_user_id = request.GET.get('prev')
         if previous_user_id:
-            # Find specifically the submitter AFTER the previously inspected one.
-            submitters_after = submitters.filter(id__gt=previous_user_id)
+            # get the previous time
+            previous_time = submitters.filter(user__id=previous_user_id).first().earliest_submission
+
+            # remove the submitters who have been assessed (we do this after we've found the
+            # previous submitter's time as the previous submitter might have been assessed)
+            submitters = submitters.filter(count_assessed=0)
+
+            # Find specifically the submitter who's submission was submitted right after this one
+            submitters_after = submitters.filter(earliest_submission__gt=previous_time)
             submitter = submitters_after.first()
 
         if not submitter:
+            submitters = submitters.filter(count_assessed=0)
             submitter = submitters.first()
 
         if not submitter:


Test patch:
diff --git a/exercise/tests.py b/exercise/tests.py
--- a/exercise/tests.py
+++ b/exercise/tests.py
@@ -1513,3 +1513,94 @@ def test_enrollment_questionaire_opening_time(self):
             self.enrollment_exercise.check_submission_allowed(self.user.userprofile)[0],
             BaseExercise.SUBMIT_STATUS.CANNOT_ENROLL,
         )
+
+    def test_next_unassessed_submitter_view(self):
+        # parses the user ID from the URL response of NextUnassessedSubmitterView for convenience. If the URL format is
+        # different (e.g. redirect when all have been graded) return just the url
+        def get_url_user_id(args=''):
+            response = self.client.get(
+                f"{exercise.get_absolute_url()}submitters/next-unassessed/{args}")
+            try:
+                return int(response.url.split('/submissions/')[1].split('/inspect/')[0])
+            except Exception:
+                return response.url
+
+        def create_submission(user, submission_time):
+            submission = Submission.objects.create(
+                exercise=exercise,
+            )
+            submission.submitters.add(user)
+            submission.submission_time = submission_time
+            submission.save()
+            return submission
+
+        self.client.login(username="staff", password="staffPassword")
+
+        exercise = BaseExercise.objects.create(
+            order=10,
+            name="Unassessed Exercise Submitter View",
+            course_module=self.course_module,
+            category=self.learning_object_category,
+            url="unassessed",
+            max_submissions=1,
+        )
+        exercise.save()
+
+        user_submission = create_submission(self.user.userprofile, self.yesterday)
+
+        user2_submission = create_submission(self.user2.userprofile, self.today)
+
+        # user submission day before user2
+        self.assertEqual(user_submission.id, get_url_user_id())
+        self.assertEqual(user2_submission.id,
+                         get_url_user_id(f"?prev={self.user.id}"))
+
+        user2_submission.submission_time = self.yesterday - timedelta(days=1)
+        user2_submission.save()
+
+        # now user2 submission is earlier
+        self.assertEqual(user2_submission.id, get_url_user_id())
+
+        # we should now expect to get user1's submission first since user2s earlier submission has been graded
+        user2_submission.grader = self.teacher.userprofile
+        user2_submission.save()
+        self.assertEqual(user_submission.id, get_url_user_id())
+
+        # test with prev parameter
+        self.assertEqual(user_submission.id, get_url_user_id(f"?prev={self.user2.id}"))
+
+        # remove grader for further tests
+        user2_submission.grader = None
+        user2_submission.save()
+
+        # create a submission for user so the newest submission was made by user again and not user2
+        user_earlier_submission = create_submission(self.user.userprofile, self.yesterday - timedelta(days=2))
+
+        self.assertEqual(user_submission.id, get_url_user_id())
+
+        user2_other_exercise_submission  = Submission.objects.create(
+            exercise=self.exercise_with_attachment
+        )
+        user2_other_exercise_submission.submitters.add(self.user2.userprofile)
+        user2_other_exercise_submission.submission_time = self.yesterday - timedelta(days=3)
+        user2_other_exercise_submission.save()
+
+        # an even earlier submission in another exercise doesn't matter
+        self.assertEqual(user_submission.id, get_url_user_id())
+
+        # we should now expect user2's submission to be shown because user1's latest submission has been graded
+        user_earlier_submission.grader = self.teacher.userprofile
+        user_earlier_submission.save()
+        self.assertEqual(user2_submission.id, get_url_user_id())
+
+        # grading in an older submission (not latest) should also mean user1 is skipped
+        user_earlier_submission.grader = self.teacher.userprofile
+        user_earlier_submission.save()
+        user_submission.grader = None
+        user_submission.save()
+        self.assertEqual(user2_submission.id, get_url_user_id())
+
+        # everything has been graded (we should get the submissions list)
+        user2_submission.grader = self.teacher.userprofile
+        user2_submission.save()
+        self.assertEqual(exercise.get_submission_list_url(), get_url_user_id())