Submit form prototype
This commit is contained in:
parent
6dbc844462
commit
80f9a70c72
4 changed files with 317 additions and 6 deletions
205
frontend/src/Odevzdavatko.svelte
Normal file
205
frontend/src/Odevzdavatko.svelte
Normal file
|
@ -0,0 +1,205 @@
|
|||
<script lang="ts">
|
||||
import { isLoggedIn, parseTaskId } from "./ksp-task-grabber";
|
||||
import type { TaskStatus } from "./ksp-task-grabber";
|
||||
import type { TaskSubmitStatus, SubtaskSubmitStatus } from './ksp-submit-api'
|
||||
import * as api from './ksp-submit-api'
|
||||
import { taskStatuses } from './task-status-cache'
|
||||
|
||||
export let id: string;
|
||||
const taskFromCache: TaskStatus | undefined = $taskStatuses.get(id)
|
||||
let task: TaskSubmitStatus
|
||||
let subtaskId: string | null | undefined = null
|
||||
let uploadSubtaskId: string | null | undefined = null
|
||||
let expiresInSec: number = 0
|
||||
let tick = 0
|
||||
let validSubmitSubtasks: SubtaskSubmitStatus[] = []
|
||||
let downloading = false
|
||||
let generating = false
|
||||
|
||||
$: {
|
||||
if (task && task.id == "cviciste/" + id) {
|
||||
break $
|
||||
}
|
||||
|
||||
task = {
|
||||
// fill with guesses to prevent flashing
|
||||
id,
|
||||
name: "",
|
||||
points: taskFromCache?.points ?? 0,
|
||||
max_points: taskFromCache?.maxPoints ?? 1,
|
||||
subtasks: [
|
||||
{ id: "1", input_generated: false, max_points: 1, points: 0 }
|
||||
]
|
||||
}
|
||||
subtaskId = "1"
|
||||
|
||||
api.taskStatus(id)
|
||||
.then(t => {
|
||||
task = t
|
||||
})
|
||||
}
|
||||
|
||||
$: {
|
||||
if (!task.subtasks.find(t => t.id == subtaskId))
|
||||
subtaskId = task.subtasks.find(t => t.points < t.max_points - 0.001)?.id
|
||||
}
|
||||
|
||||
$: {
|
||||
tick
|
||||
expiresInSec = subtaskId ? calcExpires(subtaskId) : 0
|
||||
}
|
||||
window.setInterval(() => { tick++ }, 1000)
|
||||
|
||||
$: {
|
||||
tick
|
||||
validSubmitSubtasks = task.subtasks.filter(t => calcExpires(t.id) > 0)
|
||||
}
|
||||
$: {
|
||||
if (!validSubmitSubtasks.map(t => t.id).includes(uploadSubtaskId!)) {
|
||||
uploadSubtaskId = validSubmitSubtasks[0]?.id
|
||||
}
|
||||
}
|
||||
|
||||
function calcExpires(subtask: string): number {
|
||||
const st = task.subtasks.find(t => t.id == subtask)!
|
||||
if (!st.input_generated) {
|
||||
return 0
|
||||
}
|
||||
const validUntil = new Date(st.input_valid_until!).valueOf()
|
||||
const now = Date.now()
|
||||
if (validUntil < now) {
|
||||
return 0
|
||||
} else {
|
||||
return (validUntil - now) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
function nameSubtask(id: string) {
|
||||
const map: any = {
|
||||
"1": "první",
|
||||
"2": "druhý",
|
||||
"3": "třetí",
|
||||
"4": "čtvrtý",
|
||||
"5": "pátý",
|
||||
"6": "šestý",
|
||||
"7": "sedmý",
|
||||
"8": "osmý",
|
||||
"9": "devátý",
|
||||
"10": "desátý"
|
||||
}
|
||||
return map[id] ?? id
|
||||
}
|
||||
|
||||
function magicTrickSaveBlob(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
magicTrickSaveFile(url, fileName)
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
function magicTrickSaveFile(url: string, fileName: string) {
|
||||
var a = document.createElement("a");
|
||||
document.body.appendChild(a);
|
||||
a.style.display = "none";
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
a.remove()
|
||||
}
|
||||
|
||||
async function download() {
|
||||
// copy to prevent races
|
||||
const [id_, subtaskId_] = [id, subtaskId]
|
||||
if (!subtaskId_) { return }
|
||||
if (expiresInSec < 20) {
|
||||
const x = await api.generateInput(id_, subtaskId_)
|
||||
if (id_ != id) { return }
|
||||
const subtasks = [...task.subtasks]
|
||||
subtasks[subtasks.findIndex(s => s.id == x.id)] = x
|
||||
task = { ...task, subtasks }
|
||||
}
|
||||
// It's probably better to download the input using the "old" method
|
||||
// TODO: specify that as an API
|
||||
|
||||
const parsedId = parseTaskId(id_)
|
||||
magicTrickSaveFile(`/cviciste/?in=1:sub=${subtaskId}:task=${id_}:year=${parsedId!.rocnik}`, subtaskId_ + ".in.txt")
|
||||
|
||||
// const blob = await api.getInput(id_, subtaskId_)
|
||||
// magicTrickSaveBlob(blob, subtaskId_ + ".in.txt")
|
||||
}
|
||||
|
||||
async function upload(file: File) {
|
||||
const x = await api.submit(id, uploadSubtaskId!, file)
|
||||
alert(x.verdict)
|
||||
}
|
||||
|
||||
function fileChange(ev: Event) {
|
||||
const file = (ev.target as HTMLInputElement).files![0]
|
||||
upload(file)
|
||||
}
|
||||
|
||||
function drop(ev: DragEvent) {
|
||||
ev.preventDefault()
|
||||
const files = ev.dataTransfer?.files ?? []
|
||||
if (files.length > 1) {
|
||||
alert("Drag & Drop funguje, ale musíš jenom jeden soubor")
|
||||
}
|
||||
if (files.length > 0) {
|
||||
upload(files[0])
|
||||
}
|
||||
}
|
||||
function dragOver(ev: DragEvent) {
|
||||
ev.preventDefault()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
<svelte:body on:drop={drop} on:dragover={dragOver} />
|
||||
|
||||
<div class="odevzdavatko">
|
||||
<div class="download">
|
||||
<button class="download"
|
||||
on:click={download}
|
||||
disabled={subtaskId == null || downloading || generating}>
|
||||
{#if generating}
|
||||
Generuji...
|
||||
{:else if downloading}
|
||||
Stahuji...
|
||||
{:else if expiresInSec > 20}
|
||||
Stáhnout
|
||||
{:else}
|
||||
Vygenerovat a stáhnout
|
||||
{/if}
|
||||
</button>
|
||||
<select bind:value={subtaskId}>
|
||||
<option value={null}></option>
|
||||
{#each task.subtasks as subtask}
|
||||
<option value={subtask.id}>
|
||||
{nameSubtask(subtask.id)}
|
||||
{#if subtask.points > subtask.max_points - 0.0001}
|
||||
<span title="Splněno">✔️</span>
|
||||
{/if}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
vstup.
|
||||
|
||||
</div>
|
||||
|
||||
{#if validSubmitSubtasks.length > 0}
|
||||
<div class="upload">
|
||||
Odevzdat
|
||||
<select value={uploadSubtaskId}>
|
||||
{#each validSubmitSubtasks as subtask}
|
||||
<option value={subtask.id}>
|
||||
{nameSubtask(subtask.id)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
vstup:
|
||||
|
||||
<input type="file" on:change={fileChange}> (nebo přetáhni soubor na stránku)
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,10 +1,11 @@
|
|||
<script type="ts">
|
||||
import { grabAssignment, grabSolution } from "./ksp-task-grabber";
|
||||
import { grabAssignment, grabSolution, isLoggedIn } from "./ksp-task-grabber";
|
||||
import type { TaskStatus } from "./ksp-task-grabber";
|
||||
import { nonNull } from './helpers'
|
||||
import App from "./App.svelte";
|
||||
import { taskStatuses } from "./task-status-cache";
|
||||
import type { TaskDescriptor } from "./tasks";
|
||||
import Odevzdavatko from "./Odevzdavatko.svelte";
|
||||
|
||||
export let task: TaskDescriptor | null | undefined
|
||||
|
||||
|
@ -24,6 +25,14 @@
|
|||
referenceId = r
|
||||
}
|
||||
}
|
||||
|
||||
let loginUrl: string = null!
|
||||
function updateLoginUrl() {
|
||||
loginUrl = `/z/auth/login.cgi?redirect=${encodeURIComponent(location.href)}`
|
||||
}
|
||||
updateLoginUrl()
|
||||
window.addEventListener("onhashchange", updateLoginUrl)
|
||||
|
||||
</script>
|
||||
<style>
|
||||
div {
|
||||
|
@ -72,7 +81,8 @@
|
|||
</div>
|
||||
</div>
|
||||
{@html task.description}
|
||||
<div class="clearfloat" />
|
||||
|
||||
<hr class="clearfloat" />
|
||||
|
||||
<div class="solution">
|
||||
{#if !showSolution}
|
||||
|
@ -82,13 +92,22 @@
|
|||
</a>
|
||||
{:else}
|
||||
<h4>Řešení</h4>
|
||||
{#await grabSolution(referenceId)}
|
||||
{#await grabSolution(nonNull(referenceId))}
|
||||
Načítám...
|
||||
{:then solution}
|
||||
{@html solution.description}
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !showSolution}
|
||||
<hr class="clearfloat" />
|
||||
{#if isLoggedIn()}
|
||||
<Odevzdavatko id={task.id} />
|
||||
{:else}
|
||||
<p class="zs-warning">Pro odevzdávání je potřeba se <a href={loginUrl}>přihlásit</a>.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
{/if}
|
||||
|
|
87
frontend/src/ksp-submit-api.ts
Normal file
87
frontend/src/ksp-submit-api.ts
Normal file
|
@ -0,0 +1,87 @@
|
|||
import { fetchHtml } from "./ksp-task-grabber";
|
||||
|
||||
let apitoken : string | null = null
|
||||
|
||||
async function getToken(): Promise<string | undefined> {
|
||||
if (apitoken != null) {
|
||||
return apitoken
|
||||
}
|
||||
let doc = await fetchHtml("/auth/apitoken.cgi?show=1")
|
||||
const token = Array.from(doc.querySelectorAll("#content p")).map(x => /Aktuální token: (.*)/.exec(x.innerHTML.trim())).filter(x => x != null).map(x => x![1])[0]
|
||||
if (token) {
|
||||
return apitoken = token;
|
||||
}
|
||||
const form = doc.getElementById("apitoken") as HTMLFormElement
|
||||
const op = form.elements.namedItem("op")!.value
|
||||
const submit = form.elements.namedItem("submit")!.value
|
||||
const csrfToken = form.elements.namedItem("_token")!.value
|
||||
const body = `op=${encodeURIComponent(op)}&submit=${encodeURIComponent(submit)}&_token=${encodeURIComponent(csrfToken)}`
|
||||
console.log(`Creating new API token`)
|
||||
await fetch("/auth/apitoken.cgi", { method: "POST", body, headers: [["Content-Type", "application/x-www-form-urlencoded"]], redirect: "manual" })
|
||||
return await getToken()
|
||||
}
|
||||
|
||||
|
||||
async function request(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
const token = await getToken()
|
||||
const headers = new Headers(options.headers)
|
||||
headers.append("Authorization", "Bearer " + token)
|
||||
const opts: RequestInit = { ...options, headers }
|
||||
const r = await fetch(url, opts)
|
||||
if (r.status >= 400) {
|
||||
throw await r.json()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function requestJson<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const r = await request(url, options)
|
||||
return await r.json()
|
||||
}
|
||||
|
||||
export function listTasks(): Promise<string[]> {
|
||||
return requestJson("/api/tasks/list?set=cviciste")
|
||||
}
|
||||
|
||||
export type SubtaskSubmitStatus = {
|
||||
id: string
|
||||
points: number
|
||||
max_points: number
|
||||
input_generated: boolean
|
||||
input_valid_until?: string
|
||||
submitted_on?: string
|
||||
verdict?: string
|
||||
}
|
||||
|
||||
export type TaskSubmitStatus = {
|
||||
id: string
|
||||
name: string
|
||||
points: number
|
||||
max_points: number
|
||||
subtasks: SubtaskSubmitStatus[]
|
||||
}
|
||||
|
||||
export function taskStatus(id: string): Promise<TaskSubmitStatus> {
|
||||
return requestJson(`/api/tasks/status?task=${encodeURIComponent("cviciste/" + id)}`)
|
||||
}
|
||||
|
||||
export function generateInput(id: string, subtask: string): Promise<SubtaskSubmitStatus> {
|
||||
return requestJson(`/api/tasks/generate?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`, { method: "POST" })
|
||||
}
|
||||
|
||||
export async function getInput(id: string, subtask: string): Promise<Blob> {
|
||||
const r = await request(`/api/tasks/input?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`)
|
||||
return await r.blob()
|
||||
}
|
||||
|
||||
export async function submit(id: string, subtask: string, uploadedData: string | Blob): Promise<SubtaskSubmitStatus> {
|
||||
return requestJson(
|
||||
`/api/tasks/submit?task=${encodeURIComponent("cviciste/" + id)}&subtask=${encodeURIComponent(subtask)}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: uploadedData,
|
||||
headers: [
|
||||
["Content-Type", "text/plain"]
|
||||
]
|
||||
})
|
||||
}
|
|
@ -38,14 +38,14 @@ function fixAllLinks(e: any) {
|
|||
}
|
||||
}
|
||||
|
||||
type ParsedTaskId = {
|
||||
export type ParsedTaskId = {
|
||||
rocnik: string
|
||||
z: boolean
|
||||
serie: string
|
||||
uloha: string
|
||||
}
|
||||
|
||||
function parseTaskId(id: string): ParsedTaskId | null {
|
||||
export function parseTaskId(id: string): ParsedTaskId | null {
|
||||
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
|
||||
if (!m) return null
|
||||
const [_, rocnik, z, serie, uloha] = m
|
||||
|
@ -135,7 +135,7 @@ function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchHtml(url: string) {
|
||||
export async function fetchHtml(url: string) {
|
||||
const r = await fetch(url, { headers: { "Accept": "text/html,application/xhtml+xml" } })
|
||||
if (r.status >= 400) {
|
||||
throw Error(r.statusText)
|
||||
|
|
Reference in a new issue