Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes for Tourney Admin #923

Merged
merged 3 commits into from
Jan 24, 2025
Merged
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
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
Loading