|
34 | 34 | from django.core.files.base import ContentFile |
35 | 35 | from django.core.files.uploadedfile import SimpleUploadedFile |
36 | 36 | from django.db import IntegrityError, transaction |
37 | | -from django.db.models import Count |
| 37 | +from django.db.models import Count, Q |
38 | 38 | from django.utils import dateparse, timezone |
39 | 39 | from drf_spectacular.utils import ( |
40 | 40 | OpenApiParameter, |
|
64 | 64 | from apps.accounts.authentication import ExpiringTokenAuthentication |
65 | 65 |
|
66 | 66 | from .aws_utils import generate_aws_eks_bearer_token |
| 67 | +from .constants import submission_status_to_exclude |
67 | 68 | from .filters import SubmissionFilter |
68 | 69 | from .models import Submission |
69 | 70 | from .sender import publish_submission_message |
|
77 | 78 | from .utils import ( |
78 | 79 | calculate_distinct_sorted_leaderboard_data, |
79 | 80 | get_leaderboard_data_model, |
80 | | - get_remaining_submission_for_a_phase, |
81 | 81 | get_submission_model, |
82 | 82 | handle_submission_rerun, |
83 | 83 | handle_submission_resume, |
@@ -869,6 +869,73 @@ def get_all_entries_on_public_leaderboard(request, challenge_phase_split_pk): |
869 | 869 | return paginator.get_paginated_response(response_data) |
870 | 870 |
|
871 | 871 |
|
| 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 | + |
872 | 939 | @api_view(["GET"]) |
873 | 940 | @throttle_classes([UserRateThrottle]) |
874 | 941 | @permission_classes((permissions.IsAuthenticated, HasVerifiedEmail)) |
@@ -910,34 +977,62 @@ def get_remaining_submissions(request, challenge_pk): |
910 | 977 | """ |
911 | 978 | phases_data = {} |
912 | 979 | 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 | + |
913 | 988 | challenge_phases = ChallengePhase.objects.filter( |
914 | 989 | challenge=challenge |
915 | 990 | ).order_by("pk") |
916 | 991 | 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 = [] |
921 | 1021 | 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, |
927 | 1028 | ) |
928 | | - if response_status != status.HTTP_200_OK: |
929 | | - return Response( |
930 | | - remaining_submission_message, status=response_status |
931 | | - ) |
932 | 1029 | phase_data_list.append( |
933 | 1030 | RemainingSubmissionDataSerializer( |
934 | | - phase, context={"limits": remaining_submission_message} |
| 1031 | + phase, context={"limits": limits} |
935 | 1032 | ).data |
936 | 1033 | ) |
| 1034 | + |
937 | 1035 | phases_data["phases"] = phase_data_list |
938 | | - participant_team = get_participant_team_of_user_for_a_challenge( |
939 | | - request.user, challenge_pk |
940 | | - ) |
941 | 1036 | phases_data["participant_team"] = participant_team.team_name |
942 | 1037 | phases_data["participant_team_id"] = participant_team.id |
943 | 1038 | return Response(phases_data, status=status.HTTP_200_OK) |
|
0 commit comments