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 type { TasksFile, TaskDescriptor } from "./task-loader";
import { createNodesAndEdges } from "./graph-types"; import { createNodesAndEdges } from "./graph-types";
import { taskForce } from "./task-force"; 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 tasks: TasksFile;
export let repulsionForce: number = -1000; export let repulsionForce: number = -1000;
@ -18,6 +21,7 @@
let clientHeight: number; let clientHeight: number;
let clientWidth: number; let clientWidth: number;
let svgElement: SVGElement; let svgElement: SVGElement;
let taskStatuses = new Map<string, TaskStatus>();
// this prevents svelte from updating nodes and edges // this prevents svelte from updating nodes and edges
// when we update 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 // start simulation and center view on create
onMount(() => { onMount(() => {
// set center of the SVG at (0,0) // set center of the SVG at (0,0)
@ -137,6 +152,7 @@
on:click={nodeClick(task.task)} on:click={nodeClick(task.task)}
on:hoveringChange={nodeHover(task.task)} on:hoveringChange={nodeHover(task.task)}
on:positionChange={() => { tasks = tasks; }} on:positionChange={() => { tasks = tasks; }}
status={taskStatuses.get(task.id)}
draggingEnabled={nodeDraggingEnabled} /> draggingEnabled={nodeDraggingEnabled} />
{/each} {/each}
</g> </g>

7
frontend/src/GraphNode.svelte

@ -3,9 +3,11 @@
import { createEventDispatcher, onMount } from "svelte"; import { createEventDispatcher, onMount } from "svelte";
import type { TaskId } from "./graph-types"; import type { TaskId } from "./graph-types";
import type { TaskStatus } from "./ksp-task-grabber";
export let task: TaskId; export let task: TaskId;
export let draggingEnabled: boolean = false; export let draggingEnabled: boolean = false;
export let status: TaskStatus | undefined = undefined
let hovering: boolean = false; let hovering: boolean = false;
let text_element: SVGTextElement; let text_element: SVGTextElement;
@ -74,9 +76,12 @@
ellipse { ellipse {
fill: #69b3a2 fill: #69b3a2
} }
.submitted ellipse {
fill: red
}
</style> </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} /> <ellipse rx={ellipse_rx} ry={20} {cx} {cy} />
<text <text
bind:this={text_element} bind:this={text_element}

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

@ -1,3 +1,5 @@
import type { TaskId } from "./graph-types"
export type TaskAssignmentData = { export type TaskAssignmentData = {
id: string, id: string,
name: string, name: string,
@ -13,19 +15,16 @@ type TaskLocation = {
startElement: string startElement: string
} }
function fixLink(link: string, currentPath: string): string { export type TaskStatus = {
const url = new URL(link, currentPath) id: string
if (url.host == "ksp.mff.cuni.cz" || url.host == "ksp-test.ks.matfyz.cz") { name: string
url.host = location.host submitted: boolean
} points: number
if (url.host == location.host) { maxPoints: number
return url.pathname type: string
} else {
return url.href
}
} }
function fixAllLinks(e: any, currentPath: string) { function fixAllLinks(e: any) {
if (typeof e.src == "string") { if (typeof e.src == "string") {
e.src = e.src e.src = e.src
} }
@ -34,11 +33,25 @@ function fixAllLinks(e: any, currentPath: string) {
} }
let c = (e as HTMLElement).firstElementChild let c = (e as HTMLElement).firstElementChild
while (c) { while (c) {
fixAllLinks(c, currentPath) fixAllLinks(c)
c = c.nextElementSibling 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 { function getLocation(id: string, solution: boolean): TaskLocation | null {
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id) const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
if (!m) return null 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 { function parseTask(startElementId: string, doc: HTMLDocument): 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);
}
const titleElement = doc.getElementById(startElementId) const titleElement = doc.getElementById(startElementId)
if (!titleElement) if (!titleElement)
throw new Error(`Document does not contain ${startElementId}`) throw new Error(`Document does not contain ${startElementId}`)
fixAllLinks(titleElement, currentPath) fixAllLinks(titleElement)
const elements = [] const elements = []
let e = titleElement let e = titleElement
@ -99,7 +104,7 @@ function parseTask(startElementId: string, html: string, currentPath: string): T
let r = "" let r = ""
for (const e of elements) { for (const e of elements) {
fixAllLinks(e, currentPath) fixAllLinks(e)
r += e.outerHTML + "\n" r += e.outerHTML + "\n"
} }
return { 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" } }) const r = await fetch(url, { headers: { "Accept": "text/html,application/xhtml+xml" } })
if (r.status >= 400) { 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 doc
return parseTask(startElement, rText, url) }
async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssignmentData> {
const html = await fetchHtml(url)
return parseTask(startElement, html)
} }
function virtualTask(id: string): TaskAssignmentData { 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> { export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
const l = getLocation(id, false) const l = getLocation(id, false)
if (!l) return virtualTask(id) if (!l) return virtualTask(id)