Skip to content

Commit 20f305a

Browse files
authored
Introduce an option for the leaderboard endpoint to include all contest members (#120)
1 parent c0a265c commit 20f305a

6 files changed

Lines changed: 108 additions & 82 deletions

File tree

apps/backend/src/routes/contest/ContestHandler.ts

Lines changed: 92 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { generateDocument } from "../../lib/document";
3333
import { generateSnowflake } from "../../lib/snowflake";
3434
import { useValidation } from "../../middlewares/useValidation";
3535
import {
36+
hasContestPermission as requestHasContestPermission,
3637
hasOrganisationPermission,
3738
mustHaveContestPermission,
3839
mustHaveCurrentOrganisationPermission,
@@ -430,88 +431,106 @@ ContestHandler.get("/members/self", async (req, res) => {
430431
);
431432
});
432433

433-
ContestHandler.get("/:contest_id/leaderboard", async (req, res) => {
434-
const contest = await extractContest(req);
434+
const LeaderboardQuerySchema = Type.Object({
435+
show_all_users: Type.Optional(Type.Union([Type.Literal("true"), Type.Literal("false")])),
436+
});
435437

436-
if (
437-
(!contest.show_leaderboard_during_contest || !isContestRunning(contest)) &&
438-
!isContestOver(contest)
439-
) {
440-
await mustHaveContestPermission(req, ContestMemberPermissions.VIEW_PRIVATE, contest.id);
441-
}
438+
ContestHandler.get(
439+
"/:contest_id/leaderboard",
440+
useValidation(LeaderboardQuerySchema, { query: true }),
441+
async (req, res) => {
442+
const contest = await extractContest(req);
442443

443-
const _contestMembers = await Database.selectFrom("contest_members", "*", {
444-
contest_id: contest.id,
445-
});
446-
447-
const users = (
448-
await Promise.all(
449-
R.chunk(_contestMembers, 100).map((chunk) => {
450-
return Database.selectFrom("users", "*", {
451-
id: eqIn(...chunk.map((it) => it.user_id)),
452-
});
453-
})
454-
)
455-
).flat();
444+
if (
445+
(!contest.show_leaderboard_during_contest || !isContestRunning(contest)) &&
446+
!isContestOver(contest)
447+
) {
448+
await mustHaveContestPermission(req, ContestMemberPermissions.VIEW_PRIVATE, contest.id);
449+
}
456450

457-
// if for every contestMember doesn't exist a corresponding user
458-
if (!_contestMembers.every((it) => users.some((user) => user.id === it.user_id)))
459-
throw new SafeError(StatusCodes.INTERNAL_SERVER_ERROR);
451+
const _contestMembers = await Database.selectFrom("contest_members", "*", {
452+
contest_id: contest.id,
453+
});
460454

461-
const contestMembers = _contestMembers.filter(
462-
(it, _, __, user = users.find((user) => user.id === it.user_id)!) =>
463-
!hasContestPermission(
464-
it.contest_permissions,
465-
ContestMemberPermissions.VIEW_PRIVATE,
466-
user.permissions
455+
const users = (
456+
await Promise.all(
457+
R.chunk(_contestMembers, 100).map((chunk) => {
458+
return Database.selectFrom("users", "*", {
459+
id: eqIn(...chunk.map((it) => it.user_id)),
460+
});
461+
})
467462
)
468-
);
463+
).flat();
469464

470-
const eduUsers = (
471-
await Promise.all(
472-
R.chunk(contestMembers, 100).map((chunk) => {
473-
return Database.selectFrom("edu_users", "*", {
474-
id: eqIn(...chunk.map((it) => it.user_id)),
475-
});
476-
})
477-
)
478-
).flat();
465+
// if for every contestMember doesn't exist a corresponding user
466+
if (!_contestMembers.every((it) => users.some((user) => user.id === it.user_id)))
467+
throw new SafeError(StatusCodes.INTERNAL_SERVER_ERROR);
479468

480-
const organisationMembers = await Database.selectFrom(
481-
"organisation_members",
482-
"*",
483-
{
484-
organisation_id: contest.organisation_id,
485-
},
486-
"ALLOW FILTERING"
487-
);
488-
489-
return respond(
490-
res,
491-
StatusCodes.OK,
492-
contestMembers
493-
.map(
494-
(
495-
it,
496-
_,
497-
__,
498-
user = users.find((user) => user.id === it.user_id)!,
499-
eduUser = eduUsers.find((user) => user.id === it.user_id)
500-
) => ({
501-
...it,
502-
...R.pick(
503-
organisationMembers.find((member) => member.user_id === it.user_id)!,
504-
["elo"]
505-
),
506-
full_name:
507-
(contest.require_edu_verification && eduUser?.full_name) || user.full_name,
508-
email_domain: user.email.split("@").at(-1),
509-
edu_mail_domain: eduUser?.email.split("@").at(-1),
469+
const showAll =
470+
req.query.show_all_users === "true" &&
471+
(await requestHasContestPermission(
472+
req,
473+
ContestMemberPermissions.VIEW_PRIVATE,
474+
contest.id
475+
).catch(() => false));
476+
477+
const contestMembers = _contestMembers.filter(
478+
(it, _, __, user = users.find((user) => user.id === it.user_id)!) =>
479+
showAll ||
480+
!hasContestPermission(
481+
it.contest_permissions,
482+
ContestMemberPermissions.VIEW_PRIVATE,
483+
user.permissions
484+
)
485+
);
486+
487+
const eduUsers = (
488+
await Promise.all(
489+
R.chunk(contestMembers, 100).map((chunk) => {
490+
return Database.selectFrom("edu_users", "*", {
491+
id: eqIn(...chunk.map((it) => it.user_id)),
492+
});
510493
})
511494
)
512-
.map((it) => ({ ...it, score: it.score ?? {} }))
513-
);
514-
});
495+
).flat();
496+
497+
const organisationMembers = await Database.selectFrom(
498+
"organisation_members",
499+
"*",
500+
{
501+
organisation_id: contest.organisation_id,
502+
},
503+
"ALLOW FILTERING"
504+
);
505+
506+
return respond(
507+
res,
508+
StatusCodes.OK,
509+
contestMembers
510+
.map(
511+
(
512+
it,
513+
_,
514+
__,
515+
user = users.find((user) => user.id === it.user_id)!,
516+
eduUser = eduUsers.find((user) => user.id === it.user_id)
517+
) => ({
518+
...it,
519+
...R.pick(
520+
organisationMembers.find((member) => member.user_id === it.user_id)!,
521+
["elo"]
522+
),
523+
full_name:
524+
(contest.require_edu_verification && eduUser?.full_name) ||
525+
user.full_name,
526+
email_domain: user.email.split("@").at(-1),
527+
edu_mail_domain: eduUser?.email.split("@").at(-1),
528+
})
529+
)
530+
.map((it) => ({ ...it, score: it.score ?? {} }))
531+
);
532+
}
533+
);
515534

516535
ContestHandler.get("/:contest_id", async (req, res) => {
517536
const contest = await extractContest(req);

apps/frontend/src/hooks/contest/participants/useAllContestMembers.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,19 @@ import { useQuery } from "react-query";
33

44
import { http, QueryHandler, wrapAxios } from "../../../api/http";
55

6-
export const useAllContestMembers: QueryHandler<ContestMemberWithInfo[], Snowflake> = (
7-
contest_id,
8-
options
9-
) =>
6+
export const useAllContestMembers: QueryHandler<
7+
ContestMemberWithInfo[],
8+
[Snowflake, { showAllUsers?: boolean }]
9+
> = ([contest_id, leaderboardOptions], options) =>
1010
useQuery({
1111
queryKey: ["contests", contest_id, "members"],
12-
queryFn: () => wrapAxios(http.get(`/contest/${contest_id}/leaderboard`)),
12+
queryFn: () =>
13+
wrapAxios(
14+
http.get(
15+
`/contest/${contest_id}/leaderboard?show_all_users=${
16+
leaderboardOptions.showAllUsers ?? false
17+
}`
18+
)
19+
),
1320
...options,
1421
});

apps/frontend/src/pages/contests/Leaderboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const Leaderboard: FC<Properties> = ({ contest, problems }) => {
2828
[contest, contestEnded, user]
2929
);
3030

31-
const { isSuccess, data } = useAllContestMembers(contest.id, {
31+
const { isSuccess, data } = useAllContestMembers([contest.id, {}], {
3232
enabled: leaderboardVisible,
3333
});
3434

apps/frontend/src/pages/management/contest/overview/ContestOverviewPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export const ContestOverviewPage: FC = () => {
9696

9797
// I guess we could make a route to get this info without getting all data, but it should be fine
9898
const questions = useAllContestQuestions(contest.id);
99-
const members = useAllContestMembers(contest.id);
99+
const members = useAllContestMembers([contest.id, {}]);
100100
const announcements = useAllContestAnnouncements(contest.id);
101101

102102
const { t } = useTranslation();

apps/frontend/src/pages/management/contest/participants/ContestParticipantsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const AddParticipantSchema = z.object({
144144
export const ContestParticipantsPage: FC = () => {
145145
const { contest } = useContestContext();
146146

147-
const { data: members } = useAllContestMembers(contest.id);
147+
const { data: members } = useAllContestMembers([contest.id, { showAllUsers: true }]);
148148

149149
const addMutation = useAddParticipant(contest.id);
150150

apps/frontend/src/pages/management/contest/results/ContestResultsPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const calculateGradeFromScale = (
3636
export const ContestResultsPage: FC = () => {
3737
const { contest } = useContestContext();
3838

39-
const { data: members } = useAllContestMembers(contest.id);
39+
const { data: members } = useAllContestMembers([contest.id, {}]);
4040
const { data: problems } = useAllProblems(contest.id);
4141
const { data: gradingScales } = useAllContestGradingScales(contest.id);
4242

0 commit comments

Comments
 (0)