Skip to content

Commit 2dbd315

Browse files
Fixes for Tourney Admin (#923)
Co-authored-by: Nour Massri <[email protected]>
1 parent 4eada47 commit 2dbd315

File tree

9 files changed

+162
-57
lines changed

9 files changed

+162
-57
lines changed

backend/siarnaq/api/compete/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,9 @@ def to_representation(self, instance):
282282
)
283283
or instance.participants.filter(team__status=TeamStatus.INVISIBLE).exists()
284284
):
285+
# TODO: Not sure why removing this doesn't work(shows hidden matches)
286+
# but need to ship the PR
287+
285288
# Fully redact matches from private tournaments, unreleased tournament
286289
# rounds, and those with invisible teams.
287290
data["participants"] = data["replay_url"] = data["maps"] = None

backend/siarnaq/api/compete/test_views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def test_admin_has_staff_team_tournament_hidden(self):
408408
"tournament": self.r_hidden.tournament.pk,
409409
"external_id": self.r_hidden.external_id,
410410
"name": self.r_hidden.name,
411-
"maps": None,
411+
"maps": [self.map.pk],
412412
"release_status": self.r_hidden.release_status,
413413
"display_order": self.r_hidden.display_order,
414414
"in_progress": self.r_hidden.in_progress,

backend/siarnaq/api/compete/views.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -266,23 +266,25 @@ class MatchViewSet(
266266
serializer_class = MatchSerializer
267267
permission_classes = (IsEpisodeMutable | IsAdminUser,)
268268

269-
def get_queryset(self):
269+
def get_queryset(self, prefetch_related=True):
270270
queryset = (
271271
Match.objects.filter(episode=self.kwargs["episode_id"])
272272
.select_related("tournament_round__tournament")
273-
.prefetch_related(
273+
.order_by("-pk")
274+
)
275+
276+
# Only prefetch rating and team data if needed
277+
if prefetch_related:
278+
queryset = queryset.prefetch_related(
274279
"participants__previous_participation__rating",
275280
"participants__rating",
276281
"participants__team__profile__rating",
277282
"participants__team__members",
278283
"maps",
279284
)
280-
.order_by("-pk")
281-
)
282285

283-
# Check if the user is not staff
286+
# Exclude matches where tournament round is not null and not released
284287
if not self.request.user.is_staff:
285-
# Exclude matches where tournament round is not null and not released
286288
queryset = (
287289
queryset.exclude(
288290
Q(tournament_round__isnull=False)
@@ -347,12 +349,12 @@ def tournament(self, request, *, episode_id):
347349
Passing the external_id_private of a tournament allows match lookup for the
348350
tournament, even if it's private. Client uses the external_id_private parameter
349351
"""
352+
# Get the matches for the episode, excluding those that should not be shown
350353
queryset = (
351-
Match.objects.filter(episode=self.kwargs["episode_id"]).select_related(
352-
"tournament_round__tournament"
353-
)
354-
).order_by("-pk")
355-
354+
Match.objects.filter(episode=self.kwargs["episode_id"])
355+
.select_related("tournament_round__tournament")
356+
.order_by("-pk")
357+
)
356358
external_id_private = self.request.query_params.get("external_id_private")
357359
tournaments = None
358360
if external_id_private is not None:

backend/siarnaq/api/episodes/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,21 @@ class TournamentAdmin(admin.ModelAdmin):
187187
"display_date",
188188
"submission_freeze",
189189
"is_public",
190+
"private_challonge_link",
190191
)
191192
list_filter = ("episode",)
192193
list_select_related = ("episode",)
193194
ordering = ("-episode__game_release", "-submission_freeze")
194195
search_fields = ("name_short", "name_long")
195196
search_help_text = "Search for a full or abbreviated name."
196197

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

198206
class MatchInline(admin.TabularInline):
199207
model = Match

backend/siarnaq/api/episodes/models.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,34 @@ def enqueue(self):
414414
match_participant_objects
415415
)
416416

417+
# IMPORTANT: bulk create does not respect the map ordering so we reset it here
418+
tournament_round_maps = self.maps.all()
419+
map_order = {map.id: index for index, map in enumerate(tournament_round_maps)}
420+
421+
# Prepare bulk update data
422+
through_model = matches[0].maps.through
423+
bulk_updates = []
424+
425+
with transaction.atomic():
426+
for match in matches:
427+
match_maps = match.maps.all()
428+
for map in match_maps:
429+
if map.id in map_order:
430+
bulk_updates.append(
431+
through_model(
432+
match_id=match.id,
433+
map_id=map.id,
434+
sort_value=map_order[map.id],
435+
)
436+
)
437+
438+
# Delete existing relationships
439+
through_model.objects.filter(match__in=matches).delete()
440+
441+
# Bulk create new relationships with correct order
442+
through_model.objects.bulk_create(bulk_updates)
443+
444+
# Enqueue the matches
417445
Match.objects.filter(pk__in=[match.pk for match in matches]).enqueue()
418446

419447
self.in_progress = True

backend/siarnaq/api/episodes/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class Meta:
106106

107107
def to_representation(self, instance):
108108
data = super().to_representation(instance)
109+
# If user is staff, do not redact anything
110+
if self.context["user_is_staff"]:
111+
return data
109112
# Redact maps if not yet fully released
110113
if instance.release_status != ReleaseStatus.RESULTS:
111114
data["maps"] = None

backend/siarnaq/api/episodes/views.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
44
from rest_framework import status, viewsets
55
from rest_framework.decorators import action
6+
from rest_framework.exceptions import APIException
67
from rest_framework.permissions import AllowAny, IsAdminUser
78
from rest_framework.response import Response
89

@@ -18,6 +19,18 @@
1819
)
1920

2021

22+
class NoMatchesToRequeue(APIException):
23+
status_code = status.HTTP_400_BAD_REQUEST
24+
default_detail = "No failed matches to requeue"
25+
default_code = "no_failed_matches"
26+
27+
28+
class RoundInProgress(APIException):
29+
status_code = status.HTTP_409_CONFLICT
30+
default_detail = "Round is already in progress"
31+
default_code = "round_in_progress"
32+
33+
2134
class EpisodeViewSet(viewsets.ReadOnlyModelViewSet):
2235
"""
2336
A viewset for retrieving Episodes.
@@ -197,8 +210,10 @@ def enqueue(self, request, pk=None, *, episode_id, tournament):
197210

198211
# Set the tournament round's maps to the provided list
199212
old_maps = instance.maps.all()
213+
200214
map_ids = self.request.query_params.getlist("maps")
201-
maps = Map.objects.filter(episode_id=episode_id, id__in=map_ids)
215+
map_objs = Map.objects.filter(episode_id=episode_id, id__in=map_ids)
216+
maps = sorted(list(map_objs), key=lambda m: map_ids.index(str(m.id)))
202217

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

217232
return Response(None, status=status.HTTP_204_NO_CONTENT)
218233

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

295310
if not failed.exists():
296-
return Response(None, status=status.HTTP_400_BAD_REQUEST)
311+
raise NoMatchesToRequeue()
297312

298313
# TODO: should we logger.info the round requeue here?
299314
failed.enqueue_all()

frontend/src/api/episode/useEpisode.ts

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,10 @@ export const useCreateAndEnqueueMatches = (
231231
maps,
232232
}: EpisodeTournamentRoundEnqueueCreateRequest) => {
233233
const toastFn = async (): Promise<void> => {
234-
await createAndEnqueueMatches({ episodeId, tournament, id, maps });
235-
236-
// Refetch this tournament round and its matches
237234
try {
235+
await createAndEnqueueMatches({ episodeId, tournament, id, maps });
236+
237+
// Refetch this tournament round and its matches
238238
const roundInfo = queryClient.refetchQueries({
239239
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
240240
episodeId,
@@ -248,31 +248,33 @@ export const useCreateAndEnqueueMatches = (
248248
});
249249

250250
await Promise.all([roundInfo, tourneyMatches]);
251-
} catch (e) {
252-
toast.error((e as ResponseError).message);
251+
} catch (e: unknown) {
252+
const error = e as ResponseError;
253+
// Parse the response text as JSON, detail propety contains the error message
254+
const errorJson = (await error.response.json()) as {
255+
detail?: string;
256+
};
257+
const errorDetail =
258+
errorJson.detail ?? "An unexpected error occurred.";
259+
throw new Error(errorDetail);
253260
}
254261
};
255262

256263
await toast.promise(toastFn(), {
257264
loading: "Creating and enqueuing matches...",
258265
success: "Matches created and enqueued!",
259-
error: "Error creating and enqueuing matches.",
266+
error: (error: Error) => error.message, // Return the error message thrown in toastFn
260267
});
261268
},
262269
});
263270

264271
/**
265272
* For releasing the given tournament round to the bracket service.
266273
*/
267-
export const useReleaseTournamentRound = ({
268-
episodeId,
269-
tournament,
270-
id,
271-
}: EpisodeTournamentRoundReleaseCreateRequest): UseMutationResult<
272-
void,
273-
Error,
274-
EpisodeTournamentRoundReleaseCreateRequest
275-
> =>
274+
export const useReleaseTournamentRound = (
275+
{ episodeId, tournament, id }: EpisodeTournamentRoundReleaseCreateRequest,
276+
queryClient: QueryClient,
277+
): UseMutationResult<void, Error, EpisodeTournamentRoundReleaseCreateRequest> =>
276278
useMutation({
277279
mutationKey: episodeMutationKeys.releaseTournamentRound({
278280
episodeId,
@@ -285,26 +287,40 @@ export const useReleaseTournamentRound = ({
285287
id,
286288
}: EpisodeTournamentRoundReleaseCreateRequest) => {
287289
const toastFn = async (): Promise<void> => {
288-
await releaseTournamentRound({ episodeId, tournament, id });
290+
try {
291+
await releaseTournamentRound({ episodeId, tournament, id });
292+
293+
await queryClient.refetchQueries({
294+
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
295+
episodeId,
296+
tournament,
297+
id,
298+
}),
299+
});
300+
} catch (e: unknown) {
301+
const error = e as ResponseError;
302+
// Parse the response text as JSON, detail propety contains the error message
303+
const errorJson = (await error.response.json()) as {
304+
detail?: string;
305+
};
306+
const errorDetail =
307+
errorJson.detail ?? "An unexpected error occurred.";
308+
throw new Error(errorDetail);
309+
}
289310
};
290311

291312
await toast.promise(toastFn(), {
292313
loading: "Initiating round release...",
293314
success: "Round release initiated!",
294-
error: "Error releasing tournament round.",
315+
error: (error: Error) => error.message, // Return the error message thrown in toastFn
295316
});
296317
},
297318
});
298319

299-
export const useRequeueTournamentRound = ({
300-
episodeId,
301-
tournament,
302-
id,
303-
}: EpisodeTournamentRoundRequeueCreateRequest): UseMutationResult<
304-
void,
305-
Error,
306-
EpisodeTournamentRoundRequeueCreateRequest
307-
> =>
320+
export const useRequeueTournamentRound = (
321+
{ episodeId, tournament, id }: EpisodeTournamentRoundRequeueCreateRequest,
322+
queryClient: QueryClient,
323+
): UseMutationResult<void, Error, EpisodeTournamentRoundRequeueCreateRequest> =>
308324
useMutation({
309325
mutationKey: episodeMutationKeys.requeueTournamentRound({
310326
episodeId,
@@ -317,15 +333,39 @@ export const useRequeueTournamentRound = ({
317333
id,
318334
}: EpisodeTournamentRoundRequeueCreateRequest) => {
319335
const toastFn = async (): Promise<void> => {
320-
await requeueTournamentRound({ episodeId, tournament, id });
336+
try {
337+
await requeueTournamentRound({ episodeId, tournament, id });
338+
339+
// Refetch this tournament round and its matches
340+
const roundInfo = queryClient.refetchQueries({
341+
queryKey: buildKey(episodeQueryKeys.tournamentRoundInfo, {
342+
episodeId,
343+
tournament,
344+
id,
345+
}),
346+
});
321347

322-
// TODO: refetch the round and its matches :)
348+
const tourneyMatches = queryClient.refetchQueries({
349+
queryKey: buildKey(competeQueryKeys.matchBase, { episodeId }),
350+
});
351+
352+
await Promise.all([roundInfo, tourneyMatches]);
353+
} catch (e: unknown) {
354+
const error = e as ResponseError;
355+
// Parse the response text as JSON, detail propety contains the error message
356+
const errorJson = (await error.response.json()) as {
357+
detail?: string;
358+
};
359+
const errorDetail =
360+
errorJson.detail ?? "An unexpected error occurred.";
361+
throw new Error(errorDetail);
362+
}
323363
};
324364

325365
await toast.promise(toastFn(), {
326366
loading: "Initiating round requeue...",
327367
success: "Failed matches requeued!",
328-
error: "Error requeueing tournament round.",
368+
error: (error: Error) => error.message, // Return the error message thrown in toastFn
329369
});
330370
},
331371
});

0 commit comments

Comments
 (0)