Contest Import feature

Useful for testing web UI with real data from historical contests
This commit is contained in:
dpoeschl 2024-01-16 12:55:48 -08:00
parent 27d2132bfa
commit 1fc04f7617
4 changed files with 247 additions and 2 deletions

View File

@ -94,7 +94,7 @@ model ActiveTeam {
model Contest { model Contest {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String @unique
teams Team[] @relation("TeamContestRelation") teams Team[] @relation("TeamContestRelation")
problems Problem[] @relation("ProblemContestRelation") problems Problem[] @relation("ProblemContestRelation")
activeTeams ActiveTeam[] activeTeams ActiveTeam[]

View File

@ -11,7 +11,8 @@
<h1 style="text-align:center" class="mb-1"><i class="bi bi-flag"></i> Contests</h1> <h1 style="text-align:center" class="mb-1"><i class="bi bi-flag"></i> Contests</h1>
<div class="d-flex flex-row justify-content-end"> <div class="d-flex flex-row justify-content-end">
<a href="/admin/contests/create" class="btn btn-outline-success">Create</a> <a href="/admin/contests/create" class="btn btn-outline-success m-1">Create</a>
<a href="/admin/contests/import" class="btn btn-outline-success m-1">Import</a>
</div> </div>
<div class="mt-3 table-responsive"> <div class="mt-3 table-responsive">

View File

@ -0,0 +1,167 @@
import { db } from '$lib/server/prisma';
import { Language, SubmissionState } from '@prisma/client';
import type { Actions, PageServerLoad } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import { genPassword } from '../../teams/util';
export const load = (async () => { }) satisfies PageServerLoad;
export type ContestImportData = {
Name: string,
Problems: ProblemImportData[],
Teams: TeamImportData[],
Submissions: SubmissionImportData[]
}
export type ProblemImportData = {
ProblemName: string,
ShortName: string,
SampleInput: string,
SampleOutput: string,
RealInput: string,
RealOutput: string
}
export type TeamImportData = {
TeamName: string
}
export type SubmissionImportData = {
TeamName: string,
ProblemShortName: string,
State: string,
SubmitTime: number,
TeamOutput: string,
Code: string | null,
Language: "Java" | "C#" | "C++" | null
}
export const actions = {
default: async ({ request }) => {
let parsedContest: ContestImportData;
let includeSubmissions: boolean;
try {
const formData = await request.formData();
const contestJson = formData.get('jsonText')?.toString();
if (!contestJson) {
return fail(400, { message: "Could not get json text" });
}
parsedContest = JSON.parse(contestJson);
includeSubmissions = formData.get('includeSubmissions')?.toString() == "on";
}
catch (err) {
return fail(400, { message: "Could not parse contest data: " + err?.toString() });
}
try {
let contestStart: Date | null = null;
let hasSubmissions = false;
if (includeSubmissions && parsedContest.Submissions.length > 0) {
hasSubmissions = true;
const maxSubmitTimeMinutes = Math.max(...parsedContest.Submissions.map(s => s.SubmitTime));
const now = new Date();
contestStart = new Date(now.getTime() - maxSubmitTimeMinutes * 60 * 1000);
}
// Single transaction
await db.contest.create({
data: {
name: parsedContest.Name,
startTime: contestStart,
teams: {
connectOrCreate: parsedContest.Teams.map(team => ({
where: { name: team.TeamName },
create: {
name: team.TeamName,
password: genPassword(),
language: inferTeamLanguage(parsedContest, team) ?? Language.Java
}
})),
},
problems: {
connectOrCreate: parsedContest.Problems.map(problem => ({
where: {
friendlyName: problem.ProblemName,
pascalName: problem.ShortName,
sampleInput: problem.SampleInput,
sampleOutput: problem.SampleOutput,
realInput: problem.RealInput,
realOutput: problem.RealOutput,
},
create: {
friendlyName: problem.ProblemName,
pascalName: problem.ShortName,
sampleInput: problem.SampleInput,
sampleOutput: problem.SampleOutput,
realInput: problem.RealInput,
realOutput: problem.RealOutput
}
})),
},
submissions: {
create: hasSubmissions
? parsedContest.Submissions.toSorted((a, b) => a.SubmitTime - b.SubmitTime).map(submission => ({
createdAt: dateFromContestMinutes(contestStart!, submission.SubmitTime),
gradedAt: dateFromContestMinutes(contestStart!, submission.SubmitTime + 1),
state: convertSubmissionState(submission),
actualOutput: submission.TeamOutput,
commitHash: "",
problem: {
connect: {
pascalName: submission.ProblemShortName
}
},
team: {
connect: {
name: submission.TeamName
}
}
}))
: []
}
}
});
} catch (err) {
return fail(400, { message: "Error updating database: " + err?.toString() });
}
return redirect(303, "/admin/contests");
}
} satisfies Actions;
function convertSubmissionState(submission: SubmissionImportData): SubmissionState {
switch (submission.State) {
case "Correct":
return SubmissionState.Correct;
case "Incorrect":
return SubmissionState.Incorrect;
default:
return SubmissionState.InReview;
}
}
function inferTeamLanguage(parsedContest: ContestImportData, team: TeamImportData): Language | null {
const submissionWithCode = parsedContest.Submissions.find(s => s.TeamName == team.TeamName && s.Code != null);
if (!submissionWithCode) {
return null;
}
switch (submissionWithCode.Language) {
case "Java":
return Language.Java;
case "C#":
return Language.CSharp;
case "C++":
return Language.CPP;
default:
throw new Error("Unrecognized language: " + submissionWithCode.Language);
}
}
function dateFromContestMinutes(contestStart: Date, minutesFromStart: number): Date {
return new Date(contestStart.getTime() + minutesFromStart * 60 * 1000);
}

View File

@ -0,0 +1,77 @@
<script lang="ts">
import { goto } from '$app/navigation';
import FormAlert from '$lib/FormAlert.svelte';
import type { Actions } from './$types';
import type { ContestImportData } from './+page.server';
export let form: Actions;
$: if (form && form.success) {
goto('/admin/contests');
}
let jsonText = '';
let parsesCorrectly: boolean | null = null;
let numProblems: number | null = null;
let numTeams: number | null = null;
let numSubmissions: number | null = null;
$: jsonText, updateUIFromJson();
function updateUIFromJson() {
try {
JSON.parse(jsonText);
const parsedContest: ContestImportData = JSON.parse(jsonText);
numProblems = parsedContest.Problems?.length ?? null;
numTeams = parsedContest.Teams?.length ?? null;
numSubmissions = parsedContest.Submissions?.length ?? null;
parsesCorrectly = numProblems > 0 && numTeams > 0 && parsedContest.Name?.length > 0;
} catch {
numProblems = null;
numTeams = null;
numSubmissions = null;
parsesCorrectly = false;
}
if (jsonText.length == 0) {
parsesCorrectly = null;
}
}
</script>
<svelte:head>
<title>Import Contest</title>
</svelte:head>
<h1 style="text-align:center" class="mb-4"><i class="bi bi-flag"></i> Import Contest</h1>
<FormAlert />
<form method="POST">
<div class="mb-4">
<h3>Contest JSON:</h3>
<textarea
id="jsonTextArea"
name="jsonText"
class="form-control"
rows="10"
bind:value={jsonText}
style="{parsesCorrectly == null ? "" : `border: 2px solid ${parsesCorrectly ? 'green' : 'red'}`}"
/>
</div>
<div class="mb-4">
<h3>Import Info:</h3>
<span>{numTeams ?? 'No'} Teams</span><br />
<span>{numProblems ?? 'No'} Problems</span><br />
<span>{numSubmissions ?? 'No'} Submissions</span>
(<input type="checkbox" checked name="includeSubmissions" id="includeSubmissions" />
<label id="includeSubmissionsLabel" for="includeSubmissions">Include</label>)
</div>
<div class="d-flex flex-row justify-content-end gap-2 m-2">
<a href="/admin/contests" class="btn btn-outline-secondary">Cancel</a>
<button class="btn btn-success">Import</button>
</div>
</form>