Contest Import feature
Useful for testing web UI with real data from historical contests
This commit is contained in:
parent
27d2132bfa
commit
1fc04f7617
@ -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[]
|
||||||
|
@ -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">
|
||||||
|
167
web/src/routes/admin/contests/import/+page.server.ts
Normal file
167
web/src/routes/admin/contests/import/+page.server.ts
Normal 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);
|
||||||
|
}
|
77
web/src/routes/admin/contests/import/+page.svelte
Normal file
77
web/src/routes/admin/contests/import/+page.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user