Browse Source

Grab task status from /cviciste/

see #11
mj-deploy
Standa Lukeš 4 years ago
parent
commit
80017529a7
  1. 16
      frontend/src/Graph.svelte
  2. 7
      frontend/src/GraphNode.svelte
  3. 106
      frontend/src/ksp-task-grabber.ts

16
frontend/src/Graph.svelte

@ -6,6 +6,9 @@
import type { TasksFile, TaskDescriptor } from "./task-loader";
import { createNodesAndEdges } from "./graph-types";
import { taskForce } from "./task-force";
import { grabTaskStates, isLoggedIn } from "./ksp-task-grabber";
import type { TaskStatus } from "./ksp-task-grabber"
import { json } from "d3";
export let tasks: TasksFile;
export let repulsionForce: number = -1000;
@ -18,6 +21,7 @@
let clientHeight: number;
let clientWidth: number;
let svgElement: SVGElement;
let taskStatuses = new Map<string, TaskStatus>();
// this prevents svelte from updating nodes and edges
// when we update nodes and edges
@ -87,6 +91,17 @@
}
if (isLoggedIn()) {
const cachedTaskStatuses = localStorage.getItem("taskStatuses-cache")
if (cachedTaskStatuses) {
try { taskStatuses = new Map(JSON.parse(cachedTaskStatuses)) } catch(e) { console.warn(e) }
}
grabTaskStates(tasks.tasks.map(t => t.id)).then(t => {
taskStatuses = t
localStorage.setItem("taskStatuses-cache", JSON.stringify(Array.from(t.entries())))
})
}
// start simulation and center view on create
onMount(() => {
// set center of the SVG at (0,0)
@ -137,6 +152,7 @@
on:click={nodeClick(task.task)}
on:hoveringChange={nodeHover(task.task)}
on:positionChange={() => { tasks = tasks; }}
status={taskStatuses.get(task.id)}
draggingEnabled={nodeDraggingEnabled} />
{/each}
</g>

7
frontend/src/GraphNode.svelte

@ -3,9 +3,11 @@
import { createEventDispatcher, onMount } from "svelte";
import type { TaskId } from "./graph-types";
import type { TaskStatus } from "./ksp-task-grabber";
export let task: TaskId;
export let draggingEnabled: boolean = false;
export let status: TaskStatus | undefined = undefined
let hovering: boolean = false;
let text_element: SVGTextElement;
@ -74,9 +76,12 @@
ellipse {
fill: #69b3a2
}
.submitted ellipse {
fill: red
}
</style>
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag}>
<g on:mouseenter={enter} on:mouseleave={leave} on:click={click} on:mousedown={dragStart} on:mouseup={dragStop} on:mousemove={drag} class={status && status.submitted ? "submitted" : ""}>
<ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
<text
bind:this={text_element}

106
frontend/src/ksp-task-grabber.ts

@ -1,3 +1,5 @@
import type { TaskId } from "./graph-types"
export type TaskAssignmentData = {
id: string,
name: string,
@ -13,19 +15,16 @@ type TaskLocation = {
startElement: string
}
function fixLink(link: string, currentPath: string): string {
const url = new URL(link, currentPath)
if (url.host == "ksp.mff.cuni.cz" || url.host == "ksp-test.ks.matfyz.cz") {
url.host = location.host
}
if (url.host == location.host) {
return url.pathname
} else {
return url.href
}
export type TaskStatus = {
id: string
name: string
submitted: boolean
points: number
maxPoints: number
type: string
}
function fixAllLinks(e: any, currentPath: string) {
function fixAllLinks(e: any) {
if (typeof e.src == "string") {
e.src = e.src
}
@ -34,11 +33,25 @@ function fixAllLinks(e: any, currentPath: string) {
}
let c = (e as HTMLElement).firstElementChild
while (c) {
fixAllLinks(c, currentPath)
fixAllLinks(c)
c = c.nextElementSibling
}
}
type ParsedTaskId = {
rocnik: string
z: boolean
serie: string
uloha: string
}
function parseTaskId(id: string): ParsedTaskId | null {
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
if (!m) return null
const [_, rocnik, z, serie, uloha] = m
return { rocnik, z: !!z, serie, uloha }
}
function getLocation(id: string, solution: boolean): TaskLocation | null {
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
if (!m) return null
@ -58,19 +71,11 @@ function getLocation(id: string, solution: boolean): TaskLocation | null {
}
}
function parseTask(startElementId: string, html: string, currentPath: string): TaskAssignmentData {
const parser = new DOMParser()
const doc = parser.parseFromString(html, "text/html")
if (!doc.head.querySelector("base")) {
let baseEl = doc.createElement('base');
baseEl.setAttribute('href', new URL(currentPath, location.href).href);
doc.head.append(baseEl);
}
function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentData {
const titleElement = doc.getElementById(startElementId)
if (!titleElement)
throw new Error(`Document does not contain ${startElementId}`)
fixAllLinks(titleElement, currentPath)
fixAllLinks(titleElement)
const elements = []
let e = titleElement
@ -99,7 +104,7 @@ function parseTask(startElementId: string, html: string, currentPath: string): T
let r = ""
for (const e of elements) {
fixAllLinks(e, currentPath)
fixAllLinks(e)
r += e.outerHTML + "\n"
}
return {
@ -111,13 +116,41 @@ function parseTask(startElementId: string, html: string, currentPath: string): T
}
}
async function loadTask({ url, startElement }: TaskLocation) {
function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
return rows.map(r => {
const submitted = !r.classList.contains("zs-unsubmitted")
const id = r.cells[0].innerText.trim()
const type = r.cells[1].innerText.trim()
const name = r.cells[2].innerText.trim()
const pointsStr = r.cells[4].innerText.trim()
const pointsMatch = /(–|\d+) *\/ *(\d+)/.exec(pointsStr)
if (!pointsMatch) throw new Error()
const points = +pointsMatch[1]
const maxPoints = +pointsMatch[2]
return { id, name, submitted, type, points, maxPoints }
})
}
async function fetchHtml(url: string) {
const r = await fetch(url, { headers: { "Accept": "text/html,application/xhtml+xml" } })
if (r.status >= 400) {
throw Error("Bad request")
throw Error(r.statusText)
}
const html = await r.text()
const parser = new DOMParser()
const doc = parser.parseFromString(html, "text/html")
if (!doc.head.querySelector("base")) {
let baseEl = doc.createElement('base');
baseEl.setAttribute('href', new URL(url, location.href).href);
doc.head.append(baseEl);
}
const rText = await r.text()
return parseTask(startElement, rText, url)
return doc
}
async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssignmentData> {
const html = await fetchHtml(url)
return parseTask(startElement, html)
}
function virtualTask(id: string): TaskAssignmentData {
@ -130,6 +163,25 @@ function virtualTask(id: string): TaskAssignmentData {
}
}
export function isLoggedIn(): boolean {
return !!document.querySelector(".auth a[href='/profil/profil.cgi']")
}
export async function grabTaskStates(kspIds: string[]): Promise<Map<string, TaskStatus>> {
if (!isLoggedIn()) throw new Error()
const ids = new Set<string>(kspIds.map(parseTaskId).filter(t => t != null).map(t => t!.rocnik))
const results = await Promise.all(Array.from(ids.keys()).map(async (rocnik) => {
const html = await fetchHtml(`/cviciste/?year=${rocnik}`)
return parseTaskStatuses(html)
}))
return new Map<string, TaskStatus>(
([] as TaskStatus[]).concat(...results)
.map(r => [r.id, r])
)
}
export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
const l = getLocation(id, false)
if (!l) return virtualTask(id)