Skip to content

Commit

Permalink
feat: leaderboard by track (#245)
Browse files Browse the repository at this point in the history
* feat: add leaderbaord by track

* feat: generate prisma migration

* fix: score should be based on question track

---------

Co-authored-by: Krish120003 <krish120003@gmail.com>
  • Loading branch information
arian81 and Krish120003 authored Jan 12, 2025
1 parent 61593b2 commit 29fcfa5
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 62 deletions.
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

0 comments on commit 29fcfa5

Please sign in to comment.