parent
a264dbead9
commit
80017529a7
3 changed files with 101 additions and 28 deletions
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 rText = await r.text()
|
const html = await r.text()
|
||||||
return parseTask(startElement, rText, url)
|
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);
|
||||||
|
}
|
||||||
|
return doc
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
Reference in a new issue