From 51ff743d98fd03c11a642d739a82c186e28deabc Mon Sep 17 00:00:00 2001 From: orosmatthew Date: Mon, 8 May 2023 13:58:47 -0400 Subject: [PATCH] [web] Handle auto grading submissions --- sandbox/.gitignore | 3 +- sandbox/package-lock.json | 262 +++++++++++------- sandbox/package.json | 37 +-- sandbox/src/index.ts | 128 +++++++++ sandbox/src/run/java.ts | 21 +- sandbox/tsconfig.json | 6 +- web/prisma/ERD.svg | 2 +- web/prisma/schema.prisma | 7 +- .../admin/diff/[submissionId]/+page.server.ts | 16 +- web/src/routes/admin/reviews/+page.server.ts | 82 +++--- web/src/routes/admin/reviews/+page.svelte | 11 +- web/src/routes/api/submission/+server.ts | 71 +++++ .../api/team/[session]/submit/+server.ts | 45 +++ 13 files changed, 507 insertions(+), 184 deletions(-) create mode 100644 web/src/routes/api/submission/+server.ts create mode 100644 web/src/routes/api/team/[session]/submit/+server.ts diff --git a/sandbox/.gitignore b/sandbox/.gitignore index 76add87..a0d218e 100644 --- a/sandbox/.gitignore +++ b/sandbox/.gitignore @@ -1,2 +1,3 @@ node_modules -dist \ No newline at end of file +dist +.env \ No newline at end of file diff --git a/sandbox/package-lock.json b/sandbox/package-lock.json index 52214a5..bf3adf0 100644 --- a/sandbox/package-lock.json +++ b/sandbox/package-lock.json @@ -1,95 +1,171 @@ { - "name": "sandbox", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "sandbox", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "fs-extra": "^11.1.1" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.1", - "typescript": "^5.0.4" - } - }, - "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/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/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" - } - } - } + "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" + } + } + } } diff --git a/sandbox/package.json b/sandbox/package.json index 1dd1c92..4e49a91 100644 --- a/sandbox/package.json +++ b/sandbox/package.json @@ -1,18 +1,23 @@ { - "name": "sandbox", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "build": "tsc" - }, - "author": "", - "license": "ISC", - "devDependencies": { - "@types/fs-extra": "^11.0.1", - "typescript": "^5.0.4" - }, - "dependencies": { - "fs-extra": "^11.1.1" - } + "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" + } } diff --git a/sandbox/src/index.ts b/sandbox/src/index.ts index e69de29..3c9deba 100644 --- a/sandbox/src/index.ts +++ b/sandbox/src/index.ts @@ -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; + +async function fetchQueuedSubmission(): Promise { + 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(); diff --git a/sandbox/src/run/java.ts b/sandbox/src/run/java.ts index be0756e..c9ab573 100644 --- a/sandbox/src/run/java.ts +++ b/sandbox/src/run/java.ts @@ -8,26 +8,19 @@ import util from 'util'; const execPromise = util.promisify(exec); export async function runJava( - srcDir: string, + javaBinPath: string, + buildDir: string, mainFile: string, mainClass: string, input: string ): Promise { - const javaPath = ''; - if (javaPath == '') { - throw error('Java path not set'); - } - 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}`; + const compileCommand = `${join(javaBinPath, 'javac')} -cp ${join( + buildDir, + 'src' + )} ${mainFile} -d ${join(buildDir, 'build')}`; 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) => { let outputBuffer = ''; diff --git a/sandbox/tsconfig.json b/sandbox/tsconfig.json index 62fe3a2..6694831 100644 --- a/sandbox/tsconfig.json +++ b/sandbox/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "target": "es6", - "module": "CommonJS", + "module": "NodeNext", "sourceMap": true, "outDir": "./dist", - "esModuleInterop": true + "esModuleInterop": true, + "strict": true }, "include": ["./src/**/*"] } diff --git a/web/prisma/ERD.svg b/web/prisma/ERD.svg index 8da27f0..060ed3f 100644 --- a/web/prisma/ERD.svg +++ b/web/prisma/ERD.svg @@ -1 +1 @@ -SubmissionStateQueuedQueuedInReviewInReviewCorrectCorrectIncorrectIncorrectUserIntid🗝️StringusernameStringpasswordSessionStringtoken🗝️DateTimecreatedAtSubmissionIntid🗝️DateTimecreatedAtDateTimegradedAtSubmissionStatestateStringactualOutputStringmessageProblemIntid🗝️StringfriendlyNameStringpascalNameStringsampleInputStringsampleOutputStringrealInputStringrealOutputTeamIntid🗝️StringnameStringpasswordActiveTeamIntid🗝️StringsessionTokenDateTimesessionCreatedAtContestIntid🗝️Stringnamesessionsuserenum:stateteamproblemsubmissionscontestsSubmissioncontestsactiveTeamteamcontestteamsproblemsactiveTeams \ No newline at end of file +SubmissionStateQueuedQueuedInReviewInReviewCorrectCorrectIncorrectIncorrectUserIntid🗝️StringusernameStringpasswordSessionStringtoken🗝️DateTimecreatedAtSubmissionIntid🗝️DateTimecreatedAtDateTimegradedAtSubmissionStatestateStringactualOutputStringcommitHashStringdiffStringmessageProblemIntid🗝️StringfriendlyNameStringpascalNameStringsampleInputStringsampleOutputStringrealInputStringrealOutputTeamIntid🗝️StringnameStringpasswordActiveTeamIntid🗝️StringsessionTokenDateTimesessionCreatedAtContestIntid🗝️Stringnamesessionsuserenum:stateteamproblemcontestsubmissionscontestsSubmissioncontestsactiveTeamteamcontestteamsproblemsactiveTeamssubmissions \ No newline at end of file diff --git a/web/prisma/schema.prisma b/web/prisma/schema.prisma index 7c6f304..bfe14dd 100644 --- a/web/prisma/schema.prisma +++ b/web/prisma/schema.prisma @@ -37,12 +37,16 @@ model Submission { createdAt DateTime @default(now()) gradedAt DateTime? state SubmissionState - actualOutput String + actualOutput String? + commitHash String + diff String? message String? team Team @relation(fields: [teamId], references: [id]) teamId Int problem Problem @relation(fields: [problemId], references: [id]) problemId Int + contestId Int + contest Contest @relation(fields: [contestId], references: [id]) } model Problem { @@ -82,4 +86,5 @@ model Contest { teams Team[] @relation("TeamContestRelation") problems Problem[] @relation("ProblemContestRelation") activeTeams ActiveTeam[] + submissions Submission[] } diff --git a/web/src/routes/admin/diff/[submissionId]/+page.server.ts b/web/src/routes/admin/diff/[submissionId]/+page.server.ts index 4e7c480..fda6c42 100644 --- a/web/src/routes/admin/diff/[submissionId]/+page.server.ts +++ b/web/src/routes/admin/diff/[submissionId]/+page.server.ts @@ -17,12 +17,16 @@ export const load = (async ({ params }) => { if (!problem) { throw error(500, 'Invalid problem'); } - let diff = Diff.createTwoFilesPatch( - 'expected', - 'actual', - problem.realOutput, - submission.actualOutput - ); + let diff: string | undefined; + if (submission.actualOutput) { + diff = Diff.createTwoFilesPatch( + 'expected', + 'actual', + problem.realOutput, + submission.actualOutput + ); + } + return { diff: diff }; }) satisfies PageServerLoad; diff --git a/web/src/routes/admin/reviews/+page.server.ts b/web/src/routes/admin/reviews/+page.server.ts index 47e3059..9487e6d 100644 --- a/web/src/routes/admin/reviews/+page.server.ts +++ b/web/src/routes/admin/reviews/+page.server.ts @@ -19,44 +19,44 @@ export const load = (async () => { }; }) satisfies PageServerLoad; -export const actions = { - submission: async ({ request }) => { - const data = await request.formData(); - const teamId = data.get('teamId'); - const problemId = data.get('problemId'); - const actual = data.get('actual'); - if (!teamId || !problemId || !actual) { - return { success: false }; - } - const problemIdInt = parseInt(problemId.toString()); - const teamIdInt = parseInt(teamId.toString()); - if (isNaN(problemIdInt) || isNaN(teamIdInt)) { - return { success: false }; - } - const problem = await db.problem.findUnique({ where: { id: problemIdInt } }); - if (!problem) { - return { success: false }; - } - if (problem.realOutput === actual.toString()) { - await db.submission.create({ - data: { - state: SubmissionState.Correct, - actualOutput: actual.toString(), - teamId: teamIdInt, - problemId: problemIdInt, - gradedAt: new Date() - } - }); - return { success: true }; - } - await db.submission.create({ - data: { - state: SubmissionState.InReview, - actualOutput: actual.toString(), - teamId: teamIdInt, - problemId: problemIdInt - } - }); - return { success: true }; - } -} satisfies Actions; +// 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; diff --git a/web/src/routes/admin/reviews/+page.svelte b/web/src/routes/admin/reviews/+page.svelte index 7d33b26..bfea196 100644 --- a/web/src/routes/admin/reviews/+page.svelte +++ b/web/src/routes/admin/reviews/+page.svelte @@ -1,12 +1,7 @@ @@ -26,7 +21,7 @@ {/each} -
+ diff --git a/web/src/routes/api/submission/+server.ts b/web/src/routes/api/submission/+server.ts new file mode 100644 index 0000000..f04213a --- /dev/null +++ b/web/src/routes/api/submission/+server.ts @@ -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; diff --git a/web/src/routes/api/team/[session]/submit/+server.ts b/web/src/routes/api/team/[session]/submit/+server.ts new file mode 100644 index 0000000..1ff71f3 --- /dev/null +++ b/web/src/routes/api/team/[session]/submit/+server.ts @@ -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;