diff --git a/apps/challenges/views.py b/apps/challenges/views.py index ce61e8513a..5635a09e38 100644 --- a/apps/challenges/views.py +++ b/apps/challenges/views.py @@ -56,7 +56,7 @@ from django.core.files.base import ContentFile from django.core.files.uploadedfile import SimpleUploadedFile from django.db import transaction -from django.db.models import Prefetch +from django.db.models import Case, Count, Prefetch, Q, When from django.http import HttpResponse from django.utils import timezone from drf_spectacular.utils import ( @@ -840,31 +840,45 @@ def get_all_challenges( def get_all_challenges_submission_metrics(request): """ Returns the submission metrics for all challenges and their phases + + This function optimizes database queries by using aggregation to count + submissions in a single query rather than looping through challenges. """ if not is_user_a_staff(request.user): response_data = { "error": "Sorry, you are not authorized to make this request" } return Response(response_data, status=status.HTTP_403_FORBIDDEN) - challenges = Challenge.objects.all() - submission_metrics = {} submission_statuses = [status[0] for status in Submission.STATUS_OPTIONS] - for challenge in challenges: - challenge_id = challenge.id - challenge_metrics = {} + # Get all challenge IDs + challenge_ids = Challenge.objects.values_list('id', flat=True) - # Fetch challenge phases for the challenge - challenge_phases = ChallengePhase.objects.filter(challenge=challenge) + # Initialize metrics dictionary with all challenges and zero counts + submission_metrics = {} + for challenge_id in challenge_ids: + submission_metrics[challenge_id] = { + status_choice: 0 for status_choice in submission_statuses + } - for submission_status in submission_statuses: - count = Submission.objects.filter( - challenge_phase__in=challenge_phases, status=submission_status - ).count() - challenge_metrics[submission_status] = count + # Use aggregation to count submissions by challenge and status in a single query + # Group submissions by their challenge phase's challenge and by status + submission_counts = ( + Submission.objects + .select_related('challenge_phase__challenge') + .values('challenge_phase__challenge_id', 'status') + .annotate(count=Count('id')) + ) + + # Populate the metrics dictionary with actual counts + for item in submission_counts: + challenge_id = item['challenge_phase__challenge_id'] + submission_status = item['status'] + count = item['count'] - submission_metrics[challenge_id] = challenge_metrics + if challenge_id in submission_metrics: + submission_metrics[challenge_id][submission_status] = count return Response(submission_metrics, status=status.HTTP_200_OK) diff --git a/apps/hosts/views.py b/apps/hosts/views.py index c6e9cf48de..0a40e5f780 100644 --- a/apps/hosts/views.py +++ b/apps/hosts/views.py @@ -247,16 +247,17 @@ def remove_self_from_challenge_host_team(request, challenge_host_team_pk): except ChallengeHostTeam.DoesNotExist: response_data = {"error": "ChallengeHostTeam does not exist"} return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) - try: - challenge_host = ChallengeHost.objects.filter( - user=request.user.id, team_name__pk=challenge_host_team_pk - ) - challenge_host.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except: # noqa E722 + + challenge_host = ChallengeHost.objects.filter( + user=request.user.id, team_name__pk=challenge_host_team_pk + ) + if not challenge_host.exists(): response_data = {"error": "Sorry, you do not belong to this team."} return Response(response_data, status=status.HTTP_401_UNAUTHORIZED) + challenge_host.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + @api_view(["POST"]) @throttle_classes([UserRateThrottle]) diff --git a/apps/participants/views.py b/apps/participants/views.py index a7b6aa55fc..940c1f8eb8 100644 --- a/apps/participants/views.py +++ b/apps/participants/views.py @@ -137,6 +137,20 @@ def get_participant_team_challenge_list(request, participant_team_pk): @permission_classes((permissions.IsAuthenticated, HasVerifiedEmail)) @authentication_classes((JWTAuthentication, ExpiringTokenAuthentication)) def participant_team_detail(request, pk): + """ + API endpoint to retrieve or update participant team details. + + GET: Returns team details if the user is a team member + PUT: Updates team details (full update) + PATCH: Partially updates team details (only team creator can do this) + + Args: + request: Django REST framework request object + pk: Primary key of the participant team + + Returns: + Response with team details or error message + """ try: participant_team = ParticipantTeam.objects.get(pk=pk) @@ -196,6 +210,25 @@ def participant_team_detail(request, pk): @permission_classes((permissions.IsAuthenticated, HasVerifiedEmail)) @authentication_classes((JWTAuthentication, ExpiringTokenAuthentication)) def invite_participant_to_team(request, pk): + """ + API endpoint to invite a user to join a participant team. + + Validates that: + - The participant team exists + - The requesting user is a member of the team + - The invited user exists + - The invited user is not already part of the team + - The invited user hasn't participated in challenges where the team has + - Neither the team nor invited user are banned from team's challenges + - Email domain restrictions (allowed/blocked) are respected + + Args: + request: Django REST framework request object + pk: Primary key of the participant team + + Returns: + Response with success message or error details + """ try: participant_team = ParticipantTeam.objects.get(pk=pk) except ParticipantTeam.DoesNotExist: @@ -246,9 +279,10 @@ def invite_participant_to_team(request, pk): return Response(response_data, status=status.HTTP_406_NOT_ACCEPTABLE) if len(team_participated_challenges) > 0: - for challenge_pk in team_participated_challenges: - challenge = get_challenge_model(challenge_pk) + # Fetch all challenges in a single query to avoid N+1 problem + challenges = Challenge.objects.filter(pk__in=team_participated_challenges) + for challenge in challenges: if len(challenge.banned_email_ids) > 0: # Check if team participants emails are banned for ( @@ -281,7 +315,7 @@ def invite_participant_to_team(request, pk): # Check if user is in allowed list. if len(challenge.allowed_email_domains) > 0: - if not is_user_in_allowed_email_domains(email, challenge_pk): + if not is_user_in_allowed_email_domains(email, challenge.pk): message = "Sorry, users with {} email domain(s) are only allowed to participate in this challenge." domains = "" for domain in challenge.allowed_email_domains: @@ -293,7 +327,7 @@ def invite_participant_to_team(request, pk): ) # Check if user is in blocked list. - if is_user_in_blocked_email_domains(email, challenge_pk): + if is_user_in_blocked_email_domains(email, challenge.pk): message = "Sorry, users with {} email domain(s) are not allowed to participate in this challenge." domains = "" for domain in challenge.blocked_email_domains: diff --git a/setup.cfg b/setup.cfg index 1913d9d9a2..bd0748db05 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,6 +2,6 @@ max-line-length = 120 exclude = .git,*/migrations/*,*/static/CACHE/*,docs/,env/,fabfile/,node_modules/,bower_components/ -[pytest] +[tool:pytest] DJANGO_SETTINGS_MODULE = settings.test norecursedirs = .git */migrations/* */static/* docs env