Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 28 additions & 14 deletions apps/challenges/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added import brings in Case, Q, and When, but they do not appear to be used in this module (only Count is used for the new aggregation). This will trigger unused-import lint errors (e.g., flake8 F401). Please remove the unused imports or use them if needed.

Suggested change
from django.db.models import Case, Count, Prefetch, Q, When
from django.db.models import Count, Prefetch

Copilot uses AI. Check for mistakes.
from django.http import HttpResponse
from django.utils import timezone
from drf_spectacular.utils import (
Expand Down Expand Up @@ -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')
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

select_related('challenge_phase__challenge') is ineffective when followed by .values(...).annotate(...) (no model instances are materialized), and can add confusion. Consider removing select_related here to keep the query intent clear.

Suggested change
.select_related('challenge_phase__challenge')

Copilot uses AI. Check for mistakes.
.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)

Expand Down
15 changes: 8 additions & 7 deletions apps/hosts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
42 changes: 38 additions & 4 deletions apps/participants/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Comment on lines 281 to +285
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

team_participated_challenges is a queryset; len(team_participated_challenges) forces a DB query, and then Challenge.objects.filter(pk__in=team_participated_challenges) triggers another query. To keep the intended optimization, avoid len() here (e.g., just iterate for challenge in challenges without the guard, or use .exists() on the queryset you actually plan to iterate).

Copilot uses AI. Check for mistakes.
if len(challenge.banned_email_ids) > 0:
# Check if team participants emails are banned
for (
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +5 to 7
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setup.cfg now contains a [tool:pytest] section, but the repo already has pytest.ini with a [pytest] section. Pytest will typically prefer pytest.ini, so these settings may be ignored or become divergent over time. Consider consolidating pytest configuration into a single file (either move everything to pytest.ini or remove pytest.ini and ensure setup.cfg contains the full set of options like python_files, DJANGO_SERVER, etc.).

Copilot uses AI. Check for mistakes.