Skip to content

Commit

Permalink
Allow user to skip submitters when using manual assessment
Browse files Browse the repository at this point in the history
Bugs in the NextUnassessedSubmitterView were previously fixed in PR #1413.
However, the previous fixes removed the possibility to skip submitters.
These changes re-implement skipping, but this time a bit differently.

Fixes #1439
  • Loading branch information
ihalaij1 committed Feb 4, 2025
1 parent e9848a0 commit 5b42086
Show file tree
Hide file tree
Showing 8 changed files with 482 additions and 374 deletions.
2 changes: 1 addition & 1 deletion course/templates/course/staff/all_submissions_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<p>
{% if count <= default_limit and not limited %}
{% blocktranslate trimmed with count=count url=all_url %}
NUM_OF_SUBMISSIONS_DISPLAYED -- {{ count }}, {{ url }}
NUM_OF_SUBMISSIONS_DISPLAYED -- {{ count }}
{% endblocktranslate %}
{% elif limited %}
{% blocktranslate trimmed with limit=default_limit url=all_url %}
Expand Down
54 changes: 43 additions & 11 deletions exercise/staff_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,8 @@ class NextUnassessedSubmitterView(ExerciseBaseView, BaseRedirectView):
"""
access_mode = ACCESS.ASSISTANT

def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
# Query submitters who have not been assessed yet.
submitter = None
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: # pylint: disable=too-many-locals
# Query submitters who have not been assessed yet
submitters = (UserProfile.objects
.filter(submissions__exercise=self.exercise)
.annotate(
Expand All @@ -408,30 +407,63 @@ def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
earliest_submission=Min('submissions__submission_time'),
)
.order_by('earliest_submission'))

total_submitters = submitters.count()
unassessed_submitters = submitters.filter(count_assessed=0).order_by('earliest_submission')
submitter = unassessed_submitters.first()

assessed_submitters_count = total_submitters - unassessed_submitters.count() + 1
# Retrieve skipped submitters from the session
skipped_submitters = request.session.get('manual_assessment_skipped_submitters', [])

unskipped_and_unassessed_submitters = (
submitters.filter(count_assessed=0).exclude(id__in=skipped_submitters).order_by('earliest_submission')
)

# If no unassessed, unskipped submitters remain, reset skipped submitters list and retry
if not unskipped_and_unassessed_submitters.exists() and skipped_submitters:
request.session['manual_assessment_skipped_submitters'] = []
return self.get(request, *args, **kwargs)

submitter = unskipped_and_unassessed_submitters.first()

if not submitter:
# There are no more unassessed submitters.
# There are no more unassessed submitters
messages.success(request, _('ALL_SUBMITTERS_HAVE_BEEN_ASSESSED'))
return self.redirect(self.exercise.get_submission_list_url())

# Find the submitter's best submission using the cache.
previous_submitter_id = request.GET.get('prev')
if previous_submitter_id:
try:
previous_submitter_id = int(request.GET.get('prev'))
except ValueError:
previous_submitter_id = None

# If user moved on to the next submitter without assessing the previous submitter, skip the current submitter
if previous_submitter_id == submitter.id:
skipped_submitters = request.session.get('manual_assessment_skipped_submitters', [])
if submitter.id not in skipped_submitters:
# Save the skipped submitter to the session
skipped_submitters.append(submitter.id)
request.session['manual_assessment_skipped_submitters'] = skipped_submitters
return self.redirect(self.exercise.get_url('submission-next-unassessed'))

# Find the submitter's best submission using the cache
cache = CachedPoints(self.instance, submitter.user, True)
ids = cache.submission_ids(exercise_id=self.exercise.id, best=True, fallback_to_last=True)
if not ids:
raise Http404()

assessed_submitters_count = total_submitters - unskipped_and_unassessed_submitters.count() + 1

percentage = f"{int(assessed_submitters_count / total_submitters * 100)}%" if total_submitters else "0%"
self.request.session['manually_assessed_counter'] = (
f"{assessed_submitters_count} / {total_submitters} ({percentage})"
)
counter_str = f"{assessed_submitters_count} / {total_submitters} ({percentage})"
if len(skipped_submitters) > 0:
counter_str += " (" + _('SKIPPED') + f" {len(skipped_submitters)})"
self.request.session['manually_assessed_counter'] = counter_str

url = reverse(
'submission-inspect',
kwargs={'submission_id': ids[0], **kwargs},
)

return self.redirect(url)


Expand Down
4 changes: 2 additions & 2 deletions exercise/templates/exercise/module_goal_modal.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ <h4 class="title" id="pointsGoalModalLabel">{% translate "PERSONALIZED_POINTS_MO
<form id="pointsGoalForm" method="POST"
action="{% url 'save_points_goal_form_view' course_slug=course.url instance_slug=instance.url module_slug=module.url %}"
data-module-url="{{module.url|lower}}"
data-personalized-points-goal-text="{% translate 'PERSONALIZED_POINTS_GOAL ' %}"
data-personalized-points-goal-text="{% translate 'PERSONALIZED_POINTS_GOAL' %}"
data-personalized-points-goal-tooltip-text="{% translate 'POINTS_GOAL' %}"
data-points="{{ points }}"
data-points-goal={{ points_goal }}
Expand All @@ -53,7 +53,7 @@ <h4 class="title" id="pointsGoalModalLabel">{% translate "PERSONALIZED_POINTS_MO
<form id="deletePointsGoalForm" method="DELETE"
action="{% url 'delete_points_goal_form_view' course_slug=course.url instance_slug=instance.url module_slug=module.url %}"
data-module-url="{{module.url|lower}}"
data-personalized-points-goal-text="{% translate 'PERSONALIZED_POINTS_GOAL ' %}"
data-personalized-points-goal-text="{% translate 'PERSONALIZED_POINTS_GOAL' %}"
data-personalized-points-goal-tooltip-text="{% translate 'POINTS_GOAL' %}"
data-points="{{ points }}"
data-points-goal={{ points_goal }}
Expand Down
2 changes: 1 addition & 1 deletion exercise/templates/exercise/staff/_submissions_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
{% exercise_text_stats exercise %} |
{% if count <= default_limit and not limited %}
{% blocktranslate trimmed with count=count url=all_url %}
NUM_OF_SUBMISSIONS_DISPLAYED -- {{ count }}, {{ url }}
NUM_OF_SUBMISSIONS_DISPLAYED -- {{ count }}
{% endblocktranslate %}
{% elif limited %}
{% blocktranslate trimmed with limit=default_limit url=all_url %}
Expand Down
2 changes: 1 addition & 1 deletion exercise/templates/exercise/staff/inspect_submission.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

<div class="col-md-9">
<div>
<a href="{{ exercise|url:'submission-next-unassessed' }}" class="pull-right">
<a href="{{ exercise|url:'submission-next-unassessed' }}?prev={{ submission.submitters.all.first.id }}" class="pull-right">
{% translate "ASSESS_NEXT_SUBMITTER_MANUALLY" %}
{{ request.session.manually_assessed_counter }}
<span aria-hidden="true">&raquo;</span>
Expand Down
7 changes: 5 additions & 2 deletions exercise/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1524,9 +1524,9 @@ def test_enrollment_questionaire_opening_time(self):
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():
def get_url_user_id(args=''):
response = self.client.get(
f"{exercise.get_absolute_url()}submitters/next-unassessed/")
f"{exercise.get_absolute_url()}submitters/next-unassessed/{args}")
try:
return int(response.url.split('/submissions/')[1].split('/inspect/')[0])
except Exception:
Expand Down Expand Up @@ -1571,6 +1571,9 @@ def create_submission(user, submission_time):
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()
Expand Down
Loading

0 comments on commit 5b42086

Please sign in to comment.