Merge branch 'main' of https://github.com/orosmatthew/bw-hspc-contest-env
This commit is contained in:
commit
7df32c63d1
3
sandbox/.gitignore
vendored
Normal file
3
sandbox/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
8
sandbox/.prettierrc
Normal file
8
sandbox/.prettierrc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"pluginSearchDirs": ["."]
|
||||||
|
}
|
171
sandbox/package-lock.json
generated
Normal file
171
sandbox/package-lock.json
generated
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
{
|
||||||
|
"name": "sandbox",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "sandbox",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"simple-git": "^3.18.0",
|
||||||
|
"url-join": "^5.0.0",
|
||||||
|
"zod": "^3.21.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@kwsites/file-exists": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@kwsites/promise-deferred": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
|
||||||
|
},
|
||||||
|
"node_modules/@types/fs-extra": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/jsonfile": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/jsonfile": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "20.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
|
||||||
|
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "4.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
|
||||||
|
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fs-extra": {
|
||||||
|
"version": "11.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
|
||||||
|
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/graceful-fs": {
|
||||||
|
"version": "4.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
|
||||||
|
},
|
||||||
|
"node_modules/jsonfile": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
|
},
|
||||||
|
"node_modules/simple-git": {
|
||||||
|
"version": "3.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.18.0.tgz",
|
||||||
|
"integrity": "sha512-Yt0GJ5aYrpPci3JyrYcsPz8Xc05Hi4JPSOb+Sgn/BmPX35fn/6Fp9Mef8eMBCrL2siY5w4j49TA5Q+bxPpri1Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@kwsites/file-exists": "^1.1.1",
|
||||||
|
"@kwsites/promise-deferred": "^1.1.1",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/universalify": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/url-join": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.21.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||||
|
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
sandbox/package.json
Normal file
23
sandbox/package.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "sandbox",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"simple-git": "^3.18.0",
|
||||||
|
"url-join": "^5.0.0",
|
||||||
|
"zod": "^3.21.4"
|
||||||
|
}
|
||||||
|
}
|
128
sandbox/src/index.ts
Normal file
128
sandbox/src/index.ts
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import urlJoin from 'url-join';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import os from 'os';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { simpleGit, SimpleGit } from 'simple-git';
|
||||||
|
import { runJava } from './run/java.js';
|
||||||
|
|
||||||
|
const submissionGetData = z
|
||||||
|
.object({
|
||||||
|
success: z.boolean(),
|
||||||
|
submission: z
|
||||||
|
.object({
|
||||||
|
id: z.number(),
|
||||||
|
contestId: z.number(),
|
||||||
|
teamId: z.number(),
|
||||||
|
problem: z.object({
|
||||||
|
id: z.number(),
|
||||||
|
pascalName: z.string(),
|
||||||
|
realInput: z.string()
|
||||||
|
}),
|
||||||
|
commitHash: z.string()
|
||||||
|
})
|
||||||
|
.nullable()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
type SubmissionGetData = z.infer<typeof submissionGetData>;
|
||||||
|
|
||||||
|
async function fetchQueuedSubmission(): Promise<SubmissionGetData | undefined> {
|
||||||
|
const res = await fetch(urlJoin(adminUrl, 'api/submission'), { method: 'GET' });
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error('Failed to fetch submission');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const data = submissionGetData.parse(await res.json());
|
||||||
|
if (!data.success) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cloneAndRun(submissionData: SubmissionGetData) {
|
||||||
|
if (!submissionData.submission || !submissionData.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const buildDir = join(tmpDir, 'bwcontest_java');
|
||||||
|
if (fs.existsSync(buildDir)) {
|
||||||
|
fs.removeSync(buildDir);
|
||||||
|
}
|
||||||
|
fs.mkdirSync(buildDir);
|
||||||
|
const repoDir = join(buildDir, 'src');
|
||||||
|
fs.mkdirSync(repoDir);
|
||||||
|
|
||||||
|
const git: SimpleGit = simpleGit({ baseDir: repoDir });
|
||||||
|
await git.clone(
|
||||||
|
urlJoin(
|
||||||
|
repoUrl,
|
||||||
|
submissionData.submission.contestId.toString(),
|
||||||
|
submissionData.submission.teamId.toString() + '.git'
|
||||||
|
),
|
||||||
|
'.'
|
||||||
|
);
|
||||||
|
await git.checkout(submissionData.submission.commitHash);
|
||||||
|
const problemName = submissionData.submission.problem.pascalName;
|
||||||
|
const output = await runJava(
|
||||||
|
javaBinPath,
|
||||||
|
buildDir,
|
||||||
|
join(repoDir, problemName, problemName + '.java'),
|
||||||
|
problemName,
|
||||||
|
submissionData.submission.problem.realInput
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ submissionId: submissionData.submission.id, output: output })
|
||||||
|
});
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error('Failed to POST output');
|
||||||
|
}
|
||||||
|
const data = (await res.json()) as { success: boolean };
|
||||||
|
if (!data.success) {
|
||||||
|
console.error('Output POST unsuccessful');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateEnv(): boolean {
|
||||||
|
return (
|
||||||
|
process.env.ADMIN_URL !== undefined &&
|
||||||
|
process.env.REPO_URL !== undefined &&
|
||||||
|
process.env.JAVA_PATH !== undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
if (!validateEnv()) {
|
||||||
|
throw Error('Invalid environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminUrl = process.env.ADMIN_URL as string;
|
||||||
|
const repoUrl = process.env.REPO_URL as string;
|
||||||
|
const javaBinPath = process.env.JAVA_PATH as string;
|
||||||
|
|
||||||
|
async function loop() {
|
||||||
|
const submissionData = await fetchQueuedSubmission();
|
||||||
|
if (!submissionData) {
|
||||||
|
console.error('Unable to fetch submission data');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
cloneAndRun(submissionData);
|
||||||
|
} catch {
|
||||||
|
console.error('Unable to clone and run');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
while (true) {
|
||||||
|
await loop();
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 15000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
43
sandbox/src/run/java.ts
Normal file
43
sandbox/src/run/java.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
import { join } from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { exec, spawn } from 'child_process';
|
||||||
|
import { error } from 'console';
|
||||||
|
import util from 'util';
|
||||||
|
|
||||||
|
const execPromise = util.promisify(exec);
|
||||||
|
|
||||||
|
export async function runJava(
|
||||||
|
javaBinPath: string,
|
||||||
|
buildDir: string,
|
||||||
|
mainFile: string,
|
||||||
|
mainClass: string,
|
||||||
|
input: string
|
||||||
|
): Promise<string> {
|
||||||
|
const compileCommand = `${join(javaBinPath, 'javac')} -cp ${join(
|
||||||
|
buildDir,
|
||||||
|
'src'
|
||||||
|
)} ${mainFile} -d ${join(buildDir, 'build')}`;
|
||||||
|
await execPromise(compileCommand);
|
||||||
|
|
||||||
|
const runCommand = `${join(javaBinPath, 'java')} -cp "${join(buildDir, 'build')}" ${mainClass}`;
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let outputBuffer = '';
|
||||||
|
const child = spawn(runCommand, { shell: true });
|
||||||
|
child.stdout.setEncoding('utf8');
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
outputBuffer += data.toString();
|
||||||
|
});
|
||||||
|
child.stderr.setEncoding('utf8');
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
outputBuffer += data.toString();
|
||||||
|
});
|
||||||
|
child.stdin.write(input);
|
||||||
|
child.stdin.end();
|
||||||
|
|
||||||
|
child.on('close', () => {
|
||||||
|
resolve(outputBuffer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
10
sandbox/tsconfig.json
Normal file
10
sandbox/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*"]
|
||||||
|
}
|
950
web/package-lock.json
generated
950
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -25,6 +25,7 @@
|
|||||||
"eslint-plugin-svelte3": "^4.0.0",
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"prettier-plugin-svelte": "^2.10.0",
|
"prettier-plugin-svelte": "^2.10.0",
|
||||||
|
"prisma-erd-generator": "^1.7.0",
|
||||||
"svelte": "^3.58.0",
|
"svelte": "^3.58.0",
|
||||||
"svelte-check": "^3.2.0",
|
"svelte-check": "^3.2.0",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
@ -35,10 +36,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^4.13.0",
|
"@prisma/client": "^4.13.0",
|
||||||
"@sveltejs/adapter-node": "^1.2.4",
|
"@sveltejs/adapter-node": "^1.2.4",
|
||||||
|
"@types/fs-extra": "^11.0.1",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"bootstrap": "^5.2.3",
|
"bootstrap": "^5.2.3",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"diff2html": "^3.4.35",
|
"diff2html": "^3.4.35",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
"highlight.js": "^11.8.0",
|
"highlight.js": "^11.8.0",
|
||||||
"memfs": "^3.5.1",
|
"memfs": "^3.5.1",
|
||||||
"node-git-server": "^1.0.0",
|
"node-git-server": "^1.0.0",
|
||||||
|
1
web/prisma/ERD.svg
Normal file
1
web/prisma/ERD.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 47 KiB |
@ -2,6 +2,10 @@ generator client {
|
|||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator erd {
|
||||||
|
provider = "prisma-erd-generator"
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
@ -22,6 +26,7 @@ model Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum SubmissionState {
|
enum SubmissionState {
|
||||||
|
Queued
|
||||||
InReview
|
InReview
|
||||||
Correct
|
Correct
|
||||||
Incorrect
|
Incorrect
|
||||||
@ -32,17 +37,22 @@ model Submission {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
gradedAt DateTime?
|
gradedAt DateTime?
|
||||||
state SubmissionState
|
state SubmissionState
|
||||||
actualOutput String
|
actualOutput String?
|
||||||
|
commitHash String
|
||||||
|
diff String?
|
||||||
message String?
|
message String?
|
||||||
team Team @relation(fields: [teamId], references: [id])
|
team Team @relation(fields: [teamId], references: [id])
|
||||||
teamId Int
|
teamId Int
|
||||||
problem Problem @relation(fields: [problemId], references: [id])
|
problem Problem @relation(fields: [problemId], references: [id])
|
||||||
problemId Int
|
problemId Int
|
||||||
|
contestId Int
|
||||||
|
contest Contest @relation(fields: [contestId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Problem {
|
model Problem {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
friendlyName String @unique
|
friendlyName String @unique
|
||||||
|
pascalName String @unique
|
||||||
sampleInput String
|
sampleInput String
|
||||||
sampleOutput String
|
sampleOutput String
|
||||||
realInput String
|
realInput String
|
||||||
@ -76,4 +86,5 @@ model Contest {
|
|||||||
teams Team[] @relation("TeamContestRelation")
|
teams Team[] @relation("TeamContestRelation")
|
||||||
problems Problem[] @relation("ProblemContestRelation")
|
problems Problem[] @relation("ProblemContestRelation")
|
||||||
activeTeams ActiveTeam[]
|
activeTeams ActiveTeam[]
|
||||||
|
submissions Submission[]
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export function startGitServer() {
|
|||||||
const repoDir = 'repo';
|
const repoDir = 'repo';
|
||||||
|
|
||||||
repos = new Git(join(repoDir), {
|
repos = new Git(join(repoDir), {
|
||||||
autoCreate: true,
|
autoCreate: true
|
||||||
});
|
});
|
||||||
|
|
||||||
repos.on('push', (push) => {
|
repos.on('push', (push) => {
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { error, redirect, type Actions } from '@sveltejs/kit';
|
import { error, redirect, type Actions } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { createRepos } from '../util';
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
export const load = (async ({ params }) => {
|
||||||
const contestId = parseInt(params.contestId);
|
const contestId = parseInt(params.contestId);
|
||||||
@ -9,7 +12,7 @@ export const load = (async ({ params }) => {
|
|||||||
}
|
}
|
||||||
const contest = await db.contest.findUnique({
|
const contest = await db.contest.findUnique({
|
||||||
where: { id: contestId },
|
where: { id: contestId },
|
||||||
include: { problems: true, teams: true }
|
include: { problems: true, teams: true, activeTeams: true }
|
||||||
});
|
});
|
||||||
if (!contest) {
|
if (!contest) {
|
||||||
throw redirect(302, '/admin/contests');
|
throw redirect(302, '/admin/contests');
|
||||||
@ -17,11 +20,12 @@ export const load = (async ({ params }) => {
|
|||||||
return {
|
return {
|
||||||
name: contest.name,
|
name: contest.name,
|
||||||
problems: contest.problems.map((problem) => {
|
problems: contest.problems.map((problem) => {
|
||||||
return { name: problem.friendlyName };
|
return { id: problem.id, name: problem.friendlyName };
|
||||||
}),
|
}),
|
||||||
teams: contest.teams.map((team) => {
|
teams: contest.teams.map((team) => {
|
||||||
return { name: team.name };
|
return { id: team.id, name: team.name };
|
||||||
})
|
}),
|
||||||
|
activeTeams: contest.activeTeams.length
|
||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
@ -36,5 +40,68 @@ export const actions = {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
throw redirect(302, '/admin/contests');
|
throw redirect(302, '/admin/contests');
|
||||||
|
},
|
||||||
|
start: async ({ params }) => {
|
||||||
|
if (!params.contestId) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const contestId = parseInt(params.contestId);
|
||||||
|
if (isNaN(contestId)) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const contest = await db.contest.findUnique({
|
||||||
|
where: { id: contestId },
|
||||||
|
include: { activeTeams: true, teams: { include: { activeTeam: true } } }
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
!contest ||
|
||||||
|
contest.teams.length === 0 ||
|
||||||
|
contest.activeTeams.length !== 0 ||
|
||||||
|
contest.teams.find((team) => {
|
||||||
|
return team.activeTeam;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
contest.teams.forEach(async (team) => {
|
||||||
|
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
stop: async ({ params }) => {
|
||||||
|
if (!params.contestId) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const contestId = parseInt(params.contestId);
|
||||||
|
if (isNaN(contestId)) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const contest = await db.contest.findUnique({
|
||||||
|
where: { id: contestId },
|
||||||
|
include: { activeTeams: true }
|
||||||
|
});
|
||||||
|
if (!contest || contest.activeTeams.length === 0) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
contest.activeTeams.forEach(async (activeTeam) => {
|
||||||
|
await db.activeTeam.delete({ where: { id: activeTeam.id } });
|
||||||
|
});
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
repo: async ({ params }) => {
|
||||||
|
if (!params.contestId) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
const contestId = parseInt(params.contestId);
|
||||||
|
if (isNaN(contestId)) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
if (fs.existsSync(join('repo', contestId.toString()))) {
|
||||||
|
fs.removeSync(join('repo', contestId.toString()));
|
||||||
|
}
|
||||||
|
await createRepos(contestId);
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
export let form: Actions;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -11,27 +12,38 @@
|
|||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">{data.name}</h1>
|
<h1 style="text-align:center" class="mb-4">{data.name}</h1>
|
||||||
|
|
||||||
|
{#if data.activeTeams !== 0}
|
||||||
|
<div class="alert alert-success">In Progress</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if form && !form.success}
|
||||||
|
<div class="alert alert-danger">An error occured</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-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">
|
<div class="col-6 text-end">
|
||||||
<div class="text-end">
|
<form
|
||||||
<form
|
method="POST"
|
||||||
method="POST"
|
use:enhance={({ cancel }) => {
|
||||||
action="?/delete"
|
if (!confirm('Are you sure?')) {
|
||||||
use:enhance={({ cancel }) => {
|
cancel();
|
||||||
if (!confirm('Are you sure?')) {
|
}
|
||||||
cancel();
|
return async ({ update }) => {
|
||||||
}
|
update();
|
||||||
return async ({ update }) => {
|
};
|
||||||
update();
|
}}
|
||||||
};
|
>
|
||||||
}}
|
{#if data.activeTeams === 0}
|
||||||
>
|
<button type="submit" formaction="?/delete" class="btn btn-danger">Delete</button>
|
||||||
<button type="submit" class="btn btn-danger">Delete</button>
|
<button type="submit" formaction="?/repo" class="btn btn-warning">Recreate Repos</button>
|
||||||
</form>
|
<button type="submit" formaction="?/start" class="btn btn-success">Start</button>
|
||||||
</div>
|
{:else}
|
||||||
|
<button type="submit" formaction="?/stop" class="btn btn-outline-danger">Stop</button>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -40,7 +52,9 @@
|
|||||||
<h4>Teams</h4>
|
<h4>Teams</h4>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{#each data.teams as team}
|
{#each data.teams as team}
|
||||||
<div class="list-group-item">{team.name}</div>
|
<a href={`/admin/teams/${team.id}`} class="list-group-item list-group-item-action"
|
||||||
|
>{team.name}</a
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,7 +62,9 @@
|
|||||||
<h4>Problems</h4>
|
<h4>Problems</h4>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{#each data.problems as problem}
|
{#each data.problems as problem}
|
||||||
<div class="list-group-item">{problem.name}</div>
|
<a href={`/admin/problems/${problem.id}`} class="list-group-item list-group-item-action"
|
||||||
|
>{problem.name}</a
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@ import path, { join } from 'path';
|
|||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { simpleGit } from 'simple-git';
|
import { simpleGit } from 'simple-git';
|
||||||
|
import { createRepos } from '../util';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
const teams = await db.team.findMany();
|
const teams = await db.team.findMany();
|
||||||
@ -34,7 +35,7 @@ function copyFolderSync(source: string, target: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
export const actions = {
|
export const actions = {
|
||||||
create: async ({ request, params }) => {
|
create: async ({ request }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const name = data.get('name');
|
const name = data.get('name');
|
||||||
const problems = (await db.problem.findMany()).filter((problem) => {
|
const problems = (await db.problem.findMany()).filter((problem) => {
|
||||||
@ -63,30 +64,8 @@ export const actions = {
|
|||||||
include: { teams: true, problems: true }
|
include: { teams: true, problems: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create repos
|
await createRepos(createdContest.id);
|
||||||
|
|
||||||
if (fs.existsSync('temp')) {
|
|
||||||
fs.rmSync('temp', { recursive: true });
|
|
||||||
}
|
|
||||||
fs.mkdirSync('temp');
|
|
||||||
createdContest.teams.forEach(async (team) => {
|
|
||||||
fs.mkdirSync(join('temp', team.id.toString()));
|
|
||||||
const git = simpleGit({ baseDir: join('temp', team.id.toString()) });
|
|
||||||
await git.init();
|
|
||||||
await git.checkoutLocalBranch('master');
|
|
||||||
createdContest.problems.forEach((problem) => {
|
|
||||||
copyFolderSync(
|
|
||||||
'templates/java/problem',
|
|
||||||
join('temp', team.id.toString(), problem.friendlyName)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
await git.add('.');
|
|
||||||
await git.commit('Initial', { '--author': 'Admin <>' });
|
|
||||||
await git.push(
|
|
||||||
'http://localhost:7006/' + createdContest.id.toString() + '/' + team.id.toString(),
|
|
||||||
'master'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
} satisfies Actions;
|
} satisfies Actions;
|
||||||
|
41
web/src/routes/admin/contests/util.ts
Normal file
41
web/src/routes/admin/contests/util.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import { join } from 'path';
|
||||||
|
import simpleGit from 'simple-git';
|
||||||
|
|
||||||
|
export async function createRepos(contestId: number) {
|
||||||
|
if (fs.existsSync('temp')) {
|
||||||
|
fs.rmSync('temp', { recursive: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync('temp');
|
||||||
|
const contest = await db.contest.findUnique({
|
||||||
|
where: { id: contestId },
|
||||||
|
include: { teams: true, problems: true }
|
||||||
|
});
|
||||||
|
if (!contest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contest.teams.forEach(async (team) => {
|
||||||
|
fs.mkdirSync(join('temp', team.id.toString()));
|
||||||
|
const git = simpleGit({ baseDir: join('temp', team.id.toString()) });
|
||||||
|
await git.init();
|
||||||
|
await git.checkoutLocalBranch('master');
|
||||||
|
contest.problems.forEach((problem) => {
|
||||||
|
fs.mkdirSync(join('temp', team.id.toString(), problem.pascalName));
|
||||||
|
fs.writeFileSync(
|
||||||
|
join('temp', team.id.toString(), problem.pascalName, problem.pascalName + '.java'),
|
||||||
|
`public class ${problem.pascalName} {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
System.out.println("Hello ${problem.pascalName}!");
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await git.add('.');
|
||||||
|
await git.commit('Initial', { '--author': 'Admin <>' });
|
||||||
|
await git.push(
|
||||||
|
'http://localhost:7006/' + contest.id.toString() + '/' + team.id.toString(),
|
||||||
|
'master'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -17,12 +17,16 @@ export const load = (async ({ params }) => {
|
|||||||
if (!problem) {
|
if (!problem) {
|
||||||
throw error(500, 'Invalid problem');
|
throw error(500, 'Invalid problem');
|
||||||
}
|
}
|
||||||
let diff = Diff.createTwoFilesPatch(
|
let diff: string | undefined;
|
||||||
'expected',
|
if (submission.actualOutput) {
|
||||||
'actual',
|
diff = Diff.createTwoFilesPatch(
|
||||||
problem.realOutput,
|
'expected',
|
||||||
submission.actualOutput
|
'actual',
|
||||||
);
|
problem.realOutput,
|
||||||
|
submission.actualOutput
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return { diff: diff };
|
return { diff: diff };
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
@ -22,17 +22,19 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const name = data.get('name');
|
const name = data.get('name');
|
||||||
|
const pascalName = data.get('pascalName');
|
||||||
const sampleInput = data.get('sampleInput');
|
const sampleInput = data.get('sampleInput');
|
||||||
const sampleOutput = data.get('sampleOutput');
|
const sampleOutput = data.get('sampleOutput');
|
||||||
const realInput = data.get('realInput');
|
const realInput = data.get('realInput');
|
||||||
const realOutput = data.get('realOutput');
|
const realOutput = data.get('realOutput');
|
||||||
if (!name || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
if (!name || !pascalName || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.problem.update({
|
await db.problem.update({
|
||||||
where: { id: problemId },
|
where: { id: problemId },
|
||||||
data: {
|
data: {
|
||||||
|
pascalName: pascalName.toString(),
|
||||||
friendlyName: name.toString(),
|
friendlyName: name.toString(),
|
||||||
sampleInput: sampleInput.toString(),
|
sampleInput: sampleInput.toString(),
|
||||||
sampleOutput: sampleOutput.toString(),
|
sampleOutput: sampleOutput.toString(),
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="/admin/problems" class="btn btn-outline-primary">Back</a>
|
<a href="/admin/problems" class="btn btn-outline-primary">All Problems</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 text-end">
|
<div class="col-6 text-end">
|
||||||
<button on:click={deleteProblem} type="button" class="btn btn-danger">Delete</button>
|
<button on:click={deleteProblem} type="button" class="btn btn-danger">Delete</button>
|
||||||
@ -61,8 +61,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form method="POST" action="?/edit">
|
<form method="POST" action="?/edit">
|
||||||
<h4 style="text-align:center" class="mt-3">Name</h4>
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
<h4 style="text-align:center" class="mt-3">Name</h4>
|
||||||
<div class="col-md-auto">
|
<div class="col-md-auto">
|
||||||
<textarea
|
<textarea
|
||||||
name="name"
|
name="name"
|
||||||
@ -71,6 +71,15 @@
|
|||||||
disabled={!editing}
|
disabled={!editing}
|
||||||
use:stretchTextarea>{data.problemData.friendlyName}</textarea
|
use:stretchTextarea>{data.problemData.friendlyName}</textarea
|
||||||
>
|
>
|
||||||
|
<h4 style="text-align:center" class="mt-3">PascalCase Name (for filenames)</h4>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<input
|
||||||
|
value={data.problemData.pascalName}
|
||||||
|
disabled={!editing}
|
||||||
|
name="pascalName"
|
||||||
|
class="form-control"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
||||||
|
@ -5,23 +5,29 @@ export const actions = {
|
|||||||
create: async ({ request }) => {
|
create: async ({ request }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
const name = data.get('name');
|
const name = data.get('name');
|
||||||
|
const pascalName = data.get('pascalName');
|
||||||
const sampleInput = data.get('sampleInput');
|
const sampleInput = data.get('sampleInput');
|
||||||
const sampleOutput = data.get('sampleOutput');
|
const sampleOutput = data.get('sampleOutput');
|
||||||
const realInput = data.get('realInput');
|
const realInput = data.get('realInput');
|
||||||
const realOutput = data.get('realOutput');
|
const realOutput = data.get('realOutput');
|
||||||
if (!name || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
if (!name || !pascalName || !sampleInput || !sampleOutput || !realInput || !realOutput) {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.problem.create({
|
try {
|
||||||
data: {
|
await db.problem.create({
|
||||||
friendlyName: name.toString(),
|
data: {
|
||||||
sampleInput: sampleInput.toString(),
|
pascalName: pascalName.toString(),
|
||||||
sampleOutput: sampleOutput.toString(),
|
friendlyName: name.toString(),
|
||||||
realInput: realInput.toString(),
|
sampleInput: sampleInput.toString(),
|
||||||
realOutput: realOutput.toString()
|
sampleOutput: sampleOutput.toString(),
|
||||||
}
|
realInput: realInput.toString(),
|
||||||
});
|
realOutput: realOutput.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
@ -27,11 +27,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<form method="POST" action="?/create" use:enhance>
|
<form method="POST" action="?/create" use:enhance>
|
||||||
<h4 style="text-align:center" class="mt-3">Name</h4>
|
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
<h4 style="text-align:center" class="mt-3">Name</h4>
|
||||||
<div class="col-md-auto">
|
<div class="col-md-auto">
|
||||||
<textarea name="name" class="form-control" />
|
<textarea name="name" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
|
<h4 style="text-align:center" class="mt-3">PascalCase Name (for filenames)</h4>
|
||||||
|
<div class="col-md-auto">
|
||||||
|
<input name="pascalName" class="form-control" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
<h4 style="text-align:center" class="mt-5">Sample Data</h4>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -19,44 +19,44 @@ export const load = (async () => {
|
|||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
export const actions = {
|
// export const actions = {
|
||||||
submission: async ({ request }) => {
|
// submission: async ({ request }) => {
|
||||||
const data = await request.formData();
|
// const data = await request.formData();
|
||||||
const teamId = data.get('teamId');
|
// const teamId = data.get('teamId');
|
||||||
const problemId = data.get('problemId');
|
// const problemId = data.get('problemId');
|
||||||
const actual = data.get('actual');
|
// const actual = data.get('actual');
|
||||||
if (!teamId || !problemId || !actual) {
|
// if (!teamId || !problemId || !actual) {
|
||||||
return { success: false };
|
// return { success: false };
|
||||||
}
|
// }
|
||||||
const problemIdInt = parseInt(problemId.toString());
|
// const problemIdInt = parseInt(problemId.toString());
|
||||||
const teamIdInt = parseInt(teamId.toString());
|
// const teamIdInt = parseInt(teamId.toString());
|
||||||
if (isNaN(problemIdInt) || isNaN(teamIdInt)) {
|
// if (isNaN(problemIdInt) || isNaN(teamIdInt)) {
|
||||||
return { success: false };
|
// return { success: false };
|
||||||
}
|
// }
|
||||||
const problem = await db.problem.findUnique({ where: { id: problemIdInt } });
|
// const problem = await db.problem.findUnique({ where: { id: problemIdInt } });
|
||||||
if (!problem) {
|
// if (!problem) {
|
||||||
return { success: false };
|
// return { success: false };
|
||||||
}
|
// }
|
||||||
if (problem.realOutput === actual.toString()) {
|
// if (problem.realOutput === actual.toString()) {
|
||||||
await db.submission.create({
|
// await db.submission.create({
|
||||||
data: {
|
// data: {
|
||||||
state: SubmissionState.Correct,
|
// state: SubmissionState.Correct,
|
||||||
actualOutput: actual.toString(),
|
// actualOutput: actual.toString(),
|
||||||
teamId: teamIdInt,
|
// teamId: teamIdInt,
|
||||||
problemId: problemIdInt,
|
// problemId: problemIdInt,
|
||||||
gradedAt: new Date()
|
// gradedAt: new Date()
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
return { success: true };
|
// return { success: true };
|
||||||
}
|
// }
|
||||||
await db.submission.create({
|
// await db.submission.create({
|
||||||
data: {
|
// data: {
|
||||||
state: SubmissionState.InReview,
|
// state: SubmissionState.InReview,
|
||||||
actualOutput: actual.toString(),
|
// actualOutput: actual.toString(),
|
||||||
teamId: teamIdInt,
|
// teamId: teamIdInt,
|
||||||
problemId: problemIdInt
|
// problemId: problemIdInt
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
return { success: true };
|
// return { success: true };
|
||||||
}
|
// }
|
||||||
} satisfies Actions;
|
// } satisfies Actions;
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import type { PageData } from './$types';
|
||||||
import type { ActionData, PageData } from './$types';
|
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
|
||||||
|
|
||||||
let selectedTeam: (typeof data.teams)[0] | null;
|
|
||||||
let selectedProblem: (typeof data.problems)[0] | null;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -26,7 +21,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr />
|
<!-- <hr />
|
||||||
<h2>For Testing Purposes - Create Fake Submission</h2>
|
<h2>For Testing Purposes - Create Fake Submission</h2>
|
||||||
{#if form && !form.success}
|
{#if form && !form.success}
|
||||||
<div class="alert alert-danger">Invalid Submission</div>
|
<div class="alert alert-danger">Invalid Submission</div>
|
||||||
@ -87,4 +82,4 @@
|
|||||||
<button type="submit" class="mt-3 btn btn-secondary">Submit</button>
|
<button type="submit" class="mt-3 btn btn-secondary">Submit</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form> -->
|
||||||
|
28
web/src/routes/api/contest/[session]/+server.ts
Normal file
28
web/src/routes/api/contest/[session]/+server.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
|
||||||
|
export const GET = (async ({ params }) => {
|
||||||
|
const session = params.session;
|
||||||
|
const activeTeam = await db.activeTeam.findUnique({
|
||||||
|
where: { sessionToken: session },
|
||||||
|
include: { contest: { include: { problems: true } } }
|
||||||
|
});
|
||||||
|
if (!activeTeam) {
|
||||||
|
return json({ success: false });
|
||||||
|
}
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
contestId: activeTeam.contestId,
|
||||||
|
teamId: activeTeam.teamId,
|
||||||
|
problems: activeTeam.contest.problems.map((problem) => {
|
||||||
|
return {
|
||||||
|
id: problem.id,
|
||||||
|
name: problem.friendlyName,
|
||||||
|
pascalName: problem.pascalName,
|
||||||
|
sampleInput: problem.sampleInput,
|
||||||
|
sampleOutput: problem.sampleOutput
|
||||||
|
};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}) satisfies RequestHandler;
|
76
web/src/routes/api/submission/+server.ts
Normal file
76
web/src/routes/api/submission/+server.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import { SubmissionState } from '@prisma/client';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import * as Diff from 'diff';
|
||||||
|
|
||||||
|
export const GET = (async () => {
|
||||||
|
const submissions = await db.submission.findMany({
|
||||||
|
where: { state: SubmissionState.Queued },
|
||||||
|
orderBy: { createdAt: 'asc' },
|
||||||
|
include: { problem: true },
|
||||||
|
take: 1
|
||||||
|
});
|
||||||
|
if (submissions.length !== 0) {
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
submission: {
|
||||||
|
id: submissions[0].id,
|
||||||
|
contestId: submissions[0].contestId,
|
||||||
|
teamId: submissions[0].teamId,
|
||||||
|
problem: {
|
||||||
|
id: submissions[0].problemId,
|
||||||
|
pascalName: submissions[0].problem.pascalName,
|
||||||
|
realInput: submissions[0].problem.realInput
|
||||||
|
},
|
||||||
|
commitHash: submissions[0].commitHash
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return json({ success: true, submission: null });
|
||||||
|
}
|
||||||
|
}) satisfies RequestHandler;
|
||||||
|
|
||||||
|
const submissionPostData = z
|
||||||
|
.object({
|
||||||
|
submissionId: z.number(),
|
||||||
|
output: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export const POST = (async ({ request }) => {
|
||||||
|
const data = submissionPostData.safeParse(await request.json());
|
||||||
|
if (!data.success) {
|
||||||
|
throw error(400);
|
||||||
|
}
|
||||||
|
const submission = await db.submission.findUnique({
|
||||||
|
where: { id: data.data.submissionId },
|
||||||
|
include: { problem: true }
|
||||||
|
});
|
||||||
|
if (!submission) {
|
||||||
|
return json({ success: false });
|
||||||
|
}
|
||||||
|
if (submission.state !== SubmissionState.Queued) {
|
||||||
|
return json({ success: false });
|
||||||
|
}
|
||||||
|
if (data.data.output.trimEnd() === submission.problem.realOutput.trimEnd()) {
|
||||||
|
await db.submission.update({
|
||||||
|
where: { id: data.data.submissionId },
|
||||||
|
data: { state: SubmissionState.Correct, gradedAt: new Date(), actualOutput: data.data.output }
|
||||||
|
});
|
||||||
|
return json({ success: true });
|
||||||
|
} else {
|
||||||
|
const diff = Diff.createTwoFilesPatch(
|
||||||
|
'expected',
|
||||||
|
'actual',
|
||||||
|
data.data.output,
|
||||||
|
submission.actualOutput!
|
||||||
|
);
|
||||||
|
await db.submission.update({
|
||||||
|
where: { id: data.data.submissionId },
|
||||||
|
data: { state: SubmissionState.InReview, diff: diff, actualOutput: data.data.output }
|
||||||
|
});
|
||||||
|
return json({ success: true });
|
||||||
|
}
|
||||||
|
}) satisfies RequestHandler;
|
45
web/src/routes/api/team/[session]/submit/+server.ts
Normal file
45
web/src/routes/api/team/[session]/submit/+server.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
import { error, json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { SubmissionState } from '@prisma/client';
|
||||||
|
|
||||||
|
const submitPostData = z.object({
|
||||||
|
commitHash: z.string(),
|
||||||
|
problemId: z.number()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST = (async ({ params, request }) => {
|
||||||
|
const sessionToken = params.session;
|
||||||
|
const activeTeam = await db.activeTeam.findUnique({
|
||||||
|
where: { sessionToken: sessionToken },
|
||||||
|
include: { contest: { include: { problems: { select: { id: true } } } } }
|
||||||
|
});
|
||||||
|
if (!activeTeam) {
|
||||||
|
throw error(400);
|
||||||
|
}
|
||||||
|
const data = submitPostData.safeParse(await request.json());
|
||||||
|
if (!data.success) {
|
||||||
|
throw error(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!activeTeam.contest.problems.find((problem) => {
|
||||||
|
return problem.id == data.data.problemId;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw error(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.submission.create({
|
||||||
|
data: {
|
||||||
|
state: SubmissionState.Queued,
|
||||||
|
commitHash: data.data.commitHash,
|
||||||
|
teamId: activeTeam.teamId,
|
||||||
|
problemId: data.data.problemId,
|
||||||
|
contestId: activeTeam.contestId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return json({ success: true });
|
||||||
|
}) satisfies RequestHandler;
|
Loading…
Reference in New Issue
Block a user