Compare commits

..

No commits in common. "23c6711ad6aae2e61e188a55ba44cc23aeb04fb6" and "437bfe4c1fb794b0262da95a119713c07f9c738d" have entirely different histories.

15 changed files with 67 additions and 332 deletions

View File

@ -1,3 +1,2 @@
node_modules node_modules
.vscode .vscode
../../shared/**/*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 192 KiB

View File

@ -69,12 +69,10 @@ export const runCpp: IRunner<IRunnerParamsCpp> = async function (
child.stdout.setEncoding('utf8'); child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => { child.stdout.on('data', (data) => {
outputBuffer += data.toString(); outputBuffer += data.toString();
params.outputCallback?.(data.toString());
}); });
child.stderr.setEncoding('utf8'); child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => { child.stderr.on('data', (data) => {
outputBuffer += data.toString(); outputBuffer += data.toString();
params.outputCallback?.(data.toString());
}); });
const runStartTime = performance.now(); const runStartTime = performance.now();

View File

@ -22,7 +22,7 @@ services:
- ORIGIN=${ORIGIN} - ORIGIN=${ORIGIN}
- WEB_SANDBOX_SECRET=${WEB_SANDBOX_SECRET} - WEB_SANDBOX_SECRET=${WEB_SANDBOX_SECRET}
volumes: volumes:
- ./repo:/app/web/repo - ./repo:/app/repo
depends_on: depends_on:
- db - db
restart: unless-stopped restart: unless-stopped

24
web/package-lock.json generated
View File

@ -28,13 +28,13 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/kit": "^2.5.4", "@sveltejs/kit": "^2.5.3",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/diff": "^5.0.9", "@types/diff": "^5.0.9",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.27", "@types/node": "^20.11.26",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
@ -42,7 +42,7 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.2.2",
"sass": "^1.72.0", "sass": "^1.71.1",
"svelte": "^4.2.12", "svelte": "^4.2.12",
"svelte-check": "^3.6.7", "svelte-check": "^3.6.7",
"tslib": "^2.6.2", "tslib": "^2.6.2",
@ -996,9 +996,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.5.4", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.3.tgz",
"integrity": "sha512-eDxK2d4EGzk99QsZNoPXe7jlzA5EGqfcCpUwZ912bhnalsZ2ZsG5wGRthkydupVjYyqdmzEanVKFhLxU2vkPSQ==", "integrity": "sha512-s6x7HBn/Fp+UNvyhJohjIA0FcJ+BWHGUDQ4PCg1D0EboUlvbimJQYchINu8G6sspLXYmlcsuNsp8bbcrRk85iw==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@types/cookie": "^0.6.0", "@types/cookie": "^0.6.0",
@ -1127,9 +1127,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.27", "version": "20.11.26",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.27.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz",
"integrity": "sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==", "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@ -3632,9 +3632,9 @@
} }
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.72.0", "version": "1.71.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
"integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==", "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
"devOptional": true, "devOptional": true,
"dependencies": { "dependencies": {
"chokidar": ">=3.0.0 <4.0.0", "chokidar": ">=3.0.0 <4.0.0",

View File

@ -12,13 +12,13 @@
"format": "prettier --plugin prettier-plugin-svelte --write ." "format": "prettier --plugin prettier-plugin-svelte --write ."
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/kit": "^2.5.4", "@sveltejs/kit": "^2.5.3",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/diff": "^5.0.9", "@types/diff": "^5.0.9",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.11.27", "@types/node": "^20.11.26",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^7.2.0", "@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0", "@typescript-eslint/parser": "^7.2.0",
@ -26,7 +26,7 @@
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2", "prettier-plugin-svelte": "^3.2.2",
"sass": "^1.72.0", "sass": "^1.71.1",
"svelte": "^4.2.12", "svelte": "^4.2.12",
"svelte-check": "^3.6.7", "svelte-check": "^3.6.7",
"tslib": "^2.6.2", "tslib": "^2.6.2",

View File

@ -101,5 +101,4 @@ model Contest {
activeTeams ActiveTeam[] activeTeams ActiveTeam[]
submissions Submission[] submissions Submission[]
startTime DateTime? startTime DateTime?
freezeTime DateTime?
} }

View File

@ -10,8 +10,6 @@ import {
templateCppCMakeLists, templateCppCMakeLists,
templateCppGitIgnore, templateCppGitIgnore,
templateCppProblem, templateCppProblem,
templateCppVscodeLaunch,
templateCppVscodeTasks,
templateJavaProblem templateJavaProblem
} from './templates'; } from './templates';
@ -50,14 +48,10 @@ async function addProblemsCSharp(opts: OptsAddProblems) {
async function addProblemsCPP(opts: OptsAddProblems) { async function addProblemsCPP(opts: OptsAddProblems) {
let cmakeLists = templateCppCMakeLists; let cmakeLists = templateCppCMakeLists;
opts.contest.problems.forEach((problem) => { opts.contest.problems.forEach((problem) => {
cmakeLists += `add_executable(${problem.pascalName} ${problem.pascalName}/${problem.pascalName}.cpp)\n`; cmakeLists += `add_executable(${problem.pascalName} ${problem.pascalName}/${problem.pascalName}.cpp)`;
}); });
opts.fs.writeFileSync(join(opts.dir, 'CMakeLists.txt'), cmakeLists); opts.fs.writeFileSync(join(opts.dir, 'CMakeLists.txt'), cmakeLists);
opts.fs.mkdirSync(join(opts.dir, '.vscode'));
opts.fs.writeFileSync(join(opts.dir, '.vscode', 'launch.json'), templateCppVscodeLaunch);
opts.fs.writeFileSync(join(opts.dir, '.vscode', 'tasks.json'), templateCppVscodeTasks);
opts.contest.problems.forEach((problem) => { opts.contest.problems.forEach((problem) => {
opts.fs.mkdirSync(join(opts.dir, problem.pascalName)); opts.fs.mkdirSync(join(opts.dir, problem.pascalName));
const filledTemplate = templateCppProblem.replaceAll('%%pascalName%%', problem.pascalName); const filledTemplate = templateCppProblem.replaceAll('%%pascalName%%', problem.pascalName);
@ -68,7 +62,7 @@ async function addProblemsCPP(opts: OptsAddProblems) {
}); });
} }
export async function createRepos(contestId: number, teamIds: number[]) { export async function createRepos(contestId: number) {
const vol = new memfs.Volume(); const vol = new memfs.Volume();
const fs = createFsFromVolume(vol); const fs = createFsFromVolume(vol);
@ -81,9 +75,7 @@ export async function createRepos(contestId: number, teamIds: number[]) {
return; return;
} }
contest.teams contest.teams.forEach(async (team) => {
.filter((t) => teamIds.includes(t.id))
.forEach(async (team) => {
fs.mkdirSync(team.id.toString(), { recursive: true }); fs.mkdirSync(team.id.toString(), { recursive: true });
await git.init({ fs: fs, bare: false, defaultBranch: 'master', dir: team.id.toString() }); await git.init({ fs: fs, bare: false, defaultBranch: 'master', dir: team.id.toString() });
if (team.language === 'Java') { if (team.language === 'Java') {

View File

@ -41,67 +41,5 @@ int main()
return 0; return 0;
}`; }`;
export const templateCppGitIgnore = `/**/build export const templateCppGitIgnore = `/build
`;
export const templateCppVscodeLaunch = `{
"configurations": [
{
"name": "C/C++: g++ build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "\${fileDirname}/build/\${fileBasenameNoExtension}.out",
"args": [],
"stopAtEntry": false,
"cwd": "\${fileDirname}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "Set Disassembly Flavor to Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: g++ build active file",
"miDebuggerPath": "/usr/bin/gdb"
}
],
"version": "2.0.0"
}
`;
export const templateCppVscodeTasks = `{
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++ build active file",
"command": "/usr/bin/g++",
"args": [
"-fdiagnostics-color=always",
"-g",
"\${file}",
"-o",
"\${fileDirname}/build/\${fileBasenameNoExtension}.out"
],
"options": {
"cwd": "\${fileDirname}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
],
"version": "2.0.0"
}
`; `;

View File

@ -3,42 +3,3 @@
</svelte:head> </svelte:head>
<h1 style="text-align:center" class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h1> <h1 style="text-align:center" class="mb-4"><i class="bi bi-speedometer2"></i> Dashboard</h1>
<div class="row">
<div class="col-md-6">
<a
class="card stats-card bg-body-tertiary stats-card-link stats-card-action"
draggable="false"
href="/public/scoreboard"
>
<h4><i class="bi bi-link-45deg"></i> Public Scoreboard</h4>
</a>
</div>
<div class="col-md-6 mt-3 mt-md-0"></div>
</div>
<style>
.stats-card {
text-align: left;
padding: 20px;
border-radius: 10px;
transform: scale(1);
transition: 0.1s;
text-decoration: none;
}
.stats-card-link {
color: inherit;
text-decoration: none;
vertical-align: top;
transform: scale(1);
transition: 0.1s;
}
.stats-card-link:hover {
transform: scale(1.02);
}
.stats-card-action:hover {
cursor: pointer;
}
</style>

View File

@ -96,7 +96,7 @@ export const actions = {
}); });
return { success: true }; return { success: true };
}, },
repo: async ({ params, request }) => { repo: async ({ params }) => {
if (!params.contestId) { if (!params.contestId) {
return { success: false }; return { success: false };
} }
@ -104,46 +104,10 @@ export const actions = {
if (isNaN(contestId)) { if (isNaN(contestId)) {
return { success: false }; return { success: false };
} }
const form = await request.formData(); if (fs.existsSync(join('repo', contestId.toString()))) {
const formEntries = Array.from(form.entries()); fs.removeSync(join('repo', contestId.toString()));
const resetTeamIds = formEntries
.filter((e) => e[0].startsWith('teamId'))
.map((e) => {
return parseInt(e[1].toString());
});
resetTeamIds.forEach((teamId) => {
const repoPath = join('repo', contestId.toString(), `${teamId.toString()}.git`);
if (fs.existsSync(repoPath) === true) {
fs.removeSync(repoPath);
}
});
await createRepos(contestId, resetTeamIds);
return { success: true };
},
'freeze-time': async ({ params, request }) => {
if (!params.contestId) {
return { success: false, message: 'No contest Id specified' };
}
const contestId = parseInt(params.contestId);
if (isNaN(contestId)) {
return { success: false, message: 'Invalid contest Id' };
}
const form = await request.formData();
const formFreezeTime = form.get('freezeTime');
if (formFreezeTime === null) {
return { success: false, message: 'Invalid input' };
}
const freezeTime = new Date(formFreezeTime.toString());
const contest = await db.contest.findUnique({ where: { id: contestId } });
if (contest === null) {
return { success: false, message: 'Invalid contest' };
}
try {
await db.contest.update({ where: { id: contestId }, data: { freezeTime } });
} catch (e) {
console.error(`Database error: ${e}`);
return { success: false, message: `Database error: ${e}` };
} }
await createRepos(contestId);
return { success: true }; return { success: true };
} }
} satisfies Actions; } satisfies Actions;

View File

@ -3,20 +3,11 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import ConfirmModal from '$lib/ConfirmModal.svelte'; import ConfirmModal from '$lib/ConfirmModal.svelte';
import FormAlert from '$lib/FormAlert.svelte'; import FormAlert from '$lib/FormAlert.svelte';
import Modal from '$lib/Modal.svelte'; import type { PageData } from './$types';
import type { Actions, PageData } from './$types';
export let data: PageData; export let data: PageData;
export let form: Actions;
$: if (form) {
freezeModal.hide();
repoModal.hide();
}
let confirmModal: ConfirmModal; let confirmModal: ConfirmModal;
let freezeModal: Modal;
let repoModal: Modal;
function enhanceConfirm(form: HTMLFormElement, text: string) { function enhanceConfirm(form: HTMLFormElement, text: string) {
enhance(form, async ({ cancel }) => { enhance(form, async ({ cancel }) => {
@ -28,24 +19,6 @@
}; };
}); });
} }
let freezeTimeInputLocal: string | undefined;
let freezeTimeInput: string | null = null;
$: if (freezeTimeInputLocal !== undefined) {
freezeTimeInput = new Date(freezeTimeInputLocal).toISOString();
}
function repoSelectNone() {
document.querySelectorAll<HTMLInputElement>('.repoCheck').forEach((e) => {
e.checked = false;
});
}
function repoSelectAll() {
document.querySelectorAll<HTMLInputElement>('.repoCheck').forEach((e) => {
e.checked = true;
});
}
</script> </script>
<svelte:head> <svelte:head>
@ -54,68 +27,6 @@
<ConfirmModal bind:this={confirmModal} /> <ConfirmModal bind:this={confirmModal} />
<Modal title="Freeze Time" bind:this={freezeModal}>
<form action="?/freeze-time" method="POST" use:enhance>
<div class="modal-body">
<label class="form-label" for="freezeTimeInput">Freeze At</label>
<input
bind:value={freezeTimeInputLocal}
id="freezeTimeInput"
class="form-control"
type="datetime-local"
/>
<input type="hidden" name="freezeTime" value={freezeTimeInput} />
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => {
freezeModal.hide();
}}>Cancel</button
>
<button type="submit" class="btn btn-success">Set</button>
</div>
</form>
</Modal>
<Modal title="Reset Repos" bind:this={repoModal}>
<form action="?/repo" method="POST" use:enhance>
<div class="modal-body">
<div class="d-flex flex-row gap-2 pb-2">
<button on:click={repoSelectNone} type="button" class="btn btn-sm btn-outline-secondary"
>Select None</button
>
<button on:click={repoSelectAll} type="button" class="btn btn-sm btn-outline-secondary"
>Select All</button
>
</div>
{#each data.teams as team}
<div class="form-check">
<input
name={`teamId${team.id}`}
class="form-check-input repoCheck"
type="checkbox"
value={team.id}
id={`repoCheck${team.id}`}
/>
<label class="form-check-label" for={`repoCheck${team.id}`}>{team.name}</label>
</div>
{/each}
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-outline-secondary"
on:click={() => {
repoModal.hide();
}}>Cancel</button
>
<button type="submit" class="btn btn-warning">Reset Selected</button>
</div>
</form>
</Modal>
<h1 style="text-align:center" class="mb-4"><i class="bi bi-flag"></i> Contest - {data.name}</h1> <h1 style="text-align:center" class="mb-4"><i class="bi bi-flag"></i> Contest - {data.name}</h1>
<FormAlert /> <FormAlert />
@ -129,20 +40,6 @@
<a href="/admin/contests" class="btn btn-outline-primary">All Contests</a> <a href="/admin/contests" class="btn btn-outline-primary">All Contests</a>
</div> </div>
<div class="col-6 text-end"> <div class="col-6 text-end">
<button
type="button"
class="btn btn-outline-info"
on:click={() => {
freezeModal.show();
}}>Set Freeze Time</button
>
<button
type="button"
class="btn btn-outline-warning"
on:click={() => {
repoModal.show();
}}>Reset Repos</button
>
{#if data.activeTeams === 0} {#if data.activeTeams === 0}
<form <form
method="POST" method="POST"
@ -152,11 +49,19 @@
> >
<button type="submit" class="btn btn-danger">Delete</button> <button type="submit" class="btn btn-danger">Delete</button>
</form> </form>
<form
method="POST"
action="?/repo"
class="d-inline"
use:enhanceConfirm={'Are you sure you want to recreate repos? This WILL DELETE ALL DATA on the repos currently.'}
>
<button type="submit" class="btn btn-warning">Recreate Repos</button>
</form>
<form <form
method="POST" method="POST"
action="?/start" action="?/start"
class="d-inline" class="d-inline"
use:enhanceConfirm={'Are you sure you want to start the contest? (THIS WILL DELETE ALL DATA IF THE CONTEST HAS ALREADY BEEN RUN)'} use:enhanceConfirm={'Are you sure you want to start the contest?'}
> >
<button type="submit" class="btn btn-success">Start</button> <button type="submit" class="btn btn-success">Start</button>
</form> </form>

View File

@ -45,10 +45,7 @@ export const actions = {
include: { teams: true, problems: true } include: { teams: true, problems: true }
}); });
await createRepos( await createRepos(createdContest.id);
createdContest.id,
teams.map((t) => t.id)
);
return { success: true }; return { success: true };
} }

View File

@ -153,10 +153,7 @@ export const actions = {
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } }); await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
}); });
await createRepos( await createRepos(contest.id);
contest.id,
fullContest.teams.map((t) => t.id)
);
} }
} }
} catch (err) { } catch (err) {

View File

@ -8,24 +8,9 @@ export const load = (async ({ params }) => {
throw redirect(302, '/public/scoreboard'); throw redirect(302, '/public/scoreboard');
} }
const timestamp = new Date(); const timestamp = new Date();
const contestQuery = await db.contest.findUnique({ where: { id: contestId } });
if (contestQuery === null) {
throw redirect(302, '/public/scoreboard');
}
const contest = await db.contest.findUnique({ const contest = await db.contest.findUnique({
where: { id: contestId }, where: { id: contestId },
include: { include: { problems: true, teams: { include: { submissions: true } } }
problems: true,
teams: {
include: {
submissions:
contestQuery.freezeTime === null
? true
: { where: { createdAt: { lt: contestQuery.freezeTime } } }
}
}
}
}); });
if (contest === null) { if (contest === null) {
throw redirect(302, '/public/scoreboard'); throw redirect(302, '/public/scoreboard');