This commit is contained in:
orosmatthew 2023-05-09 17:19:45 -04:00
commit 8564dd26ec
33 changed files with 511 additions and 245 deletions

39
sandbox/Dockerfile Normal file
View File

@ -0,0 +1,39 @@
FROM ubuntu:22.04
WORKDIR /app
RUN apt-get update
RUN apt-get install curl -y
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
RUN apt-get install git -y
RUN git config --global user.name "Admin"
RUN git config --global user.email noemail@example.com
WORKDIR /opt
RUN apt-get install wget -y
RUN wget -O java.tar.gz https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.7%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz
RUN tar -xzvf java.tar.gz
RUN rm java.tar.gz
ENV JAVA_PATH=/opt/jdk-17.0.7+7/bin
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist"]

View File

@ -0,0 +1,8 @@
version: '3'
services:
sandbox:
build: .
environment:
- ADMIN_URL=http://localhost:5173
- REPO_URL=http://localhost:7006
network_mode: 'host'

View File

@ -65,13 +65,18 @@ async function cloneAndRun(submissionData: SubmissionGetData) {
); );
await git.checkout(submissionData.submission.commitHash); await git.checkout(submissionData.submission.commitHash);
const problemName = submissionData.submission.problem.pascalName; const problemName = submissionData.submission.problem.pascalName;
const output = await runJava( let output: string;
javaBinPath, try {
buildDir, output = await runJava(
join(repoDir, problemName, problemName + '.java'), javaBinPath,
problemName, buildDir,
submissionData.submission.problem.realInput join(repoDir, problemName, problemName + '.java'),
); problemName,
submissionData.submission.problem.realInput
);
} catch (error) {
output = `[An error occurred while running]\n${error}`;
}
const res = await fetch(urlJoin(adminUrl, 'api/submission'), { const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
method: 'POST', method: 'POST',
@ -106,7 +111,14 @@ const repoUrl = process.env.REPO_URL as string;
const javaBinPath = process.env.JAVA_PATH as string; const javaBinPath = process.env.JAVA_PATH as string;
async function loop() { async function loop() {
const submissionData = await fetchQueuedSubmission(); let submissionData: SubmissionGetData | undefined;
try {
submissionData = await fetchQueuedSubmission();
} catch {
console.error('Failed to fetch submission');
return;
}
if (!submissionData) { if (!submissionData) {
console.error('Unable to fetch submission data'); console.error('Unable to fetch submission data');
} else { } else {
@ -121,7 +133,7 @@ async function loop() {
async function run() { async function run() {
while (true) { while (true) {
await loop(); await loop();
await new Promise((resolve) => setTimeout(resolve, 15000)); await new Promise((resolve) => setTimeout(resolve, 10000));
} }
} }

View File

@ -1,8 +1,5 @@
import fs from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import os from 'os';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import { error } from 'console';
import util from 'util'; import util from 'util';
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);
@ -36,8 +33,22 @@ export async function runJava(
child.stdin.write(input); child.stdin.write(input);
child.stdin.end(); child.stdin.end();
let resolved = false;
child.on('close', () => { child.on('close', () => {
resolve(outputBuffer); if (!resolved) {
resolved = true;
resolve(outputBuffer);
}
}); });
setTimeout(() => {
if (!resolved) {
console.log('30 seconds reached, killing process');
resolved = true;
child.kill('SIGKILL');
resolve(outputBuffer + '\n[Timeout after 30 seconds]');
}
}, 30000);
}); });
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -62,12 +62,12 @@ model Problem {
} }
model Team { model Team {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
Submission Submission[] submissions Submission[]
contests Contest[] @relation("TeamContestRelation") contests Contest[] @relation("TeamContestRelation")
password String password String
activeTeam ActiveTeam? activeTeam ActiveTeam?
} }
model ActiveTeam { model ActiveTeam {
@ -87,4 +87,5 @@ model Contest {
problems Problem[] @relation("ProblemContestRelation") problems Problem[] @relation("ProblemContestRelation")
activeTeams ActiveTeam[] activeTeams ActiveTeam[]
submissions Submission[] submissions Submission[]
startTime DateTime?
} }

3
web/src/lib/util.ts Normal file
View File

@ -0,0 +1,3 @@
export function stretchTextarea(textarea: HTMLTextAreaElement) {
textarea.style.height = textarea.scrollHeight + 'px';
}

View File

@ -4,7 +4,7 @@
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li> <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/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/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/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="/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> <li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>

View File

@ -2,10 +2,10 @@ import { db } from '$lib/server/prisma';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load = (async () => { export const load = (async () => {
const contests = await db.contest.findMany(); const contests = await db.contest.findMany({ include: { activeTeams: true } });
return { return {
contests: contests.map((contest) => { contests: contests.map((contest) => {
return { id: contest.id, name: contest.name }; return { id: contest.id, name: contest.name, activeTeams: contest.activeTeams.length };
}) })
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View File

@ -20,7 +20,9 @@
{#each data.contests as contest} {#each data.contests as contest}
<a <a
href={'/admin/contests/' + contest.id.toString()} 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} {/each}
</div> </div>

View File

@ -64,10 +64,14 @@ export const actions = {
return { success: false }; return { success: false };
} }
await db.submission.deleteMany({ where: { contestId: contest.id } });
contest.teams.forEach(async (team) => { contest.teams.forEach(async (team) => {
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } }); 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 }; return { success: true };
}, },
stop: async ({ params }) => { stop: async ({ params }) => {

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { page } from '$app/stores';
import type { Actions, PageData } from './$types'; import type { Actions, PageData } from './$types';
export let data: PageData; export let data: PageData;
@ -50,6 +51,7 @@
<div class="mt-3 row"> <div class="mt-3 row">
<div class="col-6"> <div class="col-6">
<h4>Teams</h4> <h4>Teams</h4>
<a href={`${$page.url}/logins`} class="mb-2 btn btn-outline-secondary">Printable Logins</a>
<div class="list-group"> <div class="list-group">
{#each data.teams as team} {#each data.teams as team}
<a href={`/admin/teams/${team.id}`} class="list-group-item list-group-item-action" <a href={`/admin/teams/${team.id}`} class="list-group-item list-group-item-action"

View File

@ -0,0 +1,31 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/prisma';
export const load = (async ({ params }) => {
if (!params.contestId) {
throw redirect(302, '/admin/contests');
}
const contestId = parseInt(params.contestId);
if (isNaN(contestId)) {
throw redirect(302, '/admin/contests');
}
const contest = await db.contest.findUnique({
where: { id: contestId },
include: { teams: true }
});
if (!contest) {
throw redirect(302, '/admin/contests');
}
return {
teams: contest.teams.map((team) => {
return {
id: team.id,
name: team.name,
password: team.password
};
})
};
}) satisfies PageServerLoad;

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
{#each data.teams as team}
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>ID</th>
<th>Team Name</th>
<th>Password</th></tr
>
</thead>
<tbody>
<tr><td>{team.id}</td><td>{team.name}</td><td>{team.password}</td></tr>
</tbody>
</table>
{/each}

View File

@ -1,8 +1,5 @@
import { db } from '$lib/server/prisma'; import { db } from '$lib/server/prisma';
import path, { join } from 'path';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import fs from 'fs';
import { simpleGit } from 'simple-git';
import { createRepos } from '../util'; import { createRepos } from '../util';
export const load = (async () => { export const load = (async () => {
@ -18,22 +15,6 @@ export const load = (async () => {
}; };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
function copyFolderSync(source: string, target: string) {
if (!fs.existsSync(target)) {
fs.mkdirSync(target);
}
fs.readdirSync(source).forEach((file) => {
const sourcePath = path.join(source, file);
const targetPath = path.join(target, file);
if (fs.lstatSync(sourcePath).isDirectory()) {
copyFolderSync(sourcePath, targetPath);
} else {
fs.copyFileSync(sourcePath, targetPath);
}
});
}
export const actions = { export const actions = {
create: async ({ request }) => { create: async ({ request }) => {
const data = await request.formData(); const data = await request.formData();

View File

@ -1,5 +1,4 @@
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import * as Diff from 'diff';
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/prisma'; import { db } from '$lib/server/prisma';
import { SubmissionState } from '@prisma/client'; import { SubmissionState } from '@prisma/client';
@ -17,17 +16,8 @@ export const load = (async ({ params }) => {
if (!problem) { if (!problem) {
throw error(500, 'Invalid 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, output: submission.actualOutput };
}) satisfies PageServerLoad; }) satisfies PageServerLoad;
export const actions = { export const actions = {

View File

@ -5,6 +5,7 @@
import type { Actions, PageData } from './$types'; import type { Actions, PageData } from './$types';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { stretchTextarea } from '$lib/util';
export let data: PageData; export let data: PageData;
export let form: Actions; export let form: Actions;
@ -28,15 +29,17 @@
} }
onMount(() => { onMount(() => {
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, { if (data.diff) {
drawFileList: false, const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
matching: 'lines', drawFileList: false,
diffStyle: 'char', matching: 'lines',
outputFormat: 'side-by-side', diffStyle: 'char',
highlight: false, outputFormat: 'side-by-side',
fileContentToggle: false highlight: false,
}); fileContentToggle: false
diff2htmlUi.draw(); });
diff2htmlUi.draw();
}
incorrectBtn.addEventListener('change', () => { incorrectBtn.addEventListener('change', () => {
submitBtn.disabled = false; submitBtn.disabled = false;
@ -50,10 +53,10 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Diff</title> <title>Review Submission</title>
</svelte:head> </svelte:head>
<h1 style="text-align:center" class="mb-4">Diff</h1> <h1 style="text-align:center" class="mb-4">Review Submission</h1>
{#if form && !form.success} {#if form && !form.success}
<div class="alert alert-danger">Submission was not successful</div> <div class="alert alert-danger">Submission was not successful</div>
@ -61,7 +64,17 @@
<div class="alert alert-success">Success!</div> <div class="alert alert-success">Success!</div>
{/if} {/if}
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a> <div class="mb-3 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>
<h3>Output</h3>
<textarea use:stretchTextarea class="mb-3 form-control" disabled>{data.output}</textarea>
<h3>Diff</h3>
<div class="mt-3" id="diff" /> <div class="mt-3" id="diff" />
<form method="POST" action="?/submit" use:enhance> <form method="POST" action="?/submit" use:enhance>

View File

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { stretchTextarea } from '$lib/util';
import type { Actions, PageData } from './$types'; import type { Actions, PageData } from './$types';
let editing = false; let editing = false;
@ -9,10 +10,6 @@
export let data: PageData; export let data: PageData;
export let form: Actions; export let form: Actions;
function stretchTextarea(textarea: HTMLTextAreaElement) {
textarea.style.height = textarea.scrollHeight + 'px';
}
async function deleteProblem() { async function deleteProblem() {
const sure = confirm('Are you sure?'); const sure = confirm('Are you sure?');
if (!sure) { if (!sure) {

View File

@ -1,12 +1,13 @@
import { db } from '$lib/server/prisma'; import { db } from '$lib/server/prisma';
import { SubmissionState } from '@prisma/client'; import { SubmissionState } from '@prisma/client';
import type { Actions, PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load = (async () => { export const load = (async () => {
const submissions = await db.submission.findMany({ where: { state: SubmissionState.InReview } }); const submissions = await db.submission.findMany({ where: { state: SubmissionState.InReview } });
const teams = await db.team.findMany(); const teams = await db.team.findMany();
const problems = await db.problem.findMany(); const problems = await db.problem.findMany();
return { return {
timestamp: new Date(),
reviewList: submissions.map((row) => { reviewList: submissions.map((row) => {
return { id: row.id, createdAt: row.createdAt }; return { id: row.id, createdAt: row.createdAt };
}), }),
@ -18,45 +19,3 @@ export const load = (async () => {
}) })
}; };
}) satisfies PageServerLoad; }) 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;

View File

@ -1,7 +1,26 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { invalidateAll } from '$app/navigation';
export let data: PageData; export let data: PageData;
let updateInterval: ReturnType<typeof setInterval> | undefined;
let updating = false;
onMount(() => {
updateInterval = setInterval(async () => {
updating = true;
await invalidateAll();
updating = false;
}, 10000);
});
onDestroy(() => {
if (updateInterval) {
clearInterval(updateInterval);
}
});
</script> </script>
<svelte:head> <svelte:head>
@ -10,6 +29,13 @@
<h1 style="text-align:center" class="mb-4">Reviews</h1> <h1 style="text-align:center" class="mb-4">Reviews</h1>
<div class="mb-3 text-end">
{#if updating}
<div class="spinner-border spinner-border-sm text-secondary" />
{/if}
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
</div>
<ul class="list-group"> <ul class="list-group">
{#if data.reviewList.length === 0} {#if data.reviewList.length === 0}
<div class="alert alert-success">No Submission to Review!</div> <div class="alert alert-success">No Submission to Review!</div>
@ -20,66 +46,3 @@
> >
{/each} {/each}
</ul> </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> -->

View File

@ -3,15 +3,106 @@ import type { PageServerLoad } from './$types';
export const load = (async () => { export const load = (async () => {
const timestamp = new Date(); const timestamp = new Date();
const problems = await db.problem.findMany(); const contests = await db.contest.findMany({
const teams = await db.team.findMany(); include: { problems: true, teams: { include: { submissions: true } } }
return { });
const data = {
timestamp: timestamp, timestamp: timestamp,
problems: problems.map((row) => { contests: contests.map((contest) => {
return { friendlyName: row.friendlyName }; return {
}), name: contest.name,
teams: teams.map((row) => { problems: contest.problems.map((problem) => {
return { name: row.name }; 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;
})()
};
})
};
})
.sort((a, b) => {
if (a.solves > b.solves) {
return -1;
} else if (a.solves < b.solves) {
return 1;
} else {
if (a.time < b.time) {
return -1;
} else if (a.time > b.time) {
return 1;
} else {
return 0;
}
}
})
};
}) })
}; };
return data;
}) satisfies PageServerLoad; }) satisfies PageServerLoad;

View File

@ -25,38 +25,85 @@
<title>Admin Scoreboard</title> <title>Admin Scoreboard</title>
</svelte:head> </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">
<div class="text-end"> {#if updating}
{#if updating} <div class="spinner-border spinner-border-sm text-secondary" />
<div class="spinner-border spinner-border-sm text-secondary" /> {/if}
{/if} <strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
</div>
</div> </div>
<table class="table table-striped table-bordered"> {#each data.contests as contest}
<thead> <h2 style="text-align:center">{contest.name}</h2>
<tr> <div class="mb-3 row">
<th>Team Name</th> <div class="text-end" />
{#each data.problems as problem} </div>
<th>{problem.friendlyName}</th>
{/each} <table class="table table-striped table-bordered">
<th>Total Correct</th> <thead>
<th>Total Points</th>
</tr>
</thead>
<tbody>
{#each data.teams as team}
<tr> <tr>
<td>{team.name}</td> <th>Place</th>
{#each data.problems as _} <th>Team Name</th>
<td>-/-</td> <th>Solves</th>
<th>Time</th>
{#each contest.problems as problem}
<th>{problem.friendlyName}</th>
{/each} {/each}
<td>0</td>
<td>0</td>
</tr> </tr>
{/each} </thead>
</tbody> <tbody>
</table> {#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}

View File

@ -6,6 +6,7 @@ export const load = (async () => {
const problems = await db.problem.findMany(); const problems = await db.problem.findMany();
const teams = await db.team.findMany(); const teams = await db.team.findMany();
return { return {
timestamp: new Date(),
submissions: submissions.map((row) => { submissions: submissions.map((row) => {
return { return {
id: row.id, id: row.id,

View File

@ -1,12 +1,30 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { goto } from '$app/navigation'; import { goto, invalidateAll } from '$app/navigation';
import { onDestroy, onMount } from 'svelte';
export let data: PageData; export let data: PageData;
$: data.submissions.sort((a, b) => { $: data.submissions.sort((a, b) => {
return b.createdAt.valueOf() - a.createdAt.valueOf(); return b.createdAt.valueOf() - a.createdAt.valueOf();
}); });
let updateInterval: ReturnType<typeof setInterval> | undefined;
let updating = false;
onMount(() => {
updateInterval = setInterval(async () => {
updating = true;
await invalidateAll();
updating = false;
}, 10000);
});
onDestroy(() => {
if (updateInterval) {
clearInterval(updateInterval);
}
});
</script> </script>
<svelte:head> <svelte:head>
@ -15,7 +33,17 @@
<h1 style="text-align:center" class="mb-4">Submissions</h1> <h1 style="text-align:center" class="mb-4">Submissions</h1>
<p>Rows are color coded: Red - Incorrect, Green - Correct, Yellow - In Review</p> <div class="row">
<div class="col-8">
<p>Rows are color coded: Red - Incorrect, Green - Correct, Yellow - In Review</p>
</div>
<div class="col-4 text-end">
{#if updating}
<div class="spinner-border spinner-border-sm text-secondary" />
{/if}
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
</div>
</div>
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
<thead> <thead>

View File

@ -1,8 +1,6 @@
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/prisma'; import { db } from '$lib/server/prisma';
import * as Diff from 'diff';
import { SubmissionState } from '@prisma/client';
export const load = (async ({ params }) => { export const load = (async ({ params }) => {
const submissionId = parseInt(params.submissionId); const submissionId = parseInt(params.submissionId);
@ -21,16 +19,6 @@ export const load = (async ({ params }) => {
if (!problem) { if (!problem) {
throw error(500, 'Invalid 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 { return {
id: submission.id, id: submission.id,
state: submission.state, state: submission.state,
@ -39,6 +27,18 @@ export const load = (async ({ params }) => {
submitTime: submission.createdAt, submitTime: submission.createdAt,
gradedTime: submission.gradedAt, gradedTime: submission.gradedAt,
message: submission.message, message: submission.message,
diff: diff diff: submission.diff
}; };
}) satisfies PageServerLoad; }) 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;

View File

@ -1,22 +1,27 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { Actions, PageData } from './$types';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui-base'; import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui-base';
import 'diff2html/bundles/css/diff2html.min.css'; import 'diff2html/bundles/css/diff2html.min.css';
import { enhance } from '$app/forms';
export let data: PageData; export let data: PageData;
export let form: Actions;
onMount(() => { onMount(() => {
if (data.diff) { if (data.diff) {
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, { const diffElement = document.getElementById('diff');
drawFileList: false, if (diffElement) {
matching: 'lines', const diff2htmlUi = new Diff2HtmlUI(diffElement, data.diff, {
diffStyle: 'char', drawFileList: false,
outputFormat: 'side-by-side', matching: 'lines',
highlight: false, diffStyle: 'char',
fileContentToggle: false outputFormat: 'side-by-side',
}); highlight: false,
diff2htmlUi.draw(); fileContentToggle: false
});
diff2htmlUi.draw();
}
} }
}); });
</script> </script>
@ -27,7 +32,31 @@
<h1 style="text-align:center" class="mb-4">Submission</h1> <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"> <table class="table table-bordered">
<thead> <thead>

View File

@ -1,5 +1,6 @@
import { db } from '$lib/server/prisma'; import { db } from '$lib/server/prisma';
import type { Actions, PageServerLoad } from './$types'; import type { Actions, PageServerLoad } from './$types';
import { genPassword } from './util';
export const load = (async () => { export const load = (async () => {
const teams = await db.team.findMany(); const teams = await db.team.findMany();
@ -18,7 +19,7 @@ export const actions = {
return { success: false }; return { success: false };
} }
try { try {
await db.team.create({ data: { name: name.toString(), password: "thing" } }); await db.team.create({ data: { name: name.toString(), password: genPassword() } });
} catch { } catch {
return { success: false }; return { success: false };
} }

View File

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { genPassword } from '../util';
import type { Actions, PageData } from './$types'; import type { Actions, PageData } from './$types';
export let data: PageData; export let data: PageData;
@ -10,6 +11,11 @@
$: if (form && form.success) { $: if (form && form.success) {
changingPassword = false; changingPassword = false;
} }
function onGenPassword() {
const passEntry = document.getElementById('pass_entry') as HTMLInputElement;
passEntry.value = genPassword();
}
</script> </script>
<svelte:head> <svelte:head>
@ -72,7 +78,7 @@
{:else} {:else}
<form method="POST" action="?/password" use:enhance> <form method="POST" action="?/password" use:enhance>
<h4>Change Password</h4> <h4>Change Password</h4>
<input name="password" class="form-control" /> <input id="pass_entry" name="password" class="form-control" />
<div class="mt-2 row"> <div class="mt-2 row">
<div class="text-end"> <div class="text-end">
<button <button
@ -82,6 +88,9 @@
type="button" type="button"
class="btn btn-outline-secondary">Cancel</button class="btn btn-outline-secondary">Cancel</button
> >
<button on:click={onGenPassword} type="button" class="btn btn-outline-primary"
>Generate</button
>
<button type="submit" class="btn btn-success">Change</button> <button type="submit" class="btn btn-success">Change</button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,8 @@
export function genPassword(): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let password = '';
for (let i = 0; i < 8; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
}

View File

@ -64,8 +64,8 @@ export const POST = (async ({ request }) => {
const diff = Diff.createTwoFilesPatch( const diff = Diff.createTwoFilesPatch(
'expected', 'expected',
'actual', 'actual',
data.data.output, submission.problem.realOutput,
submission.actualOutput! data.data.output
); );
await db.submission.update({ await db.submission.update({
where: { id: data.data.submissionId }, where: { id: data.data.submissionId },

View File

@ -13,7 +13,10 @@ export const POST = (async ({ params, request }) => {
const sessionToken = params.session; const sessionToken = params.session;
const activeTeam = await db.activeTeam.findUnique({ const activeTeam = await db.activeTeam.findUnique({
where: { sessionToken: sessionToken }, where: { sessionToken: sessionToken },
include: { contest: { include: { problems: { select: { id: true } } } } } include: {
contest: { include: { problems: { select: { id: true } } } },
team: { include: { submissions: true } }
}
}); });
if (!activeTeam) { if (!activeTeam) {
throw error(400); throw error(400);
@ -31,6 +34,19 @@ export const POST = (async ({ params, request }) => {
throw error(400); throw error(400);
} }
// Make sure no submission is currently marked correct
const correctSubmissions = activeTeam.team.submissions.filter((submission) => {
return (
submission.contestId === activeTeam.contestId &&
submission.state === 'Correct' &&
submission.problemId === data.data.problemId
);
}).length;
if (correctSubmissions !== 0) {
return json({ success: false, message: 'Already submitted correct submission' });
}
await db.submission.create({ await db.submission.create({
data: { data: {
state: SubmissionState.Queued, state: SubmissionState.Queued,

BIN
web/static/correct.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
web/static/incorrect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB