Skip to content

Commit

Permalink
Fixes for Tourney Admin (#923)
Browse files Browse the repository at this point in the history
Co-authored-by: Nour Massri <mnmassri@mit.edu>
  • Loading branch information
lowtorola and nour-massri authored Jan 24, 2025
1 parent 4eada47 commit 2dbd315
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 57 deletions.
3 changes: 3 additions & 0 deletions backend/siarnaq/api/compete/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@ def to_representation(self, instance):
)
or instance.participants.filter(team__status=TeamStatus.INVISIBLE).exists()
):
# TODO: Not sure why removing this doesn't work(shows hidden matches)
# but need to ship the PR

# Fully redact matches from private tournaments, unreleased tournament
# rounds, and those with invisible teams.
data["participants"] = data["replay_url"] = data["maps"] = None
Expand Down
2 changes: 1 addition & 1 deletion backend/siarnaq/api/compete/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def test_admin_has_staff_team_tournament_hidden(self):
"tournament": self.r_hidden.tournament.pk,
"external_id": self.r_hidden.external_id,
"name": self.r_hidden.name,
"maps": None,
"maps": [self.map.pk],
"release_status": self.r_hidden.release_status,
"display_order": self.r_hidden.display_order,
"in_progress": self.r_hidden.in_progress,
Expand Down
24 changes: 13 additions & 11 deletions backend/siarnaq/api/compete/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,23 +266,25 @@ class MatchViewSet(
serializer_class = MatchSerializer
permission_classes = (IsEpisodeMutable | IsAdminUser,)

def get_queryset(self):
def get_queryset(self, prefetch_related=True):
queryset = (
Match.objects.filter(episode=self.kwargs["episode_id"])
.select_related("tournament_round__tournament")
.prefetch_related(
.order_by("-pk")
)

# Only prefetch rating and team data if needed
if prefetch_related:
queryset = queryset.prefetch_related(
"participants__previous_participation__rating",
"participants__rating",
"participants__team__profile__rating",
"participants__team__members",
"maps",
)
.order_by("-pk")
)

# Check if the user is not staff
# Exclude matches where tournament round is not null and not released
if not self.request.user.is_staff:
# Exclude matches where tournament round is not null and not released
queryset = (
queryset.exclude(
Q(tournament_round__isnull=False)
Expand Down Expand Up @@ -347,12 +349,12 @@ def tournament(self, request, *, episode_id):
Passing the external_id_private of a tournament allows match lookup for the
tournament, even if it's private. Client uses the external_id_private parameter
"""
# Get the matches for the episode, excluding those that should not be shown
queryset = (
Match.objects.filter(episode=self.kwargs["episode_id"]).select_related(
"tournament_round__tournament"
)
).order_by("-pk")

Match.objects.filter(episode=self.kwargs["episode_id"])
.select_related("tournament_round__tournament")
.order_by("-pk")
)
external_id_private = self.request.query_params.get("external_id_private")
tournaments = None
if external_id_private is not None:
Expand Down
8 changes: 8 additions & 0 deletions backend/siarnaq/api/episodes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,21 @@ class TournamentAdmin(admin.ModelAdmin):
"display_date",
"submission_freeze",
"is_public",
"private_challonge_link",
)
list_filter = ("episode",)
list_select_related = ("episode",)
ordering = ("-episode__game_release", "-submission_freeze")
search_fields = ("name_short", "name_long")
search_help_text = "Search for a full or abbreviated name."

def private_challonge_link(self, obj):
"""Generate a link to the private Challonge bracket."""
link = f"https://challonge.com/{obj.external_id_private}"
return format_html(
'<a href="{}" target="_blank">Private Challonge Link</a>', link
)


class MatchInline(admin.TabularInline):
model = Match
Expand Down
28 changes: 28 additions & 0 deletions backend/siarnaq/api/episodes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,34 @@ def enqueue(self):
match_participant_objects
)

# IMPORTANT: bulk create does not respect the map ordering so we reset it here
tournament_round_maps = self.maps.all()
map_order = {map.id: index for index, map in enumerate(tournament_round_maps)}

# Prepare bulk update data
through_model = matches[0].maps.through
bulk_updates = []

with transaction.atomic():
for match in matches:
match_maps = match.maps.all()
for map in match_maps:
if map.id in map_order:
bulk_updates.append(
through_model(
match_id=match.id,
map_id=map.id,
sort_value=map_order[map.id],
)
)

# Delete existing relationships
through_model.objects.filter(match__in=matches).delete()

# Bulk create new relationships with correct order
through_model.objects.bulk_create(bulk_updates)

# Enqueue the matches
Match.objects.filter(pk__in=[match.pk for match in matches]).enqueue()

self.in_progress = True
Expand Down
3 changes: 3 additions & 0 deletions backend/siarnaq/api/episodes/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ class Meta:

def to_representation(self, instance):
data = super().to_representation(instance)
# If user is staff, do not redact anything
if self.context["user_is_staff"]:
return data
# Redact maps if not yet fully released
if instance.release_status != ReleaseStatus.RESULTS:
data["maps"] = None
Expand Down
23 changes: 19 additions & 4 deletions backend/siarnaq/api/episodes/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import APIException
from rest_framework.permissions import AllowAny, IsAdminUser
from rest_framework.response import Response

Expand All @@ -18,6 +19,18 @@
)


class NoMatchesToRequeue(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = "No failed matches to requeue"
default_code = "no_failed_matches"


class RoundInProgress(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = "Round is already in progress"
default_code = "round_in_progress"


class EpisodeViewSet(viewsets.ReadOnlyModelViewSet):
"""
A viewset for retrieving Episodes.
Expand Down Expand Up @@ -197,8 +210,10 @@ def enqueue(self, request, pk=None, *, episode_id, tournament):

# Set the tournament round's maps to the provided list
old_maps = instance.maps.all()

map_ids = self.request.query_params.getlist("maps")
maps = Map.objects.filter(episode_id=episode_id, id__in=map_ids)
map_objs = Map.objects.filter(episode_id=episode_id, id__in=map_ids)
maps = sorted(list(map_objs), key=lambda m: map_ids.index(str(m.id)))

# We require an odd number of maps to prevent ties
if len(maps) % 2 == 0:
Expand All @@ -209,10 +224,10 @@ def enqueue(self, request, pk=None, *, episode_id, tournament):
# Attempt to enqueue the round
try:
instance.enqueue()
except RuntimeError as e:
except RuntimeError:
# Revert the maps if enqueueing failed
instance.maps.set(old_maps)
return Response(str(e), status=status.HTTP_409_CONFLICT)
raise RoundInProgress()

return Response(None, status=status.HTTP_204_NO_CONTENT)

Expand Down Expand Up @@ -293,7 +308,7 @@ def requeue(self, request, pk=None, *, episode_id, tournament):
)

if not failed.exists():
return Response(None, status=status.HTTP_400_BAD_REQUEST)
raise NoMatchesToRequeue()

# TODO: should we logger.info the round requeue here?
failed.enqueue_all()
Expand Down
98 changes: 69 additions & 29 deletions frontend/src/api/episode/useEpisode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,10 @@ export const useCreateAndEnqueueMatches = (
maps,
}: EpisodeTournamentRoundEnqueueCreateRequest) => {
const toastFn = async (): Promise<void> => {
await createAndEnqueueMatches({ episodeId, tournament, id, maps });

// Refetch this tournament round and its matches
try {
await createAndEnqueueMatches({ episodeId, tournament, id, maps });

// Refetch this tournament round and its matches
const roundInfo = queryClient.refetchQueries({
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
episodeId,
Expand All @@ -248,31 +248,33 @@ export const useCreateAndEnqueueMatches = (
});

await Promise.all([roundInfo, tourneyMatches]);
} catch (e) {
toast.error((e as ResponseError).message);
} catch (e: unknown) {
const error = e as ResponseError;
// Parse the response text as JSON, detail propety contains the error message
const errorJson = (await error.response.json()) as {
detail?: string;
};
const errorDetail =
errorJson.detail ?? "An unexpected error occurred.";
throw new Error(errorDetail);
}
};

await toast.promise(toastFn(), {
loading: "Creating and enqueuing matches...",
success: "Matches created and enqueued!",
error: "Error creating and enqueuing matches.",
error: (error: Error) => error.message, // Return the error message thrown in toastFn
});
},
});

/**
* For releasing the given tournament round to the bracket service.
*/
export const useReleaseTournamentRound = ({
episodeId,
tournament,
id,
}: EpisodeTournamentRoundReleaseCreateRequest): UseMutationResult<
void,
Error,
EpisodeTournamentRoundReleaseCreateRequest
> =>
export const useReleaseTournamentRound = (
{ episodeId, tournament, id }: EpisodeTournamentRoundReleaseCreateRequest,
queryClient: QueryClient,
): UseMutationResult<void, Error, EpisodeTournamentRoundReleaseCreateRequest> =>
useMutation({
mutationKey: episodeMutationKeys.releaseTournamentRound({
episodeId,
Expand All @@ -285,26 +287,40 @@ export const useReleaseTournamentRound = ({
id,
}: EpisodeTournamentRoundReleaseCreateRequest) => {
const toastFn = async (): Promise<void> => {
await releaseTournamentRound({ episodeId, tournament, id });
try {
await releaseTournamentRound({ episodeId, tournament, id });

await queryClient.refetchQueries({
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
episodeId,
tournament,
id,
}),
});
} catch (e: unknown) {
const error = e as ResponseError;
// Parse the response text as JSON, detail propety contains the error message
const errorJson = (await error.response.json()) as {
detail?: string;
};
const errorDetail =
errorJson.detail ?? "An unexpected error occurred.";
throw new Error(errorDetail);
}
};

await toast.promise(toastFn(), {
loading: "Initiating round release...",
success: "Round release initiated!",
error: "Error releasing tournament round.",
error: (error: Error) => error.message, // Return the error message thrown in toastFn
});
},
});

export const useRequeueTournamentRound = ({
episodeId,
tournament,
id,
}: EpisodeTournamentRoundRequeueCreateRequest): UseMutationResult<
void,
Error,
EpisodeTournamentRoundRequeueCreateRequest
> =>
export const useRequeueTournamentRound = (
{ episodeId, tournament, id }: EpisodeTournamentRoundRequeueCreateRequest,
queryClient: QueryClient,
): UseMutationResult<void, Error, EpisodeTournamentRoundRequeueCreateRequest> =>
useMutation({
mutationKey: episodeMutationKeys.requeueTournamentRound({
episodeId,
Expand All @@ -317,15 +333,39 @@ export const useRequeueTournamentRound = ({
id,
}: EpisodeTournamentRoundRequeueCreateRequest) => {
const toastFn = async (): Promise<void> => {
await requeueTournamentRound({ episodeId, tournament, id });
try {
await requeueTournamentRound({ episodeId, tournament, id });

// Refetch this tournament round and its matches
const roundInfo = queryClient.refetchQueries({
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
episodeId,
tournament,
id,
}),
});

// TODO: refetch the round and its matches :)
const tourneyMatches = queryClient.refetchQueries({
queryKey: buildKey(competeQueryKeys.matchBase, { episodeId }),
});

await Promise.all([roundInfo, tourneyMatches]);
} catch (e: unknown) {
const error = e as ResponseError;
// Parse the response text as JSON, detail propety contains the error message
const errorJson = (await error.response.json()) as {
detail?: string;
};
const errorDetail =
errorJson.detail ?? "An unexpected error occurred.";
throw new Error(errorDetail);
}
};

await toast.promise(toastFn(), {
loading: "Initiating round requeue...",
success: "Failed matches requeued!",
error: "Error requeueing tournament round.",
error: (error: Error) => error.message, // Return the error message thrown in toastFn
});
},
});
Loading

0 comments on commit 2dbd315

Please sign in to comment.