Skip to content

Commit 8b776c2

Browse files
Backend: Enhance submission limit handling in job utilities and views (#5055)
1 parent a7536db commit 8b776c2

File tree

3 files changed

+261
-32
lines changed

3 files changed

+261
-32
lines changed

apps/jobs/utils.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929

3030

3131
def get_remaining_submission_for_a_phase(
32-
user, challenge_phase_pk, challenge_pk, challenge_phase=None
32+
user,
33+
challenge_phase_pk,
34+
challenge_pk,
35+
challenge_phase=None,
36+
participant_team_pk=None,
3337
):
3438
"""
3539
Returns the number of remaining submissions that a participant can
@@ -41,14 +45,15 @@ def get_remaining_submission_for_a_phase(
4145
challenge_phase_pk: Primary key of the challenge phase
4246
challenge_pk: Primary key of the challenge
4347
challenge_phase: Optional pre-fetched ChallengePhase object to avoid N+1 queries
48+
participant_team_pk: Optional pre-fetched participant team PK to avoid
49+
repeated lookups when called in a loop
4450
"""
45-
# Use pre-fetched challenge_phase if provided, otherwise fetch it (for
46-
# backward compatibility)
4751
if challenge_phase is None:
4852
challenge_phase = get_challenge_phase_model(challenge_phase_pk)
49-
participant_team_pk = get_participant_team_id_of_user_for_a_challenge(
50-
user, challenge_pk
51-
)
53+
if participant_team_pk is None:
54+
participant_team_pk = get_participant_team_id_of_user_for_a_challenge(
55+
user, challenge_pk
56+
)
5257

5358
# Conditional check for the existence of participant team of the user.
5459
if not participant_team_pk:

apps/jobs/views.py

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from django.core.files.base import ContentFile
3535
from django.core.files.uploadedfile import SimpleUploadedFile
3636
from django.db import IntegrityError, transaction
37-
from django.db.models import Count
37+
from django.db.models import Count, Q
3838
from django.utils import dateparse, timezone
3939
from drf_spectacular.utils import (
4040
OpenApiParameter,
@@ -64,6 +64,7 @@
6464
from apps.accounts.authentication import ExpiringTokenAuthentication
6565

6666
from .aws_utils import generate_aws_eks_bearer_token
67+
from .constants import submission_status_to_exclude
6768
from .filters import SubmissionFilter
6869
from .models import Submission
6970
from .sender import publish_submission_message
@@ -77,7 +78,6 @@
7778
from .utils import (
7879
calculate_distinct_sorted_leaderboard_data,
7980
get_leaderboard_data_model,
80-
get_remaining_submission_for_a_phase,
8181
get_submission_model,
8282
handle_submission_rerun,
8383
handle_submission_resume,
@@ -869,6 +869,73 @@ def get_all_entries_on_public_leaderboard(request, challenge_phase_split_pk):
869869
return paginator.get_paginated_response(response_data)
870870

871871

872+
def _compute_remaining_limits(
873+
phase,
874+
submissions_done_count,
875+
submissions_done_this_month_count,
876+
submissions_done_today_count,
877+
now,
878+
):
879+
"""Pure-Python logic to compute remaining submission limits for a phase.
880+
881+
Mirrors the branching in get_remaining_submission_for_a_phase() but
882+
operates on pre-computed counts so it needs no database access.
883+
"""
884+
max_submissions_count = phase.max_submissions
885+
max_submissions_per_month_count = phase.max_submissions_per_month
886+
max_submissions_per_day_count = phase.max_submissions_per_day
887+
888+
if submissions_done_count >= max_submissions_count:
889+
return {
890+
"message": "You have exhausted maximum submission limit!",
891+
"submission_limit_exceeded": True,
892+
}
893+
894+
if submissions_done_this_month_count >= max_submissions_per_month_count:
895+
next_month_start = (now + datetime.timedelta(days=30)).replace(
896+
day=1, hour=0, minute=0, second=0, microsecond=0
897+
)
898+
remaining_time = next_month_start - now
899+
if submissions_done_today_count >= max_submissions_per_day_count:
900+
return {
901+
"message": "Both daily and monthly submission limits are exhausted!",
902+
"remaining_time": remaining_time,
903+
}
904+
return {
905+
"message": "You have exhausted this month's submission limit!",
906+
"remaining_time": remaining_time,
907+
}
908+
909+
if submissions_done_today_count >= max_submissions_per_day_count:
910+
tomorrow = now + datetime.timedelta(1)
911+
midnight = tomorrow.replace(hour=0, minute=0, second=0)
912+
remaining_time = midnight - now
913+
return {
914+
"message": "You have exhausted today's submission limit!",
915+
"remaining_time": remaining_time,
916+
}
917+
918+
remaining_submission_count = max_submissions_count - submissions_done_count
919+
remaining_submissions_this_month_count = (
920+
max_submissions_per_month_count - submissions_done_this_month_count
921+
)
922+
remaining_submissions_today_count = (
923+
max_submissions_per_day_count - submissions_done_today_count
924+
)
925+
remaining_submissions_this_month_count = min(
926+
remaining_submission_count, remaining_submissions_this_month_count
927+
)
928+
remaining_submissions_today_count = min(
929+
remaining_submissions_this_month_count,
930+
remaining_submissions_today_count,
931+
)
932+
return {
933+
"remaining_submissions_this_month_count": remaining_submissions_this_month_count,
934+
"remaining_submissions_today_count": remaining_submissions_today_count,
935+
"remaining_submissions_count": remaining_submission_count,
936+
}
937+
938+
872939
@api_view(["GET"])
873940
@throttle_classes([UserRateThrottle])
874941
@permission_classes((permissions.IsAuthenticated, HasVerifiedEmail))
@@ -910,34 +977,62 @@ def get_remaining_submissions(request, challenge_pk):
910977
"""
911978
phases_data = {}
912979
challenge = get_challenge_model(challenge_pk)
980+
981+
participant_team = get_participant_team_of_user_for_a_challenge(
982+
request.user, challenge_pk
983+
)
984+
if not participant_team:
985+
response_data = {"error": "You haven't participated in the challenge"}
986+
return Response(response_data, status=status.HTTP_403_FORBIDDEN)
987+
913988
challenge_phases = ChallengePhase.objects.filter(
914989
challenge=challenge
915990
).order_by("pk")
916991
if not is_user_a_host_of_challenge(request.user, challenge_pk):
917-
challenge_phases = challenge_phases.filter(
918-
challenge=challenge, is_public=True
919-
).order_by("pk")
920-
phase_data_list = list()
992+
challenge_phases = challenge_phases.filter(is_public=True)
993+
994+
now = timezone.now()
995+
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
996+
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
997+
998+
submission_filter = Q(
999+
submissions__participant_team=participant_team,
1000+
submissions__challenge_phase__challenge=challenge_pk,
1001+
) & ~Q(submissions__status__in=submission_status_to_exclude)
1002+
1003+
challenge_phases = challenge_phases.annotate(
1004+
submissions_count=Count(
1005+
"submissions",
1006+
filter=submission_filter,
1007+
),
1008+
submissions_this_month_count=Count(
1009+
"submissions",
1010+
filter=submission_filter
1011+
& Q(submissions__submitted_at__gte=month_start),
1012+
),
1013+
submissions_today_count=Count(
1014+
"submissions",
1015+
filter=submission_filter
1016+
& Q(submissions__submitted_at__gte=today_start),
1017+
),
1018+
)
1019+
1020+
phase_data_list = []
9211021
for phase in challenge_phases:
922-
(
923-
remaining_submission_message,
924-
response_status,
925-
) = get_remaining_submission_for_a_phase(
926-
request.user, phase.id, challenge_pk, challenge_phase=phase
1022+
limits = _compute_remaining_limits(
1023+
phase,
1024+
phase.submissions_count,
1025+
phase.submissions_this_month_count,
1026+
phase.submissions_today_count,
1027+
now,
9271028
)
928-
if response_status != status.HTTP_200_OK:
929-
return Response(
930-
remaining_submission_message, status=response_status
931-
)
9321029
phase_data_list.append(
9331030
RemainingSubmissionDataSerializer(
934-
phase, context={"limits": remaining_submission_message}
1031+
phase, context={"limits": limits}
9351032
).data
9361033
)
1034+
9371035
phases_data["phases"] = phase_data_list
938-
participant_team = get_participant_team_of_user_for_a_challenge(
939-
request.user, challenge_pk
940-
)
9411036
phases_data["participant_team"] = participant_team.team_name
9421037
phases_data["participant_team_id"] = participant_team.id
9431038
return Response(phases_data, status=status.HTTP_200_OK)

0 commit comments

Comments
 (0)