Merge branch 'main' of https://github.com/orosmatthew/bw-hspc-contest-env
This commit is contained in:
commit
8564dd26ec
39
sandbox/Dockerfile
Normal file
39
sandbox/Dockerfile
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update
|
||||||
|
|
||||||
|
RUN apt-get install curl -y
|
||||||
|
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get install -y nodejs
|
||||||
|
|
||||||
|
RUN apt-get install git -y
|
||||||
|
|
||||||
|
RUN git config --global user.name "Admin"
|
||||||
|
|
||||||
|
RUN git config --global user.email noemail@example.com
|
||||||
|
|
||||||
|
WORKDIR /opt
|
||||||
|
|
||||||
|
RUN apt-get install wget -y
|
||||||
|
|
||||||
|
RUN wget -O java.tar.gz https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.7%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz
|
||||||
|
|
||||||
|
RUN tar -xzvf java.tar.gz
|
||||||
|
|
||||||
|
RUN rm java.tar.gz
|
||||||
|
|
||||||
|
ENV JAVA_PATH=/opt/jdk-17.0.7+7/bin
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
CMD ["node", "dist"]
|
8
sandbox/docker-compose.yml
Normal file
8
sandbox/docker-compose.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
sandbox:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
- ADMIN_URL=http://localhost:5173
|
||||||
|
- REPO_URL=http://localhost:7006
|
||||||
|
network_mode: 'host'
|
@ -65,13 +65,18 @@ async function cloneAndRun(submissionData: SubmissionGetData) {
|
|||||||
);
|
);
|
||||||
await git.checkout(submissionData.submission.commitHash);
|
await git.checkout(submissionData.submission.commitHash);
|
||||||
const problemName = submissionData.submission.problem.pascalName;
|
const problemName = submissionData.submission.problem.pascalName;
|
||||||
const output = await runJava(
|
let output: string;
|
||||||
|
try {
|
||||||
|
output = await runJava(
|
||||||
javaBinPath,
|
javaBinPath,
|
||||||
buildDir,
|
buildDir,
|
||||||
join(repoDir, problemName, problemName + '.java'),
|
join(repoDir, problemName, problemName + '.java'),
|
||||||
problemName,
|
problemName,
|
||||||
submissionData.submission.problem.realInput
|
submissionData.submission.problem.realInput
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
output = `[An error occurred while running]\n${error}`;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
|
const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -106,7 +111,14 @@ const repoUrl = process.env.REPO_URL as string;
|
|||||||
const javaBinPath = process.env.JAVA_PATH as string;
|
const javaBinPath = process.env.JAVA_PATH as string;
|
||||||
|
|
||||||
async function loop() {
|
async function loop() {
|
||||||
const submissionData = await fetchQueuedSubmission();
|
let submissionData: SubmissionGetData | undefined;
|
||||||
|
try {
|
||||||
|
submissionData = await fetchQueuedSubmission();
|
||||||
|
} catch {
|
||||||
|
console.error('Failed to fetch submission');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!submissionData) {
|
if (!submissionData) {
|
||||||
console.error('Unable to fetch submission data');
|
console.error('Unable to fetch submission data');
|
||||||
} else {
|
} else {
|
||||||
@ -121,7 +133,7 @@ async function loop() {
|
|||||||
async function run() {
|
async function run() {
|
||||||
while (true) {
|
while (true) {
|
||||||
await loop();
|
await loop();
|
||||||
await new Promise((resolve) => setTimeout(resolve, 15000));
|
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import fs from 'fs-extra';
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import os from 'os';
|
|
||||||
import { exec, spawn } from 'child_process';
|
import { exec, spawn } from 'child_process';
|
||||||
import { error } from 'console';
|
|
||||||
import util from 'util';
|
import util from 'util';
|
||||||
|
|
||||||
const execPromise = util.promisify(exec);
|
const execPromise = util.promisify(exec);
|
||||||
@ -36,8 +33,22 @@ export async function runJava(
|
|||||||
child.stdin.write(input);
|
child.stdin.write(input);
|
||||||
child.stdin.end();
|
child.stdin.end();
|
||||||
|
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
child.on('close', () => {
|
child.on('close', () => {
|
||||||
|
if (!resolved) {
|
||||||
|
resolved = true;
|
||||||
resolve(outputBuffer);
|
resolve(outputBuffer);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!resolved) {
|
||||||
|
console.log('30 seconds reached, killing process');
|
||||||
|
resolved = true;
|
||||||
|
child.kill('SIGKILL');
|
||||||
|
resolve(outputBuffer + '\n[Timeout after 30 seconds]');
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
@ -64,7 +64,7 @@ model Problem {
|
|||||||
model Team {
|
model Team {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
Submission Submission[]
|
submissions Submission[]
|
||||||
contests Contest[] @relation("TeamContestRelation")
|
contests Contest[] @relation("TeamContestRelation")
|
||||||
password String
|
password String
|
||||||
activeTeam ActiveTeam?
|
activeTeam ActiveTeam?
|
||||||
@ -87,4 +87,5 @@ model Contest {
|
|||||||
problems Problem[] @relation("ProblemContestRelation")
|
problems Problem[] @relation("ProblemContestRelation")
|
||||||
activeTeams ActiveTeam[]
|
activeTeams ActiveTeam[]
|
||||||
submissions Submission[]
|
submissions Submission[]
|
||||||
|
startTime DateTime?
|
||||||
}
|
}
|
||||||
|
3
web/src/lib/util.ts
Normal file
3
web/src/lib/util.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function stretchTextarea(textarea: HTMLTextAreaElement) {
|
||||||
|
textarea.style.height = textarea.scrollHeight + 'px';
|
||||||
|
}
|
@ -4,7 +4,7 @@
|
|||||||
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li>
|
<li><a href="/admin/reviews" class="nav-link px-2">Reviews</a></li>
|
||||||
<li><a href="/admin/submissions" class="nav-link px-2">Submissions</a></li>
|
<li><a href="/admin/submissions" class="nav-link px-2">Submissions</a></li>
|
||||||
<li><a href="/admin/problems" class="nav-link px-2">Problems</a></li>
|
<li><a href="/admin/problems" class="nav-link px-2">Problems</a></li>
|
||||||
<li><a href="/admin/scoreboard" class="nav-link px-2">Scoreboard</a></li>
|
<li><a href="/admin/scoreboard" class="nav-link px-2">Scoreboards</a></li>
|
||||||
<li><a href="/admin/teams" class="nav-link px-2">Teams</a></li>
|
<li><a href="/admin/teams" class="nav-link px-2">Teams</a></li>
|
||||||
<li><a href="/admin/contests" class="nav-link px-2">Contests</a></li>
|
<li><a href="/admin/contests" class="nav-link px-2">Contests</a></li>
|
||||||
<li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>
|
<li><a href="/logout" class="nav-link px-2" data-sveltekit-preload-data="off">Logout</a></li>
|
||||||
|
@ -2,10 +2,10 @@ import { db } from '$lib/server/prisma';
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
const contests = await db.contest.findMany();
|
const contests = await db.contest.findMany({ include: { activeTeams: true } });
|
||||||
return {
|
return {
|
||||||
contests: contests.map((contest) => {
|
contests: contests.map((contest) => {
|
||||||
return { id: contest.id, name: contest.name };
|
return { id: contest.id, name: contest.name, activeTeams: contest.activeTeams.length };
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
@ -20,7 +20,9 @@
|
|||||||
{#each data.contests as contest}
|
{#each data.contests as contest}
|
||||||
<a
|
<a
|
||||||
href={'/admin/contests/' + contest.id.toString()}
|
href={'/admin/contests/' + contest.id.toString()}
|
||||||
class="list-group-item list-group-item-action">{contest.name}</a
|
class={`list-group-item list-group-item-action ${
|
||||||
|
contest.activeTeams === 0 ? '' : ' list-group-item-success'
|
||||||
|
}`}>{contest.name}</a
|
||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,10 +64,14 @@ export const actions = {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db.submission.deleteMany({ where: { contestId: contest.id } });
|
||||||
|
|
||||||
contest.teams.forEach(async (team) => {
|
contest.teams.forEach(async (team) => {
|
||||||
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
|
await db.activeTeam.create({ data: { teamId: team.id, contestId: contest.id } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await db.contest.update({ where: { id: contestId }, data: { startTime: new Date() } });
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
},
|
},
|
||||||
stop: async ({ params }) => {
|
stop: async ({ params }) => {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
@ -50,6 +51,7 @@
|
|||||||
<div class="mt-3 row">
|
<div class="mt-3 row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<h4>Teams</h4>
|
<h4>Teams</h4>
|
||||||
|
<a href={`${$page.url}/logins`} class="mb-2 btn btn-outline-secondary">Printable Logins</a>
|
||||||
<div class="list-group">
|
<div class="list-group">
|
||||||
{#each data.teams as team}
|
{#each data.teams as team}
|
||||||
<a href={`/admin/teams/${team.id}`} class="list-group-item list-group-item-action"
|
<a href={`/admin/teams/${team.id}`} class="list-group-item list-group-item-action"
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { db } from '$lib/server/prisma';
|
||||||
|
|
||||||
|
export const load = (async ({ params }) => {
|
||||||
|
if (!params.contestId) {
|
||||||
|
throw redirect(302, '/admin/contests');
|
||||||
|
}
|
||||||
|
const contestId = parseInt(params.contestId);
|
||||||
|
if (isNaN(contestId)) {
|
||||||
|
throw redirect(302, '/admin/contests');
|
||||||
|
}
|
||||||
|
|
||||||
|
const contest = await db.contest.findUnique({
|
||||||
|
where: { id: contestId },
|
||||||
|
include: { teams: true }
|
||||||
|
});
|
||||||
|
if (!contest) {
|
||||||
|
throw redirect(302, '/admin/contests');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
teams: contest.teams.map((team) => {
|
||||||
|
return {
|
||||||
|
id: team.id,
|
||||||
|
name: team.name,
|
||||||
|
password: team.password
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}) satisfies PageServerLoad;
|
@ -0,0 +1,20 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each data.teams as team}
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Team Name</th>
|
||||||
|
<th>Password</th></tr
|
||||||
|
>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>{team.id}</td><td>{team.name}</td><td>{team.password}</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/each}
|
@ -1,8 +1,5 @@
|
|||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
import path, { join } from 'path';
|
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import fs from 'fs';
|
|
||||||
import { simpleGit } from 'simple-git';
|
|
||||||
import { createRepos } from '../util';
|
import { createRepos } from '../util';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
@ -18,22 +15,6 @@ export const load = (async () => {
|
|||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
function copyFolderSync(source: string, target: string) {
|
|
||||||
if (!fs.existsSync(target)) {
|
|
||||||
fs.mkdirSync(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.readdirSync(source).forEach((file) => {
|
|
||||||
const sourcePath = path.join(source, file);
|
|
||||||
const targetPath = path.join(target, file);
|
|
||||||
|
|
||||||
if (fs.lstatSync(sourcePath).isDirectory()) {
|
|
||||||
copyFolderSync(sourcePath, targetPath);
|
|
||||||
} else {
|
|
||||||
fs.copyFileSync(sourcePath, targetPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
create: async ({ request }) => {
|
create: async ({ request }) => {
|
||||||
const data = await request.formData();
|
const data = await request.formData();
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import * as Diff from 'diff';
|
|
||||||
import { error, redirect } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
import { SubmissionState } from '@prisma/client';
|
import { SubmissionState } from '@prisma/client';
|
||||||
@ -17,17 +16,8 @@ export const load = (async ({ params }) => {
|
|||||||
if (!problem) {
|
if (!problem) {
|
||||||
throw error(500, 'Invalid problem');
|
throw error(500, 'Invalid problem');
|
||||||
}
|
}
|
||||||
let diff: string | undefined;
|
|
||||||
if (submission.actualOutput) {
|
|
||||||
diff = Diff.createTwoFilesPatch(
|
|
||||||
'expected',
|
|
||||||
'actual',
|
|
||||||
problem.realOutput,
|
|
||||||
submission.actualOutput
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { diff: diff };
|
return { diff: submission.diff, submissionId: submission.id, output: submission.actualOutput };
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { stretchTextarea } from '$lib/util';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: Actions;
|
export let form: Actions;
|
||||||
@ -28,6 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
if (data.diff) {
|
||||||
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
||||||
drawFileList: false,
|
drawFileList: false,
|
||||||
matching: 'lines',
|
matching: 'lines',
|
||||||
@ -37,6 +39,7 @@
|
|||||||
fileContentToggle: false
|
fileContentToggle: false
|
||||||
});
|
});
|
||||||
diff2htmlUi.draw();
|
diff2htmlUi.draw();
|
||||||
|
}
|
||||||
|
|
||||||
incorrectBtn.addEventListener('change', () => {
|
incorrectBtn.addEventListener('change', () => {
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
@ -50,10 +53,10 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Diff</title>
|
<title>Review Submission</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Diff</h1>
|
<h1 style="text-align:center" class="mb-4">Review Submission</h1>
|
||||||
|
|
||||||
{#if form && !form.success}
|
{#if form && !form.success}
|
||||||
<div class="alert alert-danger">Submission was not successful</div>
|
<div class="alert alert-danger">Submission was not successful</div>
|
||||||
@ -61,7 +64,17 @@
|
|||||||
<div class="alert alert-success">Success!</div>
|
<div class="alert alert-success">Success!</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="mb-3 col">
|
||||||
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
<a href="/admin/reviews" class="btn btn-outline-primary">All Reviews</a>
|
||||||
|
<a href={`/admin/submissions/${data.submissionId.toString()}`} class="btn btn-outline-primary"
|
||||||
|
>Go to Submission</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Output</h3>
|
||||||
|
<textarea use:stretchTextarea class="mb-3 form-control" disabled>{data.output}</textarea>
|
||||||
|
|
||||||
|
<h3>Diff</h3>
|
||||||
<div class="mt-3" id="diff" />
|
<div class="mt-3" id="diff" />
|
||||||
|
|
||||||
<form method="POST" action="?/submit" use:enhance>
|
<form method="POST" action="?/submit" use:enhance>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
|
import { stretchTextarea } from '$lib/util';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
let editing = false;
|
let editing = false;
|
||||||
@ -9,10 +10,6 @@
|
|||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: Actions;
|
export let form: Actions;
|
||||||
|
|
||||||
function stretchTextarea(textarea: HTMLTextAreaElement) {
|
|
||||||
textarea.style.height = textarea.scrollHeight + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteProblem() {
|
async function deleteProblem() {
|
||||||
const sure = confirm('Are you sure?');
|
const sure = confirm('Are you sure?');
|
||||||
if (!sure) {
|
if (!sure) {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
import { SubmissionState } from '@prisma/client';
|
import { SubmissionState } from '@prisma/client';
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
const submissions = await db.submission.findMany({ where: { state: SubmissionState.InReview } });
|
const submissions = await db.submission.findMany({ where: { state: SubmissionState.InReview } });
|
||||||
const teams = await db.team.findMany();
|
const teams = await db.team.findMany();
|
||||||
const problems = await db.problem.findMany();
|
const problems = await db.problem.findMany();
|
||||||
return {
|
return {
|
||||||
|
timestamp: new Date(),
|
||||||
reviewList: submissions.map((row) => {
|
reviewList: submissions.map((row) => {
|
||||||
return { id: row.id, createdAt: row.createdAt };
|
return { id: row.id, createdAt: row.createdAt };
|
||||||
}),
|
}),
|
||||||
@ -18,45 +19,3 @@ export const load = (async () => {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) 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;
|
|
||||||
|
@ -1,7 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
let updateInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
let updating = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateInterval = setInterval(async () => {
|
||||||
|
updating = true;
|
||||||
|
await invalidateAll();
|
||||||
|
updating = false;
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (updateInterval) {
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -10,6 +29,13 @@
|
|||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Reviews</h1>
|
<h1 style="text-align:center" class="mb-4">Reviews</h1>
|
||||||
|
|
||||||
|
<div class="mb-3 text-end">
|
||||||
|
{#if updating}
|
||||||
|
<div class="spinner-border spinner-border-sm text-secondary" />
|
||||||
|
{/if}
|
||||||
|
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{#if data.reviewList.length === 0}
|
{#if data.reviewList.length === 0}
|
||||||
<div class="alert alert-success">No Submission to Review!</div>
|
<div class="alert alert-success">No Submission to Review!</div>
|
||||||
@ -20,66 +46,3 @@
|
|||||||
>
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<!-- <hr />
|
|
||||||
<h2>For Testing Purposes - Create Fake Submission</h2>
|
|
||||||
{#if form && !form.success}
|
|
||||||
<div class="alert alert-danger">Invalid Submission</div>
|
|
||||||
{/if}
|
|
||||||
<form method="POST" action="?/submission" use:enhance>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3">
|
|
||||||
<h5>Team</h5>
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
||||||
{selectedTeam ? selectedTeam.name : 'Select Team'}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{#each data.teams as team}
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
on:click={() => {
|
|
||||||
selectedTeam = team;
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item">{team.name}</button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-3">
|
|
||||||
<h5>Problem</h5>
|
|
||||||
<div class="dropdown">
|
|
||||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
||||||
{selectedProblem ? selectedProblem.name : 'Select Problem'}
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{#each data.problems as problem}
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
on:click={() => {
|
|
||||||
selectedProblem = problem;
|
|
||||||
}}
|
|
||||||
type="button"
|
|
||||||
class="dropdown-item">{problem.name}</button
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<h5>Actual output (like from student output)</h5>
|
|
||||||
<textarea name="actual" class="form-control" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input name="teamId" type="hidden" value={selectedTeam ? selectedTeam.id : ''} />
|
|
||||||
<input name="problemId" type="hidden" value={selectedProblem ? selectedProblem.id : ''} />
|
|
||||||
<div class="row justify-content-end">
|
|
||||||
<div class="text-end">
|
|
||||||
<button type="submit" class="mt-3 btn btn-secondary">Submit</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form> -->
|
|
||||||
|
@ -3,15 +3,106 @@ import type { PageServerLoad } from './$types';
|
|||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
const timestamp = new Date();
|
const timestamp = new Date();
|
||||||
const problems = await db.problem.findMany();
|
const contests = await db.contest.findMany({
|
||||||
const teams = await db.team.findMany();
|
include: { problems: true, teams: { include: { submissions: true } } }
|
||||||
return {
|
});
|
||||||
|
const data = {
|
||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
problems: problems.map((row) => {
|
contests: contests.map((contest) => {
|
||||||
return { friendlyName: row.friendlyName };
|
return {
|
||||||
|
name: contest.name,
|
||||||
|
problems: contest.problems.map((problem) => {
|
||||||
|
return { id: problem.id, friendlyName: problem.friendlyName };
|
||||||
}),
|
}),
|
||||||
teams: teams.map((row) => {
|
teams: contest.teams
|
||||||
return { name: row.name };
|
.map((team) => {
|
||||||
|
return {
|
||||||
|
name: team.name,
|
||||||
|
solves: team.submissions.filter((submission) => {
|
||||||
|
return submission.contestId === contest.id && submission.state === 'Correct';
|
||||||
|
}).length,
|
||||||
|
time: (() => {
|
||||||
|
const correctSubmissions = team.submissions.filter((submission) => {
|
||||||
|
return submission.contestId === contest.id && submission.state === 'Correct';
|
||||||
|
});
|
||||||
|
const penaltyTime =
|
||||||
|
team.submissions.filter((submission) => {
|
||||||
|
return (
|
||||||
|
submission.contestId === contest.id &&
|
||||||
|
submission.state === 'Incorrect' &&
|
||||||
|
correctSubmissions.find((correct) => {
|
||||||
|
return correct.problemId === submission.problemId;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}).length * 10;
|
||||||
|
let time = penaltyTime;
|
||||||
|
correctSubmissions.forEach((correctSubmission) => {
|
||||||
|
const gradedAt = correctSubmission.gradedAt!.valueOf();
|
||||||
|
const min = (gradedAt - contest.startTime!.valueOf()) / 60000;
|
||||||
|
time += min;
|
||||||
|
});
|
||||||
|
return time;
|
||||||
|
})(),
|
||||||
|
problems: contest.problems.map((problem) => {
|
||||||
|
return {
|
||||||
|
id: problem.id,
|
||||||
|
attempts: team.submissions.filter((submission) => {
|
||||||
|
return (
|
||||||
|
submission.contestId === contest.id &&
|
||||||
|
submission.problemId === problem.id &&
|
||||||
|
(submission.state === 'Correct' || submission.state === 'Incorrect')
|
||||||
|
);
|
||||||
|
}).length,
|
||||||
|
graphic: team.submissions.find((submission) => {
|
||||||
|
return (
|
||||||
|
submission.contestId === contest.id &&
|
||||||
|
submission.problemId === problem.id &&
|
||||||
|
(submission.state === 'Correct' || submission.state === 'Incorrect')
|
||||||
|
);
|
||||||
|
})
|
||||||
|
? team.submissions.find((submission) => {
|
||||||
|
return (
|
||||||
|
submission.problemId === problem.id && submission.state === 'Correct'
|
||||||
|
);
|
||||||
|
})
|
||||||
|
? 'correct'
|
||||||
|
: 'incorrect'
|
||||||
|
: null,
|
||||||
|
min: (() => {
|
||||||
|
const correctSubmission = team.submissions.find((submission) => {
|
||||||
|
return (
|
||||||
|
submission.contestId === contest.id &&
|
||||||
|
submission.problemId === problem.id &&
|
||||||
|
submission.state === 'Correct'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (correctSubmission) {
|
||||||
|
const gradedAt = correctSubmission.gradedAt!.valueOf();
|
||||||
|
return (gradedAt - contest.startTime!.valueOf()) / 60000;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})()
|
||||||
|
};
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.solves > b.solves) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.solves < b.solves) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
if (a.time < b.time) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.time > b.time) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return data;
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
@ -25,38 +25,85 @@
|
|||||||
<title>Admin Scoreboard</title>
|
<title>Admin Scoreboard</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Admin Scoreboard</h1>
|
<h1 style="text-align:center" class="mb-4">Admin Scoreboards</h1>
|
||||||
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
{#if updating}
|
{#if updating}
|
||||||
<div class="spinner-border spinner-border-sm text-secondary" />
|
<div class="spinner-border spinner-border-sm text-secondary" />
|
||||||
{/if}
|
{/if}
|
||||||
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#each data.contests as contest}
|
||||||
|
<h2 style="text-align:center">{contest.name}</h2>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<div class="text-end" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table class="table table-striped table-bordered">
|
<table class="table table-striped table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Place</th>
|
||||||
<th>Team Name</th>
|
<th>Team Name</th>
|
||||||
{#each data.problems as problem}
|
<th>Solves</th>
|
||||||
|
<th>Time</th>
|
||||||
|
{#each contest.problems as problem}
|
||||||
<th>{problem.friendlyName}</th>
|
<th>{problem.friendlyName}</th>
|
||||||
{/each}
|
{/each}
|
||||||
<th>Total Correct</th>
|
|
||||||
<th>Total Points</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.teams as team}
|
{#each contest.teams as team, i}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{team.name}</td>
|
<td style="text-align:center; font-size:24px;"><strong>{i + 1}</strong></td>
|
||||||
{#each data.problems as _}
|
<td style="font-size:18px">{team.name}</td>
|
||||||
<td>-/-</td>
|
<td style="font-size:18px">{team.solves}</td>
|
||||||
|
<td style="font-size:18px">{team.time.toFixed(0)}</td>
|
||||||
|
{#each contest.problems as problem}
|
||||||
|
<td>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3">
|
||||||
|
{#if team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.graphic !== null}
|
||||||
|
<img
|
||||||
|
src={team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.graphic === 'correct'
|
||||||
|
? '/correct.png'
|
||||||
|
: '/incorrect.png'}
|
||||||
|
alt="check or X"
|
||||||
|
width="30px"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
{#if team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.attempts !== 0}
|
||||||
|
{team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.attempts}
|
||||||
|
{team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.attempts === 1
|
||||||
|
? 'Attempt'
|
||||||
|
: 'Attempts'}<br />{#if team.problems.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})?.min}<span style="color:rgb(102,102,102)"
|
||||||
|
>{team.problems
|
||||||
|
.find((p) => {
|
||||||
|
return p.id === problem.id;
|
||||||
|
})
|
||||||
|
?.min?.toFixed(0)} min</span
|
||||||
|
>{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
<td>0</td>
|
|
||||||
<td>0</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{/each}
|
||||||
|
@ -6,6 +6,7 @@ export const load = (async () => {
|
|||||||
const problems = await db.problem.findMany();
|
const problems = await db.problem.findMany();
|
||||||
const teams = await db.team.findMany();
|
const teams = await db.team.findMany();
|
||||||
return {
|
return {
|
||||||
|
timestamp: new Date(),
|
||||||
submissions: submissions.map((row) => {
|
submissions: submissions.map((row) => {
|
||||||
return {
|
return {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
|
@ -1,12 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { goto } from '$app/navigation';
|
import { goto, invalidateAll } from '$app/navigation';
|
||||||
|
import { onDestroy, onMount } from 'svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
$: data.submissions.sort((a, b) => {
|
$: data.submissions.sort((a, b) => {
|
||||||
return b.createdAt.valueOf() - a.createdAt.valueOf();
|
return b.createdAt.valueOf() - a.createdAt.valueOf();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let updateInterval: ReturnType<typeof setInterval> | undefined;
|
||||||
|
let updating = false;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateInterval = setInterval(async () => {
|
||||||
|
updating = true;
|
||||||
|
await invalidateAll();
|
||||||
|
updating = false;
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
if (updateInterval) {
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -15,7 +33,17 @@
|
|||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Submissions</h1>
|
<h1 style="text-align:center" class="mb-4">Submissions</h1>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-8">
|
||||||
<p>Rows are color coded: Red - Incorrect, Green - Correct, Yellow - In Review</p>
|
<p>Rows are color coded: Red - Incorrect, Green - Correct, Yellow - In Review</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 text-end">
|
||||||
|
{#if updating}
|
||||||
|
<div class="spinner-border spinner-border-sm text-secondary" />
|
||||||
|
{/if}
|
||||||
|
<strong>Last Updated: </strong>{data.timestamp.toLocaleTimeString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered table-hover">
|
<table class="table table-bordered table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { error, redirect } from '@sveltejs/kit';
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
import * as Diff from 'diff';
|
|
||||||
import { SubmissionState } from '@prisma/client';
|
|
||||||
|
|
||||||
export const load = (async ({ params }) => {
|
export const load = (async ({ params }) => {
|
||||||
const submissionId = parseInt(params.submissionId);
|
const submissionId = parseInt(params.submissionId);
|
||||||
@ -21,16 +19,6 @@ export const load = (async ({ params }) => {
|
|||||||
if (!problem) {
|
if (!problem) {
|
||||||
throw error(500, 'Invalid problem');
|
throw error(500, 'Invalid problem');
|
||||||
}
|
}
|
||||||
let diff: string | null = null;
|
|
||||||
if (submission.state == SubmissionState.Incorrect) {
|
|
||||||
diff = Diff.createTwoFilesPatch(
|
|
||||||
'expected',
|
|
||||||
'actual',
|
|
||||||
problem.realOutput,
|
|
||||||
submission.actualOutput
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: submission.id,
|
id: submission.id,
|
||||||
state: submission.state,
|
state: submission.state,
|
||||||
@ -39,6 +27,18 @@ export const load = (async ({ params }) => {
|
|||||||
submitTime: submission.createdAt,
|
submitTime: submission.createdAt,
|
||||||
gradedTime: submission.gradedAt,
|
gradedTime: submission.gradedAt,
|
||||||
message: submission.message,
|
message: submission.message,
|
||||||
diff: diff
|
diff: submission.diff
|
||||||
};
|
};
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
delete: async ({ params }) => {
|
||||||
|
const submissionId = parseInt(params.submissionId);
|
||||||
|
try {
|
||||||
|
await db.submission.delete({ where: { id: submissionId } });
|
||||||
|
} catch {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
throw redirect(302, '/admin/submissions');
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui-base';
|
import { Diff2HtmlUI } from 'diff2html/lib/ui/js/diff2html-ui-base';
|
||||||
import 'diff2html/bundles/css/diff2html.min.css';
|
import 'diff2html/bundles/css/diff2html.min.css';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
export let form: Actions;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.diff) {
|
if (data.diff) {
|
||||||
const diff2htmlUi = new Diff2HtmlUI(document.getElementById('diff')!, data.diff, {
|
const diffElement = document.getElementById('diff');
|
||||||
|
if (diffElement) {
|
||||||
|
const diff2htmlUi = new Diff2HtmlUI(diffElement, data.diff, {
|
||||||
drawFileList: false,
|
drawFileList: false,
|
||||||
matching: 'lines',
|
matching: 'lines',
|
||||||
diffStyle: 'char',
|
diffStyle: 'char',
|
||||||
@ -18,6 +22,7 @@
|
|||||||
});
|
});
|
||||||
diff2htmlUi.draw();
|
diff2htmlUi.draw();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -27,7 +32,31 @@
|
|||||||
|
|
||||||
<h1 style="text-align:center" class="mb-4">Submission</h1>
|
<h1 style="text-align:center" class="mb-4">Submission</h1>
|
||||||
|
|
||||||
|
{#if form && !form.success}
|
||||||
|
<div class="alert alert-danger">Error</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
<a href="/admin/submissions" class="mb-3 btn btn-outline-primary">All Submissions</a>
|
<a href="/admin/submissions" class="mb-3 btn btn-outline-primary">All Submissions</a>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 text-end">
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action="?/delete"
|
||||||
|
use:enhance={({ cancel }) => {
|
||||||
|
if (!confirm('Are you sure?')) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
return async ({ update }) => {
|
||||||
|
update();
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button type="submit" class="btn btn-danger">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { db } from '$lib/server/prisma';
|
import { db } from '$lib/server/prisma';
|
||||||
import type { Actions, PageServerLoad } from './$types';
|
import type { Actions, PageServerLoad } from './$types';
|
||||||
|
import { genPassword } from './util';
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
const teams = await db.team.findMany();
|
const teams = await db.team.findMany();
|
||||||
@ -18,7 +19,7 @@ export const actions = {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await db.team.create({ data: { name: name.toString(), password: "thing" } });
|
await db.team.create({ data: { name: name.toString(), password: genPassword() } });
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { genPassword } from '../util';
|
||||||
import type { Actions, PageData } from './$types';
|
import type { Actions, PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
@ -10,6 +11,11 @@
|
|||||||
$: if (form && form.success) {
|
$: if (form && form.success) {
|
||||||
changingPassword = false;
|
changingPassword = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onGenPassword() {
|
||||||
|
const passEntry = document.getElementById('pass_entry') as HTMLInputElement;
|
||||||
|
passEntry.value = genPassword();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -72,7 +78,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<form method="POST" action="?/password" use:enhance>
|
<form method="POST" action="?/password" use:enhance>
|
||||||
<h4>Change Password</h4>
|
<h4>Change Password</h4>
|
||||||
<input name="password" class="form-control" />
|
<input id="pass_entry" name="password" class="form-control" />
|
||||||
<div class="mt-2 row">
|
<div class="mt-2 row">
|
||||||
<div class="text-end">
|
<div class="text-end">
|
||||||
<button
|
<button
|
||||||
@ -82,6 +88,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-secondary">Cancel</button
|
class="btn btn-outline-secondary">Cancel</button
|
||||||
>
|
>
|
||||||
|
<button on:click={onGenPassword} type="button" class="btn btn-outline-primary"
|
||||||
|
>Generate</button
|
||||||
|
>
|
||||||
<button type="submit" class="btn btn-success">Change</button>
|
<button type="submit" class="btn btn-success">Change</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
8
web/src/routes/admin/teams/util.ts
Normal file
8
web/src/routes/admin/teams/util.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function genPassword(): string {
|
||||||
|
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
let password = '';
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
password += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
@ -64,8 +64,8 @@ export const POST = (async ({ request }) => {
|
|||||||
const diff = Diff.createTwoFilesPatch(
|
const diff = Diff.createTwoFilesPatch(
|
||||||
'expected',
|
'expected',
|
||||||
'actual',
|
'actual',
|
||||||
data.data.output,
|
submission.problem.realOutput,
|
||||||
submission.actualOutput!
|
data.data.output
|
||||||
);
|
);
|
||||||
await db.submission.update({
|
await db.submission.update({
|
||||||
where: { id: data.data.submissionId },
|
where: { id: data.data.submissionId },
|
||||||
|
@ -13,7 +13,10 @@ export const POST = (async ({ params, request }) => {
|
|||||||
const sessionToken = params.session;
|
const sessionToken = params.session;
|
||||||
const activeTeam = await db.activeTeam.findUnique({
|
const activeTeam = await db.activeTeam.findUnique({
|
||||||
where: { sessionToken: sessionToken },
|
where: { sessionToken: sessionToken },
|
||||||
include: { contest: { include: { problems: { select: { id: true } } } } }
|
include: {
|
||||||
|
contest: { include: { problems: { select: { id: true } } } },
|
||||||
|
team: { include: { submissions: true } }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (!activeTeam) {
|
if (!activeTeam) {
|
||||||
throw error(400);
|
throw error(400);
|
||||||
@ -31,6 +34,19 @@ export const POST = (async ({ params, request }) => {
|
|||||||
throw error(400);
|
throw error(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure no submission is currently marked correct
|
||||||
|
const correctSubmissions = activeTeam.team.submissions.filter((submission) => {
|
||||||
|
return (
|
||||||
|
submission.contestId === activeTeam.contestId &&
|
||||||
|
submission.state === 'Correct' &&
|
||||||
|
submission.problemId === data.data.problemId
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if (correctSubmissions !== 0) {
|
||||||
|
return json({ success: false, message: 'Already submitted correct submission' });
|
||||||
|
}
|
||||||
|
|
||||||
await db.submission.create({
|
await db.submission.create({
|
||||||
data: {
|
data: {
|
||||||
state: SubmissionState.Queued,
|
state: SubmissionState.Queued,
|
||||||
|
BIN
web/static/correct.png
Normal file
BIN
web/static/correct.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
web/static/incorrect.png
Normal file
BIN
web/static/incorrect.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Loading…
Reference in New Issue
Block a user