[web] Handle auto grading submissions

This commit is contained in:
orosmatthew 2023-05-08 13:58:47 -04:00
parent 008d93704d
commit 51ff743d98
13 changed files with 507 additions and 184 deletions

3
sandbox/.gitignore vendored
View File

@ -1,2 +1,3 @@
node_modules node_modules
dist dist
.env

View File

@ -1,95 +1,171 @@
{ {
"name": "sandbox", "name": "sandbox",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "sandbox", "name": "sandbox",
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs-extra": "^11.1.1" "dotenv": "^16.0.3",
}, "fs-extra": "^11.1.1",
"devDependencies": { "simple-git": "^3.18.0",
"@types/fs-extra": "^11.0.1", "url-join": "^5.0.0",
"typescript": "^5.0.4" "zod": "^3.21.4"
} },
}, "devDependencies": {
"node_modules/@types/fs-extra": { "@types/fs-extra": "^11.0.1",
"version": "11.0.1", "typescript": "^5.0.4"
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz", }
"integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==", },
"dev": true, "node_modules/@kwsites/file-exists": {
"dependencies": { "version": "1.1.1",
"@types/jsonfile": "*", "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
"@types/node": "*" "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
} "dependencies": {
}, "debug": "^4.1.1"
"node_modules/@types/jsonfile": { }
"version": "6.1.1", },
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz", "node_modules/@kwsites/promise-deferred": {
"integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==", "version": "1.1.1",
"dev": true, "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
"dependencies": { "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="
"@types/node": "*" },
} "node_modules/@types/fs-extra": {
}, "version": "11.0.1",
"node_modules/@types/node": { "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
"version": "20.1.0", "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz", "dev": true,
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dependencies": {
"dev": true "@types/jsonfile": "*",
}, "@types/node": "*"
"node_modules/fs-extra": { }
"version": "11.1.1", },
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", "node_modules/@types/jsonfile": {
"integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "version": "6.1.1",
"dependencies": { "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
"graceful-fs": "^4.2.0", "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
"jsonfile": "^6.0.1", "dev": true,
"universalify": "^2.0.0" "dependencies": {
}, "@types/node": "*"
"engines": { }
"node": ">=14.14" },
} "node_modules/@types/node": {
}, "version": "20.1.0",
"node_modules/graceful-fs": { "resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
"version": "4.2.11", "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "dev": true
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" },
}, "node_modules/debug": {
"node_modules/jsonfile": { "version": "4.3.4",
"version": "6.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dependencies": {
"dependencies": { "ms": "2.1.2"
"universalify": "^2.0.0" },
}, "engines": {
"optionalDependencies": { "node": ">=6.0"
"graceful-fs": "^4.1.6" },
} "peerDependenciesMeta": {
}, "supports-color": {
"node_modules/typescript": { "optional": true
"version": "5.0.4", }
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", }
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", },
"dev": true, "node_modules/dotenv": {
"bin": { "version": "16.0.3",
"tsc": "bin/tsc", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"tsserver": "bin/tsserver" "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
}, "engines": {
"engines": { "node": ">=12"
"node": ">=12.20" }
} },
}, "node_modules/fs-extra": {
"node_modules/universalify": { "version": "11.1.1",
"version": "2.0.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dependencies": {
"engines": { "graceful-fs": "^4.2.0",
"node": ">= 10.0.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"
}
}
}
} }

View File

@ -1,18 +1,23 @@
{ {
"name": "sandbox", "name": "sandbox",
"version": "1.0.0", "version": "1.0.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "type": "module",
"build": "tsc" "scripts": {
}, "build": "tsc"
"author": "", },
"license": "ISC", "author": "",
"devDependencies": { "license": "ISC",
"@types/fs-extra": "^11.0.1", "devDependencies": {
"typescript": "^5.0.4" "@types/fs-extra": "^11.0.1",
}, "typescript": "^5.0.4"
"dependencies": { },
"fs-extra": "^11.1.1" "dependencies": {
} "dotenv": "^16.0.3",
"fs-extra": "^11.1.1",
"simple-git": "^3.18.0",
"url-join": "^5.0.0",
"zod": "^3.21.4"
}
} }

View 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();

View File

@ -8,26 +8,19 @@ import util from 'util';
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);
export async function runJava( export async function runJava(
srcDir: string, javaBinPath: string,
buildDir: string,
mainFile: string, mainFile: string,
mainClass: string, mainClass: string,
input: string input: string
): Promise<string> { ): Promise<string> {
const javaPath = ''; const compileCommand = `${join(javaBinPath, 'javac')} -cp ${join(
if (javaPath == '') { buildDir,
throw error('Java path not set'); 'src'
} )} ${mainFile} -d ${join(buildDir, 'build')}`;
const tempDir = os.tmpdir();
const buildDir = join(tempDir, 'bwcontest_java');
if (fs.existsSync(buildDir)) {
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
const compileCommand = `${join(javaPath, 'javac')} -cp ${srcDir} ${mainFile} -d ${buildDir}`;
await execPromise(compileCommand); await execPromise(compileCommand);
const runCommand = `${join(javaPath, 'java')} -cp "${buildDir}" ${mainClass}`; const runCommand = `${join(javaBinPath, 'java')} -cp "${join(buildDir, 'build')}" ${mainClass}`;
return new Promise((resolve) => { return new Promise((resolve) => {
let outputBuffer = ''; let outputBuffer = '';

View File

@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "module": "NodeNext",
"module": "CommonJS",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
"esModuleInterop": true "esModuleInterop": true,
"strict": true
}, },
"include": ["./src/**/*"] "include": ["./src/**/*"]
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 47 KiB

View File

@ -37,12 +37,16 @@ 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 {
@ -82,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[]
} }

View File

@ -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;

View File

@ -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;

View File

@ -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> -->

View File

@ -0,0 +1,71 @@
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.update({
where: { id: data.data.submissionId },
data: { actualOutput: data.data.output },
include: { problem: true }
});
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() }
});
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 }
});
return json({ success: true });
}
}) satisfies RequestHandler;

View 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;