[web] Working scoreboard
This commit is contained in:
parent
5280a25d4d
commit
544f5390cd
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
@ -62,12 +62,12 @@ model Problem {
|
||||
}
|
||||
|
||||
model Team {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
Submission Submission[]
|
||||
contests Contest[] @relation("TeamContestRelation")
|
||||
password String
|
||||
activeTeam ActiveTeam?
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
submissions Submission[]
|
||||
contests Contest[] @relation("TeamContestRelation")
|
||||
password String
|
||||
activeTeam ActiveTeam?
|
||||
}
|
||||
|
||||
model ActiveTeam {
|
||||
@ -87,4 +87,5 @@ model Contest {
|
||||
problems Problem[] @relation("ProblemContestRelation")
|
||||
activeTeams ActiveTeam[]
|
||||
submissions Submission[]
|
||||
startTime DateTime?
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li>
|
||||
<li><a href="/admin/submissions" class="nav-link px-2">Submissions</a></li>
|
||||
<li><a href="/admin/problems" class="nav-link px-2">Problems</a></li>
|
||||
<li><a href="/admin/scoreboard" class="nav-link px-2">Scoreboard</a></li>
|
||||
<li><a href="/admin/scoreboard" class="nav-link px-2">Scoreboards</a></li>
|
||||
<li><a href="/admin/teams" class="nav-link px-2">Teams</a></li>
|
||||
<li><a href="/admin/contests" class="nav-link px-2">Contests</a></li>
|
||||
<li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>
|
||||
|
@ -2,10 +2,10 @@ import { db } from '$lib/server/prisma';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const contests = await db.contest.findMany();
|
||||
const contests = await db.contest.findMany({ include: { activeTeams: true } });
|
||||
return {
|
||||
contests: contests.map((contest) => {
|
||||
return { id: contest.id, name: contest.name };
|
||||
return { id: contest.id, name: contest.name, activeTeams: contest.activeTeams.length };
|
||||
})
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
@ -20,7 +20,9 @@
|
||||
{#each data.contests as contest}
|
||||
<a
|
||||
href={'/admin/contests/' + contest.id.toString()}
|
||||
class="list-group-item list-group-item-action">{contest.name}</a
|
||||
class={`list-group-item list-group-item-action ${
|
||||
contest.activeTeams === 0 ? '' : ' list-group-item-success'
|
||||
}`}>{contest.name}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -68,6 +68,8 @@ export const actions = {
|
||||
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
|
||||
});
|
||||
|
||||
await db.contest.update({ where: { id: contestId }, data: { startTime: new Date() } });
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
stop: async ({ params }) => {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import * as Diff from 'diff';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/prisma';
|
||||
import { SubmissionState } from '@prisma/client';
|
||||
@ -17,17 +16,8 @@ export const load = (async ({ params }) => {
|
||||
if (!problem) {
|
||||
throw error(500, 'Invalid problem');
|
||||
}
|
||||
let diff: string | undefined;
|
||||
if (submission.actualOutput) {
|
||||
diff = Diff.createTwoFilesPatch(
|
||||
'expected',
|
||||
'actual',
|
||||
problem.realOutput,
|
||||
submission.actualOutput
|
||||
);
|
||||
}
|
||||
|
||||
return { diff: diff };
|
||||
return { diff: submission.diff, submissionId: submission.id };
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
|
@ -28,15 +28,17 @@
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
||||
drawFileList: false,
|
||||
matching: 'lines',
|
||||
diffStyle: 'char',
|
||||
outputFormat: 'side-by-side',
|
||||
highlight: false,
|
||||
fileContentToggle: false
|
||||
});
|
||||
diff2htmlUi.draw();
|
||||
if (data.diff) {
|
||||
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
||||
drawFileList: false,
|
||||
matching: 'lines',
|
||||
diffStyle: 'char',
|
||||
outputFormat: 'side-by-side',
|
||||
highlight: false,
|
||||
fileContentToggle: false
|
||||
});
|
||||
diff2htmlUi.draw();
|
||||
}
|
||||
|
||||
incorrectBtn.addEventListener('change', () => {
|
||||
submitBtn.disabled = false;
|
||||
@ -61,7 +63,11 @@
|
||||
<div class="alert alert-success">Success!</div>
|
||||
{/if}
|
||||
|
||||
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
||||
<div class="col">
|
||||
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
||||
<a href={`/admin/submissions/${data.submissionId.toString()}`} class="btn btn-outline-primary">Go to Submission</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-3" id="diff" />
|
||||
|
||||
<form method="POST" action="?/submit" use:enhance>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { db } from '$lib/server/prisma';
|
||||
import { SubmissionState } from '@prisma/client';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import type {PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const submissions = await db.submission.findMany({ where: { state: SubmissionState.InReview } });
|
||||
@ -19,44 +19,3 @@ export const load = (async () => {
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
// export const actions = {
|
||||
// submission: async ({ request }) => {
|
||||
// const data = await request.formData();
|
||||
// const teamId = data.get('teamId');
|
||||
// const problemId = data.get('problemId');
|
||||
// const actual = data.get('actual');
|
||||
// if (!teamId || !problemId || !actual) {
|
||||
// return { success: false };
|
||||
// }
|
||||
// const problemIdInt = parseInt(problemId.toString());
|
||||
// const teamIdInt = parseInt(teamId.toString());
|
||||
// if (isNaN(problemIdInt) || isNaN(teamIdInt)) {
|
||||
// return { success: false };
|
||||
// }
|
||||
// const problem = await db.problem.findUnique({ where: { id: problemIdInt } });
|
||||
// if (!problem) {
|
||||
// return { success: false };
|
||||
// }
|
||||
// if (problem.realOutput === actual.toString()) {
|
||||
// await db.submission.create({
|
||||
// data: {
|
||||
// state: SubmissionState.Correct,
|
||||
// actualOutput: actual.toString(),
|
||||
// teamId: teamIdInt,
|
||||
// problemId: problemIdInt,
|
||||
// gradedAt: new Date()
|
||||
// }
|
||||
// });
|
||||
// return { success: true };
|
||||
// }
|
||||
// await db.submission.create({
|
||||
// data: {
|
||||
// state: SubmissionState.InReview,
|
||||
// actualOutput: actual.toString(),
|
||||
// teamId: teamIdInt,
|
||||
// problemId: problemIdInt
|
||||
// }
|
||||
// });
|
||||
// return { success: true };
|
||||
// }
|
||||
// } satisfies Actions;
|
||||
|
@ -20,66 +20,3 @@
|
||||
>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<!-- <hr />
|
||||
<h2>For Testing Purposes - Create Fake Submission</h2>
|
||||
{#if form && !form.success}
|
||||
<div class="alert alert-danger">Invalid Submission</div>
|
||||
{/if}
|
||||
<form method="POST" action="?/submission" use:enhance>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<h5>Team</h5>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
{selectedTeam ? selectedTeam.name : 'Select Team'}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{#each data.teams as team}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => {
|
||||
selectedTeam = team;
|
||||
}}
|
||||
type="button"
|
||||
class="dropdown-item">{team.name}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<h5>Problem</h5>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
||||
{selectedProblem ? selectedProblem.name : 'Select Problem'}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{#each data.problems as problem}
|
||||
<li>
|
||||
<button
|
||||
on:click={() => {
|
||||
selectedProblem = problem;
|
||||
}}
|
||||
type="button"
|
||||
class="dropdown-item">{problem.name}</button
|
||||
>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h5>Actual output (like from student output)</h5>
|
||||
<textarea name="actual" class="form-control" />
|
||||
</div>
|
||||
</div>
|
||||
<input name="teamId" type="hidden" value={selectedTeam ? selectedTeam.id : ''} />
|
||||
<input name="problemId" type="hidden" value={selectedProblem ? selectedProblem.id : ''} />
|
||||
<div class="row justify-content-end">
|
||||
<div class="text-end">
|
||||
<button type="submit" class="mt-3 btn btn-secondary">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form> -->
|
||||
|
@ -3,15 +3,88 @@ import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
const timestamp = new Date();
|
||||
const problems = await db.problem.findMany();
|
||||
const teams = await db.team.findMany();
|
||||
return {
|
||||
const contests = await db.contest.findMany({
|
||||
include: { problems: true, teams: { include: { submissions: true } } }
|
||||
});
|
||||
const data = {
|
||||
timestamp: timestamp,
|
||||
problems: problems.map((row) => {
|
||||
return { friendlyName: row.friendlyName };
|
||||
}),
|
||||
teams: teams.map((row) => {
|
||||
return { name: row.name };
|
||||
contests: contests.map((contest) => {
|
||||
return {
|
||||
name: contest.name,
|
||||
problems: contest.problems.map((problem) => {
|
||||
return { id: problem.id, friendlyName: problem.friendlyName };
|
||||
}),
|
||||
teams: contest.teams.map((team) => {
|
||||
return {
|
||||
name: team.name,
|
||||
solves: team.submissions.filter((submission) => {
|
||||
return submission.contestId === contest.id && submission.state === 'Correct';
|
||||
}).length,
|
||||
time: (() => {
|
||||
const correctSubmissions = team.submissions.filter((submission) => {
|
||||
return submission.contestId === contest.id && submission.state === 'Correct';
|
||||
});
|
||||
const penaltyTime =
|
||||
team.submissions.filter((submission) => {
|
||||
return (
|
||||
submission.contestId === contest.id &&
|
||||
submission.state === 'Incorrect' &&
|
||||
correctSubmissions.find((correct) => {
|
||||
return correct.problemId === submission.problemId;
|
||||
})
|
||||
);
|
||||
}).length * 10;
|
||||
let time = penaltyTime;
|
||||
correctSubmissions.forEach((correctSubmission) => {
|
||||
const gradedAt = correctSubmission.gradedAt!.valueOf();
|
||||
const min = (gradedAt - contest.startTime!.valueOf()) / 60000;
|
||||
time += min;
|
||||
});
|
||||
return time;
|
||||
})(),
|
||||
problems: contest.problems.map((problem) => {
|
||||
return {
|
||||
id: problem.id,
|
||||
attempts: team.submissions.filter((submission) => {
|
||||
return (
|
||||
submission.contestId === contest.id &&
|
||||
submission.problemId === problem.id &&
|
||||
(submission.state === 'Correct' || submission.state === 'Incorrect')
|
||||
);
|
||||
}).length,
|
||||
graphic: team.submissions.find((submission) => {
|
||||
return (
|
||||
submission.contestId === contest.id &&
|
||||
submission.problemId === problem.id &&
|
||||
(submission.state === 'Correct' || submission.state === 'Incorrect')
|
||||
);
|
||||
})
|
||||
? team.submissions.find((submission) => {
|
||||
return submission.problemId === problem.id && submission.state === 'Correct';
|
||||
})
|
||||
? 'correct'
|
||||
: 'incorrect'
|
||||
: null,
|
||||
min: (() => {
|
||||
const correctSubmission = team.submissions.find((submission) => {
|
||||
return (
|
||||
submission.contestId === contest.id &&
|
||||
submission.problemId === problem.id &&
|
||||
submission.state === 'Correct'
|
||||
);
|
||||
});
|
||||
if (correctSubmission) {
|
||||
const gradedAt = correctSubmission.gradedAt!.valueOf();
|
||||
return (gradedAt - contest.startTime!.valueOf()) / 60000;
|
||||
}
|
||||
return undefined;
|
||||
})()
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
})
|
||||
};
|
||||
return data;
|
||||
}) satisfies PageServerLoad;
|
||||
|
@ -25,38 +25,85 @@
|
||||
<title>Admin Scoreboard</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 style="text-align:center" class="mb-4">Admin Scoreboard</h1>
|
||||
<h1 style="text-align:center" class="mb-4">Admin Scoreboards</h1>
|
||||
|
||||
<div class="mb-3 row">
|
||||
<div class="text-end">
|
||||
{#if updating}
|
||||
<div class="spinner-border spinner-border-sm text-secondary" />
|
||||
{/if}
|
||||
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{#if updating}
|
||||
<div class="spinner-border spinner-border-sm text-secondary" />
|
||||
{/if}
|
||||
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Team Name</th>
|
||||
{#each data.problems as problem}
|
||||
<th>{problem.friendlyName}</th>
|
||||
{/each}
|
||||
<th>Total Correct</th>
|
||||
<th>Total Points</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.teams as team}
|
||||
{#each data.contests as contest}
|
||||
<h2 style="text-align:center">{contest.name}</h2>
|
||||
<div class="mb-3 row">
|
||||
<div class="text-end" />
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{team.name}</td>
|
||||
{#each data.problems as _}
|
||||
<td>-/-</td>
|
||||
<th>Place</th>
|
||||
<th>Team Name</th>
|
||||
<th>Solves</th>
|
||||
<th>Time</th>
|
||||
{#each contest.problems as problem}
|
||||
<th>{problem.friendlyName}</th>
|
||||
{/each}
|
||||
<td>0</td>
|
||||
<td>0</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each contest.teams as team, i}
|
||||
<tr>
|
||||
<td style="text-align:center; font-size:24px;"><strong>{i + 1}</strong></td>
|
||||
<td style="font-size:18px">{team.name}</td>
|
||||
<td style="font-size:18px">{team.solves}</td>
|
||||
<td style="font-size:18px">{team.time.toFixed(0)}</td>
|
||||
{#each contest.problems as problem}
|
||||
<td>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
{#if team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.graphic !== null}
|
||||
<img
|
||||
src={team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.graphic === 'correct'
|
||||
? '/correct.png'
|
||||
: '/incorrect.png'}
|
||||
alt="check or X"
|
||||
width="30px"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="col-9">
|
||||
{#if team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.attempts !== 0}
|
||||
{team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.attempts}
|
||||
{team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.attempts === 1
|
||||
? 'Attempt'
|
||||
: 'Attempts'}<br />{#if team.problems.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})?.min}<span style="color:rgb(102,102,102)"
|
||||
>{team.problems
|
||||
.find((p) => {
|
||||
return p.id === problem.id;
|
||||
})
|
||||
?.min?.toFixed(0)} min</span
|
||||
>{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/each}
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import type { Actions, PageServerLoad } from './$types';
|
||||
import { db } from '$lib/server/prisma';
|
||||
import * as Diff from 'diff';
|
||||
import { SubmissionState } from '@prisma/client';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
const submissionId = parseInt(params.submissionId);
|
||||
@ -21,16 +19,6 @@ export const load = (async ({ params }) => {
|
||||
if (!problem) {
|
||||
throw error(500, 'Invalid problem');
|
||||
}
|
||||
let diff: string | null = null;
|
||||
if (submission.state == SubmissionState.Incorrect) {
|
||||
diff = Diff.createTwoFilesPatch(
|
||||
'expected',
|
||||
'actual',
|
||||
problem.realOutput,
|
||||
submission.actualOutput!
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: submission.id,
|
||||
state: submission.state,
|
||||
@ -39,6 +27,18 @@ export const load = (async ({ params }) => {
|
||||
submitTime: submission.createdAt,
|
||||
gradedTime: submission.gradedAt,
|
||||
message: submission.message,
|
||||
diff: diff
|
||||
diff: submission.diff
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
export const actions = {
|
||||
delete: async ({ params }) => {
|
||||
const submissionId = parseInt(params.submissionId);
|
||||
try {
|
||||
await db.submission.delete({ where: { id: submissionId } });
|
||||
} catch {
|
||||
return { success: false };
|
||||
}
|
||||
throw redirect(302, '/admin/submissions');
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
@ -1,22 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import type { Actions, PageData } from './$types';
|
||||
import { onMount } from 'svelte';
|
||||
import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui-base';
|
||||
import 'diff2html/bundles/css/diff2html.min.css';
|
||||
import { enhance } from '$app/forms';
|
||||
|
||||
export let data: PageData;
|
||||
export let form: Actions;
|
||||
|
||||
onMount(() => {
|
||||
if (data.diff) {
|
||||
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
||||
drawFileList: false,
|
||||
matching: 'lines',
|
||||
diffStyle: 'char',
|
||||
outputFormat: 'side-by-side',
|
||||
highlight: false,
|
||||
fileContentToggle: false
|
||||
});
|
||||
diff2htmlUi.draw();
|
||||
const diffElement = document.getElementById('diff');
|
||||
if (diffElement) {
|
||||
const diff2htmlUi = new Diff2HtmlUI(diffElement, data.diff, {
|
||||
drawFileList: false,
|
||||
matching: 'lines',
|
||||
diffStyle: 'char',
|
||||
outputFormat: 'side-by-side',
|
||||
highlight: false,
|
||||
fileContentToggle: false
|
||||
});
|
||||
diff2htmlUi.draw();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -27,7 +32,31 @@
|
||||
|
||||
<h1 style="text-align:center" class="mb-4">Submission</h1>
|
||||
|
||||
<a href="/admin/submissions" class="mb-3 btn btn-outline-primary">All Submissions</a>
|
||||
{#if form && !form.success}
|
||||
<div class="alert alert-danger">Error</div>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<a href="/admin/submissions" class="mb-3 btn btn-outline-primary">All Submissions</a>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<form
|
||||
method="POST"
|
||||
action="?/delete"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm('Are you sure?')) {
|
||||
cancel();
|
||||
}
|
||||
return async ({ update }) => {
|
||||
update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
|
@ -64,8 +64,8 @@ export const POST = (async ({ request }) => {
|
||||
const diff = Diff.createTwoFilesPatch(
|
||||
'expected',
|
||||
'actual',
|
||||
data.data.output,
|
||||
submission.actualOutput!
|
||||
submission.problem.realOutput,
|
||||
data.data.output
|
||||
);
|
||||
await db.submission.update({
|
||||
where: { id: data.data.submissionId },
|
||||
|
BIN
web/static/correct.png
Normal file
BIN
web/static/correct.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
web/static/incorrect.png
Normal file
BIN
web/static/incorrect.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Loading…
Reference in New Issue
Block a user