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

feat: leaderboard by track #245

Merged
merged 3 commits into from
Jan 12, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:

- Added the required column `tableId` to the `JudgingResult` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "JudgingResult" ADD COLUMN "tableId" STRING NOT NULL;

-- AddForeignKey
ALTER TABLE "JudgingResult" ADD CONSTRAINT "JudgingResult_tableId_fkey" FOREIGN KEY ("tableId") REFERENCES "Table"("id") ON DELETE CASCADE ON UPDATE CASCADE;
2 changes: 1 addition & 1 deletion prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
# It should be added in your version-control system (i.e. Git)
provider = "cockroachdb"
14 changes: 9 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -268,11 +268,12 @@ model Track {
}

model Table {
id String @id @default(cuid())
number Int @unique
trackId String
track Track @relation(fields: [trackId], references: [id])
TimeSlot TimeSlot[]
id String @id @default(cuid())
number Int @unique
trackId String
track Track @relation(fields: [trackId], references: [id])
TimeSlot TimeSlot[]
JudgingResult JudgingResult[]
}

model Project {
Expand Down Expand Up @@ -311,6 +312,9 @@ model JudgingResult {
responses RubricResponse[]
dhYear String

table Table @relation(fields: [tableId], references: [id], onDelete: Cascade)
tableId String

@@unique([judgeId, projectId])
}

Expand Down
34 changes: 32 additions & 2 deletions src/pages/admin/judging/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ import Drawer from "../../../components/Drawer";
import { getServerAuthSession } from "../../../server/common/get-server-auth-session";
import { rbac } from "../../../components/RBACWrapper";
import { Role } from "@prisma/client";
import { useState } from "react";

const LeaderboardPage: NextPage = () => {
const [selectedTrackId, setSelectedTrackId] = useState<string>("all");

const { data: tracks } = trpc.track.getTracks.useQuery();
const { data: leaderboard, isLoading } = trpc.judging.getLeaderboard.useQuery(
undefined,
{ trackId: selectedTrackId === "all" ? undefined : selectedTrackId },
{
refetchInterval: 30 * 1000,
refetchIntervalInBackground: true,
Expand All @@ -39,7 +43,21 @@ const LeaderboardPage: NextPage = () => {

<Drawer pageTabs={[{ pageName: "Judging", link: "/judging" }]}>
<main className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-8">Project Leaderboard</h1>
<div className="flex justify-between items-center mb-8">
<h1 className="text-2xl font-bold">Project Leaderboard</h1>
<select
className="select select-bordered w-full max-w-xs"
value={selectedTrackId}
onChange={(e) => setSelectedTrackId(e.target.value)}
>
<option value="all">All Tracks</option>
{tracks?.map((track) => (
<option key={track.id} value={track.id}>
{track.name}
</option>
))}
</select>
</div>

<div className="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
<table className="min-w-full">
Expand All @@ -57,6 +75,11 @@ const LeaderboardPage: NextPage = () => {
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Judges
</th>
{selectedTrackId === "all" && (
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
Track
</th>
)}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
Expand Down Expand Up @@ -85,6 +108,13 @@ const LeaderboardPage: NextPage = () => {
{project.numberOfJudges}
</div>
</td>
{selectedTrackId === "all" && (
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900 dark:text-white">
{project.trackName}
</div>
</td>
)}
</tr>
))}
</tbody>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/judging.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,12 @@ const Judging: NextPage = () => {
}, [existingScores, nextProject, reset]);

const onSubmit = (data: any) => {
if (!nextProject?.id) return;
if (!nextProject?.id || !selectedTable?.value) return;

submitJudgment(
{
projectId: nextProject.id,
tableId: selectedTable.value,
responses: Object.entries(data.scores || {}).map(
([questionId, score]) => ({
questionId,
Expand Down
157 changes: 104 additions & 53 deletions src/server/router/judging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export const judgingRouter = router({
.input(
z.object({
projectId: z.string(),
tableId: z.string(),
responses: z.array(
z.object({
questionId: z.string(),
Expand Down Expand Up @@ -433,6 +434,7 @@ export const judgingRouter = router({
data: {
judgeId: ctx.session.user.id,
projectId: input.projectId,
tableId: input.tableId,
dhYear: dhYearConfig.value,
},
});
Expand Down Expand Up @@ -519,73 +521,122 @@ export const judgingRouter = router({
},
});
}),
getLeaderboard: protectedProcedure.query(async ({ ctx }) => {
if (!ctx.session.user.role.includes(Role.ADMIN)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// Get current dhYear from Config
const dhYearConfig = await ctx.prisma.config.findUnique({
where: { name: "dhYear" },
});

if (!dhYearConfig) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "dhYear not configured",
getLeaderboard: protectedProcedure
.input(
z.object({
trackId: z.string().optional(),
})
)
.query(async ({ ctx, input }) => {
if (!ctx.session.user.role.includes(Role.ADMIN)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
// Get current dhYear from Config
const dhYearConfig = await ctx.prisma.config.findUnique({
where: { name: "dhYear" },
});
}

// Get all projects with their judging results and responses
const projectScores = await ctx.prisma.project.findMany({
where: {
dhYear: dhYearConfig.value,
},
select: {
id: true,
name: true,
judgingResults: {
select: {
responses: {
select: {
score: true,
question: {
select: {
points: true,
if (!dhYearConfig) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "dhYear not configured",
});
}

// Get all projects with their judging results and responses
const projectScores = await ctx.prisma.project.findMany({
where: {
dhYear: dhYearConfig.value,
...(input.trackId && {
tracks: {
some: {
trackId: input.trackId,
},
},
}),
},
select: {
id: true,
name: true,
tracks: {
include: {
track: true,
},
},
judgingResults: {
select: {
responses: {
select: {
score: true,
question: {
select: {
points: true,
trackId: true,
},
},
},
},
judgeId: true,
},
},
},
},
});
});

// Calculate total score for each project
const leaderboard = projectScores.map((project) => {
let totalScore = 0;
const numberOfJudges = project.judgingResults.length;
// Calculate total score for each project
const leaderboard = projectScores.map((project) => {
const judgeScores = new Map<
string,
{ general: number; track: number }
>();

// Calculate scores per judge
project.judgingResults.forEach((result) => {
let generalScore = 0;
let trackScore = 0;

result.responses.forEach((response) => {
const weightedScore = response.score * response.question.points;
// If trackId matches input.trackId, it's a track-specific question
// Otherwise, it's a general question
if (input.trackId && response.question.trackId === input.trackId) {
trackScore += weightedScore;
} else if (!input.trackId || response.question.trackId === null) {
generalScore += weightedScore;
}
});

// Sum up scores from all judges
project.judgingResults.forEach((result) => {
result.responses.forEach((response) => {
totalScore += response.score * response.question.points;
judgeScores.set(result.judgeId, {
general: generalScore,
track: trackScore,
});
});
});

// Calculate average score if project was judged by multiple judges
const averageScore = numberOfJudges > 0 ? totalScore / numberOfJudges : 0;
// Calculate average scores across judges
let totalGeneralScore = 0;
let totalTrackScore = 0;
judgeScores.forEach((scores) => {
totalGeneralScore += scores.general;
totalTrackScore += scores.track;
});

return {
projectId: project.id,
projectName: project.name,
score: averageScore,
numberOfJudges,
};
});
const numberOfJudges = judgeScores.size;
const averageGeneralScore =
numberOfJudges > 0 ? totalGeneralScore / numberOfJudges : 0;
const averageTrackScore =
numberOfJudges > 0 ? totalTrackScore / numberOfJudges : 0;

// Sort by score in descending order
return leaderboard.sort((a, b) => b.score - a.score);
}),
return {
projectId: project.id,
projectName: project.name,
score: averageGeneralScore + averageTrackScore,
numberOfJudges,
trackName: project.tracks[0]?.track.name || "Unknown",
};
});

// Sort by score in descending order
return leaderboard.sort((a, b) => b.score - a.score);
}),
importRubricQuestions: protectedProcedure
.input(
z.object({
Expand Down
Loading