aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-08-05 11:51:51 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-08-05 11:51:51 +0200
commit582df8885a3573a728394b473b826616f8969316 (patch)
tree9c21db03f4fbfd9956ee398e0d9194ebfa5b3234 /src
parent46ac53e11b49a7f9cdb92c3559b05f41ce90122f (diff)
downloadbycs-sol-582df8885a3573a728394b473b826616f8969316.tar.gz
bycs-sol-582df8885a3573a728394b473b826616f8969316.zip
convert a lot of js to typescript and use a single graduation DB
Diffstat (limited to 'src')
-rw-r--r--src/cockpit.js59
-rw-r--r--src/database-mock.ts30
-rw-r--r--src/database-prod.ts (renamed from src/database.js)104
-rw-r--r--src/graduation-common.js396
-rw-r--r--src/graduation-common.ts347
-rw-r--r--src/graduation-mock.ts (renamed from src/graduation-mock.js)6
-rw-r--r--src/graduation-prod.js15
-rw-r--r--src/graduation-prod.ts43
-rw-r--r--src/group-fetch.ts18
-rw-r--r--src/group.js57
-rw-r--r--src/group.ts45
-rw-r--r--src/mass-graduation.js4
-rw-r--r--src/mock.js104
-rw-r--r--src/mock.ts81
-rw-r--r--src/student-common.js115
-rw-r--r--src/student-common.ts42
-rw-r--r--src/student-mock.js9
-rw-r--r--src/student-mock.ts25
-rw-r--r--src/student-prod.js2
-rw-r--r--src/student-prod.ts90
-rw-r--r--src/untyped.d.ts4
-rw-r--r--src/utils-common.ts4
-rw-r--r--src/utils-mock.js27
-rw-r--r--src/utils-prod.js9
24 files changed, 819 insertions, 817 deletions
diff --git a/src/cockpit.js b/src/cockpit.js
index 9dea658..67619d8 100644
--- a/src/cockpit.js
+++ b/src/cockpit.js
@@ -1,28 +1,24 @@
// import {
// fetchSubjectsMock as fetchSubjects,
// fetchProgressMock as fetchProgress,
-// } from "../../js/mock.js";
+// } from "../../js/mock.ts";
import {
OBSERVATION_TYPES,
LEVEL_NAMES,
GraduationChange,
fetchGraduation,
- fetchPublicGraduation,
submitGraduationChange
-} from "./graduation-ENV.js";
+} from "./graduation-ENV.ts";
-import { fetchStudent, fetchStudents, Student } from "./student-ENV.js";
+import { fetchStudent, fetchStudents } from "./student-ENV.ts";
import {
- isTeacher as getIsTeacher,
getName,
- getUserId,
DATE_FMT,
DATETIME_FMT,
} from "./utils-ENV.js";
import { initSelection } from "./selection.js";
-let isTeacher;
let name;
let student;
@@ -81,8 +77,7 @@ function pointClickHandler(color) {
change = GraduationChange.newRedPointChange(textarea.value, coach);
break;
default:
- alert("Invalid color");
- break;
+ throw new Error("Invalid color");
}
submitGraduationChange(student, change);
textareaDiv.style.display = "none";
@@ -102,11 +97,7 @@ function pointClickHandler(color) {
}
async function fetchAndUpdateStudentInfo() {
- if (isTeacher)
- student.graduation = await fetchGraduation(student);
- else
- student.graduation = await fetchPublicGraduation(student);
-
+ student.graduation = await fetchGraduation(student);
updateStudentInfo();
}
@@ -135,14 +126,12 @@ function updateStudentInfo() {
document.getElementById("student-info-points-orange").innerHTML = student.graduation.orange;
document.getElementById("student-info-points-red").innerHTML = student.graduation.red;
- if (isTeacher) {
- document.getElementById("student-info-green").addEventListener('click',
- () => pointClickHandler("green"));
- document.getElementById("student-info-orange").addEventListener('click',
- () => pointClickHandler("orange"));
- document.getElementById("student-info-red").addEventListener('click',
- () => pointClickHandler("red"));
- }
+ document.getElementById("student-info-green").addEventListener('click',
+ () => pointClickHandler("green"));
+ document.getElementById("student-info-orange").addEventListener('click',
+ () => pointClickHandler("orange"));
+ document.getElementById("student-info-red").addEventListener('click',
+ () => pointClickHandler("red"));
renderGraduationLevel(student.graduation);
@@ -174,7 +163,7 @@ function renderObservationStatus(graduation, node) {
node.innerHTML = "";
let status;
- if (isTeacher || graduation.isObserved() || graduation.observationRequested) {
+ if (graduation.isObserved() || graduation.observationRequested) {
status = document.createElement("span");
status.classList.add("student-observation-status");
if (graduation.isObserved()) {
@@ -233,7 +222,6 @@ function renderObservationStatus(graduation, node) {
// }
async function updateStudentGraduation() {
- if (!isTeacher) return;
console.assert(student.graduation, "Student has no graduation");
const graduation = student.graduation;
@@ -258,7 +246,7 @@ async function updateStudentGraduation() {
changeTr.style.background = "orange";
break;
default:
- alert(`unexpected change: ${change.change}`);
+ console.error(`unexpected change: ${change.change}`);
}
continue;
}
@@ -339,27 +327,16 @@ async function updateStudentGraduation() {
}
async function initCockpit() {
- if (isTeacher) {
- initStudentSelection();
- const params = new URLSearchParams(location.search);
- const userId = params.get("userToLoad");
- if (userId) {
- student = await fetchStudent(userId);
- await loadStudent(student);
- }
- } else {
- // Remove all elements only suitable for teachers
- for(const elem of document.querySelectorAll("[data-availability=if-lehrer]")) {
- elem.remove();
- }
-
- const student = await Student.fetchById(getUserId());
+ initStudentSelection();
+ const params = new URLSearchParams(location.search);
+ const userId = params.get("userToLoad");
+ if (userId) {
+ student = await fetchStudent(userId);
await loadStudent(student);
}
}
window.onload = function() {
- isTeacher = getIsTeacher();
DEFAULT_STUDENT_ICON = document.getElementById("student-info-icon").src;
name = getName();
initCockpit();
diff --git a/src/database-mock.ts b/src/database-mock.ts
new file mode 100644
index 0000000..24cb08f
--- /dev/null
+++ b/src/database-mock.ts
@@ -0,0 +1,30 @@
+interface DatabaseParams {
+ [key: string]: string;
+}
+
+interface DatabaseEntry {
+ [key: string]: string;
+}
+
+class DB {
+ async fetchEntries(params?: DatabaseParams) {
+ console.log(`Mock DB fetch entries: ${params}`);
+ return [];
+ }
+
+ async saveEntry(json: string) {
+ console.log(`Mock DB save entry ${json}`);
+ return;
+ }
+
+}
+
+async function getGraduationDb() {
+ return new DB();
+}
+
+export {
+ DB,
+ DatabaseEntry, DatabaseParams,
+ getGraduationDb,
+};
diff --git a/src/database.js b/src/database-prod.ts
index e929896..6a55f25 100644
--- a/src/database.js
+++ b/src/database-prod.ts
@@ -21,67 +21,62 @@ const DB_TEMPLATE_LIST_FOOTER = `
</table>
`;
-let studentDbs = populateStudentDbs();
+let graduationDb = findGraduationDb();
const ID_QUERY_PARAM_REGEX = /[?&]id=(\d+)/
-async function populateStudentDbs() {
+async function findGraduationDb() {
+ let graduationDbId = localStorage.getItem("gruaduationDbId");
+ if (graduationDbId !== null) {
+ return new DB(Number(graduationDbId));
+ }
+
const courseUrl = `https://lernplattform.bycs.de/course/view.php?id=${getCourseId()}`;
const courseRes = await fetch(courseUrl);
const courseDoc = new DOMParser().parseFromString(await courseRes.text(), 'text/html');
- let dbs = new Map();
- for (const link of courseDoc.querySelectorAll("div.activityname a.aalink")) {
- if (link.textContent.trim() === "Submit Datenbank") {
- const dbId = link.href.match(ID_QUERY_PARAM_REGEX)[1]
- dbs.set("submit", new DB(dbId));
- continue;
- }
-
- const match = link.textContent.match(/(\d+)(Shadow|Public|Submit)/);
- if (!match) continue;
- const studentId = match[1];
- const dbType = match[2];
- const dbId = link.href.match(ID_QUERY_PARAM_REGEX)[1]
-
- if (!dbs.has(studentId)) {
- dbs.set(studentId, {});
+ for (const a of courseDoc.querySelectorAll("div.activityname a.aalink")) {
+ if (a.textContent.trim() === "Graduation Datenbank") {
+ const anchor = (a as HTMLAnchorElement);
+ const match = anchor.href.match(ID_QUERY_PARAM_REGEX);
+ if (match === null) continue;
+ const dbId = match[1];
+ localStorage.setItem("graduationDbId", dbId);
+ return new DB(Number(dbId))
}
-
- dbs.get(studentId)[dbType.toLowerCase()] = new DB(dbId);
}
+ console.assert(false, "Graduation DB not found");
+}
- return dbs;
+interface DatabaseParams {
+ [key: string]: string;
}
-async function getSubmitDb() {
- const dbs = await studentDbs;
- return dbs.get("submit");
+interface DatabaseEntry {
+ [key: string]: string;
}
class DB {
- constructor(id) {
+ id: number;
+
+ constructor(id: number) {
this.id = id;
}
- async fetchEntries(params) {
+ async fetchEntries(params?: DatabaseParams) {
return loadFromDatabaseActivity(this.id, params)
}
- async saveEntry(json) {
+ async saveEntry(json: string) {
return addEntryToDatabaseActivity(this.id, json);
}
- static async getStudentDbs(student) {
- const dbs = await studentDbs;
- return dbs.get(student.id);
- }
}
-async function loadFromDatabaseActivity(id, params) {
+async function loadFromDatabaseActivity(id: string | number, params?: DatabaseParams) {
let dbUrl = `${window.location.origin}/mod/data/view.php?id=${id}&mode=list&perpage=1000`;
// TODO(muhq): Why does this not include entries?
- if (params) {
+ if (typeof params !== "undefined") {
dbUrl += "&filter=1&advanced=1";
for (const param in params) {
dbUrl += `&${param}=${encodeURIComponent(params[param])}`
@@ -90,38 +85,37 @@ async function loadFromDatabaseActivity(id, params) {
const dbRes = await fetch(encodeURI(dbUrl));
const dbDoc = new DOMParser().parseFromString(await dbRes.text(), 'text/html');
- let data = [];
+ let data: any[] = [];
dbDoc.querySelectorAll('.db-row').forEach(row => {
- let entry = {};
- row.querySelectorAll('.db-cell').forEach(field => {
- const key = field.dataset.key;
+ let entry: DatabaseEntry = {};
+ row.querySelectorAll<HTMLElement>('.db-cell').forEach(field => {
+ const key = field.dataset.key!;
let value;
// Moodle includes the full date in the title attribute of the included span
if (key === "date") {
- value = field.querySelector("span").title;
+ value = field.querySelector("span")?.title;
} else {
value = field.innerText.trim();
}
- entry[key] = value;
+ entry[key] = value!;
});
data.push(entry);
});
return data;
}
-async function loadFromStudentDatabase(student, type) {
- const dbs = await DB.getStudentDbs(student);
- const db = dbs[type];
- return await db.fetchEntries()
+async function getGraduationDb() {
+ return (await graduationDb)!;
}
-async function addEntryToDatabaseActivity(id, json) {
+async function addEntryToDatabaseActivity(id: number, json: string) {
+ // TODO(muhq): do not fetch the form
const url = `/mod/data/edit.php?id=${id}`;
const editRes = await fetch(url);
const editDoc = new DOMParser().parseFromString(await editRes.text(), 'text/html');
- const form = editDoc.querySelector('div[role="main"] > form');
+ const form = editDoc.querySelector<HTMLFormElement>('div[role="main"] > form');
- const formData = new FormData(form);
+ const formData = new FormData(form!);
for (const key of formData.keys()) {
if (key.startsWith("field_")) {
@@ -139,22 +133,8 @@ async function addEntryToDatabaseActivity(id, json) {
});
}
-async function addEntryToStudentDatabase(student, type, json) {
- let db;
- // global submit database
- if (type === "submit") {
- db = await getSubmitDb();
- // individuell databases
- } else {
- const dbs = await DB.getStudentDbs(student);
- db = dbs[type];
- }
- await db.saveEntry(json);
-}
-
export {
DB,
- getSubmitDb,
- loadFromDatabaseActivity, loadFromStudentDatabase,
- addEntryToDatabaseActivity, addEntryToStudentDatabase,
+ DatabaseEntry, DatabaseParams,
+ getGraduationDb,
};
diff --git a/src/graduation-common.js b/src/graduation-common.js
deleted file mode 100644
index 0c28465..0000000
--- a/src/graduation-common.js
+++ /dev/null
@@ -1,396 +0,0 @@
-import { getSubmitDb, loadFromStudentDatabase, addEntryToStudentDatabase } from "./database.js"
-
-import NeulingPng from "../assets/Neuling.png";
-import EntdeckerPng from "../assets/Entdecker.png";
-import NavigatorPng from "../assets/Navigator.png";
-import PioneerPng from "../assets/Pioneer.png";
-
-const LEVEL_NAMES = ["Neuling", "Entdecker", "Navigator", "Pionieer"];
-
-const LEVEL_IMAGES = [
- new URL(NeulingPng),
- new URL(EntdeckerPng),
- new URL(NavigatorPng),
- new URL(PioneerPng),
-];
-
-class GraduationChange {
- constructor(date, change, reason, coach, context) {
- this.date = date;
- this.change = change;
- this.reason = reason;
- this.coach = coach;
- this.context = context;
- }
-
- isPublic() {
- return [
- "Grün", "Orange", "Rot",
- "ObservationStart", "ObservationEnd",
- "LevelChange"
- ].includes(this.change);
- }
-
- publicDBFields () {
- return {
- "change": this.change,
- "date": this.date,
- };
- }
-
- dbFields () {
- return {
- // We include the date in the json because Moodle reports the timeadded
- // locale dependent.
- "date": this.date,
- "change": this.change,
- "reason": this.reason,
- "context": this.context,
- };
- }
-
- static fromDBEntry(e) {
- const {coach, json} = e;
- const change = JSON.parse(json);
- return new GraduationChange(new Date(change.date), change.change, change.reason, coach, change.context);
- }
-
- static newObservationRequest() {
- return new GraduationChange(new Date(), "ObservationRequest", "", "", "");
- }
-
- static newObservationStart() {
- return new GraduationChange(new Date(), "ObservationStart", "", "", "");
- }
-
- static newObservationEnd(reason, success) {
- return new GraduationChange(new Date(), "ObservationEnd", reason, "", JSON.stringify(success));
- }
-
- static newObservationChange(observation) {
- return new GraduationChange(observation.date, "Observation", "", "", JSON.stringify(observation.status));
- }
-
- static newGreenPointChange(reason, coach) {
- return new GraduationChange(new Date(), "Grün", reason, coach, "");
- }
-
- static newOrangePointChange(reason, coach) {
- return new GraduationChange(new Date(), "Orange", reason, coach, "");
- }
-
- static newRedPointChange(reason, coach) {
- return new GraduationChange(new Date(), "Rot", reason, coach, "");
- }
-}
-
-class Graduation {
- constructor(student, green, orange, red, level, stickers, observationRequested, observation, pastObservations, changes) {
- this.student = student;
- this.green = green;
- this.orange = orange;
- this.red = red;
- this.level = level;
- this.stickers = stickers;
- this.observationRequested = observationRequested;
- this.observation = observation;
- this.pastObservations = pastObservations;
- this.changes = changes;
-
- student.graduation = this;
- }
-
- static from(student, changes) {
- let green = 0;
- let orange = 0;
- let red = 0;
- let level = 1;
- let openObservationRequest = null;
- let observationPeriod = null;
- let pastObservations = [];
- let stickers = [];
-
- // The parsing logic assumes that changes are ordered chronologically
- changes.sort((a, b) => a.date - b.date);
-
- for (const change of changes) {
- switch (change.change) {
- case "Grün":
- green++;
- break;
-
- case "Orange":
- orange++;
- break;
-
- case "Rot":
- red++;
- break;
-
- case "ObservationRequest":
- openObservationRequest = change.date;
- break;
-
- case "ObservationStart":
- console.assert(openObservationRequest, "No request for observation start");
- observationPeriod = new ObservationPeriod(change.date);
- openObservationRequest = null;
- break;
-
- case "ObservationEnd":
- console.assert(observationPeriod, "No active observation period");
- observationPeriod.finishPeriod(change.context.success);
- if (change.context.success) {
- level++;
- }
- pastObservations.push(observationPeriod);
- observationPeriod = null;
- break;
-
- case "Observation":
- console.assert(observationPeriod, "No active observation period");
- const observation = Observation.fromChange(change);
- observationPeriod.addObservation(observation);
- break;
-
- case "LevelChange":
- level = change.context.level;
- break;
-
- default:
- alert(`Unexpected change: ${change}`);
- }
- }
- return new Graduation(student, green, orange, red, level,
- stickers,
- openObservationRequest, observationPeriod, pastObservations,
- changes);
- }
-
- addChange(change) {
- // TODO(muhq): Implement incremental addition
- let changes = this.changes;
- changes.push(change);
- // This updates the student.graduation reference as well
- Graduation.from(this.student, changes);
- }
-
- /**
- * Request observation
- */
- async requestObservation() {
- const request = GraduationChange.newObservationRequest();
- this.observationRequested = request.date;
- await addEntryToStudentDatabase(this.student, "submit", JSON.stringify(request.publicDBFields()));
- }
-
- /**
- * Start an observation
- */
- async startObservation() {
- if (!this.observationRequested) {
- console.assert(this.observationRequested, "No observation requested");
- return;
- }
- this.observationRequested = null;
- this.observation = new ObservationPeriod(new Date());
- return submitGraduationChange(this.student, GraduationChange.newObservationStart());
- }
-
- /**
- * End an observation
- */
- async endObservation(reason, success) {
- if (!this.observation) {
- console.assert(this.observation, "No active observation");
- return;
- }
- this.observation.end = new Date();
- this.pastObservations.push(this.observation);
- this.observation = null;
- return submitGraduationChange(this.student, GraduationChange.newObservationEnd(reason, success));
- }
-
- /**
- * Return if the student is in observation.
- */
- isObserved() {
- return Boolean(this.observation);
- }
-
- /**
- * Return the URI of the icon representing the graduation level.
- */
- getIcon() {
- return LEVEL_IMAGES[this.level - 1];
- }
-
- /**
- * Return the name of the current graduation level.
- * @return {String} The name of the current level.
- */
- getLevelName() {
- return LEVEL_NAMES[this.level - 1];
- }
-}
-
-class ObservationPeriod {
- constructor(start) {
- this.start = start;
- this.observations = [];
- }
-
- finishPeriod(success) {
- this.end = new Date();
- this.success = success;
- }
-
- ended() {
- return Boolean(this.end);
- }
-
- ready() {
- if (this.observations.length < 10)
- return false;
-
- // at least 10 observation on different days
- let days = new Map();
- for (const observation of this.observations) {
- const d = observation.date;
- days.set(`${d.getFullYear()}${d.getMonth()}${d.getDate()}`, true);
- }
- return days.size >= 10;
- }
-
- getObservationsSums() {
- let sums = Array(OBSERVATION_TYPES.length).fill().map(() => ({"yes":0, "no":0}));
- for (const observation of this.observations) {
- for (let i = 0; i < sums.length; i++) {
- if (observation.status[i]) {
- if (observation.status[i] === "yes")
- sums[i].yes++;
- else
- sums[i].no++;
- }
- }
- }
- return sums;
- }
-
- addObservation(observation) {
- this.observations.push(observation);
- }
-}
-
-const OBSERVATION_TYPES = [
- "Arbeitet konzentriert an Aufgaben",
- "Arbeitet zunehmend selbstständig",
- "Führt das Logbuch zuverlässig",
- "Bringt Material mit",
- "Hält Regeln ein",
- "Geht verantwortungsvoll mit Materialien und LernRaum um",
- "Nimmt Rückmeldungen an und setzt sie um",
-];
-
-class Observation {
- constructor(date, status) {
- console.assert(status.length == OBSERVATION_TYPES.length, "invalid observation status");
- this.date = date;
- this.status = status;
- }
-
- static fromChange(change) {
- return new Observation(change.date, JSON.parse(change.context))
- }
-}
-
-function changesFromDbEntries(entries) {
- let changes = [];
- for (const e of entries) {
- const gradChange = GraduationChange.fromDBEntry(e);
- changes.push(gradChange);
- }
- return changes;
-}
-
-async function fetchGraduationChangesFromDb(student, type) {
- const data = await loadFromStudentDatabase(student, type);
- return changesFromDbEntries(data);
-}
-
-const submitEntriesPromise = async function() {
- const db = await getSubmitDb();
- const entries = await db.fetchEntries();
- return changesFromDbEntries(entries);
-}();
-
-async function addSubmittedChanges(student, changes) {
- let submitEntries = await submitEntriesPromise;
- for (const e of submitEntries.filter( (c) => c.coach === student.name )) {
- changes.push(e);
- }
- return changes;
-}
-
-async function fetchGraduationChanges(student) {
- let changes = await fetchGraduationChangesFromDb(student, "shadow");
- return addSubmittedChanges(student, changes);
-}
-
-async function fetchPublicGraduationChanges(student) {
- let changes = await fetchGraduationChangesFromDb(student, "public");
- return addSubmittedChanges(student, changes);
-}
-
-async function fetchGraduation(student) {
- const changes = await fetchGraduationChanges(student);
- return Graduation.from(student, changes);
-}
-
-async function fetchPublicGraduation(student) {
- const changes = await fetchPublicGraduationChanges(student);
- return Graduation.from(student, changes);
-}
-
-async function submitGraduationChange(student, change) {
- student.graduation.addChange(change);
-
- // Submit changes concurrently
- let promises = [];
-
- if (change.isPublic()) {
- promises.push(
- // TODO(muhq): strip non public fields from changes
- addEntryToStudentDatabase(student, "public", JSON.stringify(change.publicDBFields()))
- );
- }
-
- promises.push(
- addEntryToStudentDatabase(student, "shadow", JSON.stringify(change.dbFields()))
- );
-
- await Promise.all(promises);
-}
-
-async function submitObservation(student, observation) {
- console.assert(student.graduation.observation, "no active observation");
- student.graduation.observation.addObservation(observation);
- await submitGraduationChange(student, GraduationChange.newObservationChange(observation));
-}
-
-export {
- LEVEL_NAMES,
- LEVEL_IMAGES,
- GraduationChange,
- Graduation,
- fetchGraduation,
- fetchPublicGraduation,
- fetchGraduationChanges,
- fetchPublicGraduationChanges,
- submitGraduationChange,
- submitObservation,
-
- OBSERVATION_TYPES,
- Observation,
- ObservationPeriod,
-};
diff --git a/src/graduation-common.ts b/src/graduation-common.ts
new file mode 100644
index 0000000..61600d3
--- /dev/null
+++ b/src/graduation-common.ts
@@ -0,0 +1,347 @@
+import { getGraduationDb, DatabaseEntry } from "./database-ENV.ts"
+import type { Student } from "./student-common.ts"
+
+import NeulingPng from "../assets/Neuling.png";
+import EntdeckerPng from "../assets/Entdecker.png";
+import NavigatorPng from "../assets/Navigator.png";
+import PioneerPng from "../assets/Pioneer.png";
+
+const LEVEL_NAMES = ["Neuling", "Entdecker", "Navigator", "Pionieer"];
+
+const LEVEL_IMAGES = [
+ new URL(NeulingPng),
+ new URL(EntdeckerPng),
+ new URL(NavigatorPng),
+ new URL(PioneerPng),
+];
+
+class GraduationChange {
+ date: Date;
+ studentId: number;
+ change: string;
+ reason: string;
+ coach: string;
+ context: any;
+
+ constructor(date: Date, studentId: number, change: string, reason: string, coach: string, context: any) {
+ this.date = date;
+ this.studentId = studentId;
+ this.change = change;
+ this.reason = reason;
+ this.coach = coach;
+ this.context = context;
+ }
+
+ dbFields () {
+ return {
+ "studentId": this.studentId,
+ "change": this.change,
+ "reason": this.reason,
+ "context": this.context,
+ };
+ }
+
+ static fromDBEntry(e: DatabaseEntry) {
+ const {date, coach, json} = e;
+ const change = JSON.parse(json);
+ return new GraduationChange(new Date(date), change.change, change.student, change.reason, coach, change.context);
+ }
+
+ static newObservationRequest() {
+ return new GraduationChange(new Date(), -1, "ObservationRequest", "", "", "");
+ }
+
+ static newObservationStart() {
+ return new GraduationChange(new Date(), -1, "ObservationStart", "", "", "");
+ }
+
+ static newObservationEnd(reason: string, success: boolean) {
+ return new GraduationChange(new Date(), -1, "ObservationEnd", reason, "", JSON.stringify(success));
+ }
+
+ static newObservationChange(observation: Observation) {
+ return new GraduationChange(observation.date, -1, "Observation", "", "", JSON.stringify(observation.status));
+ }
+
+ static newGreenPointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Grün", reason, coach, "");
+ }
+
+ static newOrangePointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Orange", reason, coach, "");
+ }
+
+ static newRedPointChange(reason: string, coach: string) {
+ return new GraduationChange(new Date(), -1, "Rot", reason, coach, "");
+ }
+}
+
+class Graduation {
+ student: Student;
+ green: number;
+ orange: number;
+ red: number;
+ level: number;
+ observationRequested: Date | null;
+ observation: ObservationPeriod | null;
+ pastObservations: Array<ObservationPeriod>;
+ changes: Array<GraduationChange>;
+
+ constructor(student: Student, green: number, orange: number, red: number, level: number, observationRequested: Date | null, observation: ObservationPeriod | null, pastObservations: Array<ObservationPeriod>, changes: Array<GraduationChange>) {
+ this.student = student;
+ this.green = green;
+ this.orange = orange;
+ this.red = red;
+ this.level = level;
+ this.observationRequested = observationRequested;
+ this.observation = observation;
+ this.pastObservations = pastObservations;
+ this.changes = changes;
+
+ student.graduation = this;
+ }
+
+ static from(student: Student, changes: Array<GraduationChange>) {
+ let green = 0;
+ let orange = 0;
+ let red = 0;
+ let level = 1;
+ let openObservationRequest = null;
+ let observationPeriod = null;
+ let pastObservations = [];
+
+ // The parsing logic assumes that changes are ordered chronologically
+ changes.sort((a, b) => a.date.valueOf() - b.date.valueOf());
+
+ for (const change of changes) {
+ switch (change.change) {
+ case "Grün":
+ green++;
+ break;
+
+ case "Orange":
+ orange++;
+ break;
+
+ case "Rot":
+ red++;
+ break;
+
+ case "ObservationRequest":
+ openObservationRequest = change.date;
+ break;
+
+ case "ObservationStart":
+ console.assert(openObservationRequest === null,
+ "No request for observation start");
+
+ observationPeriod = new ObservationPeriod(change.date);
+ openObservationRequest = null;
+ break;
+
+ case "ObservationEnd":
+ if (observationPeriod === null) {
+ console.error("No active observation period");
+ continue;
+ }
+ observationPeriod.finishPeriod(change.context.success);
+ if (change.context.success) {
+ level++;
+ }
+ pastObservations.push(observationPeriod);
+ observationPeriod = null;
+ break;
+
+ case "Observation":
+ if (observationPeriod === null) {
+ console.error("No active observation period");
+ continue;
+ }
+ const observation = Observation.fromChange(change);
+ observationPeriod.addObservation(observation);
+ break;
+
+ case "LevelChange":
+ level = change.context.level;
+ break;
+
+ default:
+ throw new Error(`Unexpected change: ${change}`);
+ }
+ }
+ return new Graduation(student, green, orange, red, level,
+ openObservationRequest, observationPeriod, pastObservations,
+ changes);
+ }
+
+ addChange(change: GraduationChange) {
+ change.studentId = this.student.id;
+ // TODO(muhq): Implement incremental addition
+ let changes = this.changes;
+ changes.push(change);
+ // This updates the student.graduation reference as well
+ Graduation.from(this.student, changes);
+ }
+
+ /**
+ * Request observation
+ */
+ async requestObservation() {
+ const request = GraduationChange.newObservationRequest();
+ this.observationRequested = request.date;
+ const db = await getGraduationDb();
+ await db.saveEntry(JSON.stringify(request.dbFields()));
+ }
+
+ /**
+ * Start an observation
+ */
+ async startObservation() {
+ if (this.observationRequested === null) {
+ console.error("No observation requested");
+ return;
+ }
+ this.observationRequested = null;
+ this.observation = new ObservationPeriod(new Date());
+ return submitGraduationChange(this, GraduationChange.newObservationStart());
+ }
+
+ /**
+ * End an observation
+ */
+ async endObservation(reason: string, success: boolean) {
+ if (this.observation === null) {
+ console.error("No active observation");
+ return;
+ }
+ this.observation.end = new Date();
+ this.pastObservations.push(this.observation);
+ this.observation = null;
+ return submitGraduationChange(this, GraduationChange.newObservationEnd(reason, success));
+ }
+
+ /**
+ * Return if the student is in observation.
+ */
+ isObserved() {
+ return Boolean(this.observation);
+ }
+
+ /**
+ * Return the URI of the icon representing the graduation level.
+ */
+ getIcon() {
+ return LEVEL_IMAGES[this.level - 1];
+ }
+
+ /**
+ * Return the name of the current graduation level.
+ */
+ getLevelName() {
+ return LEVEL_NAMES[this.level - 1];
+ }
+}
+
+class ObservationPeriod {
+ start: Date;
+ end: Date | null;
+ observations: Array<Observation>;
+ success = false;
+
+ constructor(start: Date) {
+ this.start = start;
+ this.end = null;
+ this.observations = [];
+ }
+
+ finishPeriod(success: boolean) {
+ this.end = new Date();
+ this.success = success;
+ }
+
+ ended() {
+ return Boolean(this.end);
+ }
+
+ ready() {
+ if (this.observations.length < 10)
+ return false;
+
+ // at least 10 observation on different days
+ let days = new Map();
+ for (const observation of this.observations) {
+ const d = observation.date;
+ days.set(`${d.getFullYear()}${d.getMonth()}${d.getDate()}`, true);
+ }
+ return days.size >= 10;
+ }
+
+ getObservationsSums() {
+ let sums = Array(OBSERVATION_TYPES.length).fill(null).map(() => ({"yes":0, "no":0}));
+ for (const observation of this.observations) {
+ for (let i = 0; i < sums.length; i++) {
+ if (observation.status[i]) {
+ if (observation.status[i] === "yes")
+ sums[i].yes++;
+ else
+ sums[i].no++;
+ }
+ }
+ }
+ return sums;
+ }
+
+ addObservation(observation: Observation) {
+ this.observations.push(observation);
+ }
+}
+
+const OBSERVATION_TYPES = [
+ "Arbeitet konzentriert an Aufgaben",
+ "Arbeitet zunehmend selbstständig",
+ "Führt das Logbuch zuverlässig",
+ "Bringt Material mit",
+ "Hält Regeln ein",
+ "Geht verantwortungsvoll mit Materialien und LernRaum um",
+ "Nimmt Rückmeldungen an und setzt sie um",
+];
+
+class Observation {
+ date: Date;
+ status: Array<string>;
+
+ constructor(date: Date, status: Array<string>) {
+ console.assert(status.length == OBSERVATION_TYPES.length, "invalid observation status");
+ this.date = date;
+ this.status = status;
+ }
+
+ static fromChange(change: GraduationChange) {
+ return new Observation(change.date, JSON.parse(change.context))
+ }
+}
+
+async function submitGraduationChange(graduation: Graduation, change: GraduationChange) {
+ graduation.addChange(change);
+ const db = await getGraduationDb();
+ return db.saveEntry(JSON.stringify(change.dbFields()));
+}
+
+async function submitObservation(graduation: Graduation, observation: Observation) {
+ if(graduation.observation === null) throw new Error("no active observation");
+ graduation.observation.addObservation(observation);
+ await submitGraduationChange(graduation, GraduationChange.newObservationChange(observation));
+}
+
+export {
+ LEVEL_NAMES,
+ LEVEL_IMAGES,
+ GraduationChange,
+ Graduation,
+ submitGraduationChange,
+ submitObservation,
+
+ OBSERVATION_TYPES,
+ Observation,
+ ObservationPeriod,
+};
diff --git a/src/graduation-mock.js b/src/graduation-mock.ts
index 1c84433..84a7143 100644
--- a/src/graduation-mock.js
+++ b/src/graduation-mock.ts
@@ -6,21 +6,19 @@ import {
OBSERVATION_TYPES,
Observation,
ObservationPeriod,
-} from "./graduation-common.js";
+} from "./graduation-common.ts";
import {
fetchGraduationMock as fetchGraduation,
- fetchPublicGraduationMock as fetchPublicGraduation,
submitGraduationChangeMock as submitGraduationChange,
submitObservationMock as submitObservation
-} from "./mock.js";
+} from "./mock.ts";
export {
LEVEL_NAMES,
GraduationChange,
Graduation,
fetchGraduation,
- fetchPublicGraduation,
submitGraduationChange,
submitObservation,
diff --git a/src/graduation-prod.js b/src/graduation-prod.js
deleted file mode 100644
index 0e86956..0000000
--- a/src/graduation-prod.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export {
- LEVEL_NAMES,
- GraduationChange,
- Graduation,
- fetchGraduation,
- fetchPublicGraduation,
- fetchGraduationChanges,
- fetchPublicGraduationChanges,
- submitGraduationChange,
- submitObservation,
-
- OBSERVATION_TYPES,
- Observation,
- ObservationPeriod,
-} from "./graduation-common.js";
diff --git a/src/graduation-prod.ts b/src/graduation-prod.ts
new file mode 100644
index 0000000..975fd0f
--- /dev/null
+++ b/src/graduation-prod.ts
@@ -0,0 +1,43 @@
+import { getGraduationDb } from "./database-ENV.ts"
+import { Graduation, GraduationChange } from "./graduation-common.ts";
+import type { Student } from "./student-common.ts";
+
+const _graduationChangesPromise = async function() {
+ const db = await getGraduationDb();
+ let changes = new Map();
+ let entries = await db.fetchEntries();
+ for (const e of entries) {
+ const gradChange = GraduationChange.fromDBEntry(e);
+ const studentChanges = changes.get(gradChange.studentId);
+ if (studentChanges === null) {
+ changes.set(gradChange.studentId, [gradChange]);
+ } else {
+ studentChanges.push(gradChange);
+ }
+ }
+ return changes;
+}();
+
+const graduations: Map<number, Graduation> = new Map();
+
+export async function fetchGraduation(student: Student): Promise<Graduation> {
+ if (!graduations.has(student.id)) {
+ const studentChanges = (await _graduationChangesPromise).get(student.id) ?? [];
+ const graduation = Graduation.from(student, studentChanges);
+ graduations.set(student.id, graduation);
+ return graduation;
+ }
+ return graduations.get(student.id)!;
+}
+
+export {
+ LEVEL_NAMES,
+ GraduationChange,
+ Graduation,
+ submitGraduationChange,
+ submitObservation,
+
+ OBSERVATION_TYPES,
+ Observation,
+ ObservationPeriod,
+} from "./graduation-common.ts";
diff --git a/src/group-fetch.ts b/src/group-fetch.ts
new file mode 100644
index 0000000..de0991d
--- /dev/null
+++ b/src/group-fetch.ts
@@ -0,0 +1,18 @@
+import { fetchStudents } from "./student-ENV.ts";
+import { Group, buildGroups, _groups } from "./group.ts";
+
+/**
+ * Fetch all Groups and return them as a map.
+ */
+async function fetchGroups() {
+ if (_groups.size) {
+ return _groups;
+ }
+
+ // Fetch all students and build the groups from them
+ const students = await fetchStudents();
+ buildGroups(students);
+ return _groups;
+}
+
+export { Group, buildGroups, fetchGroups };
diff --git a/src/group.js b/src/group.js
deleted file mode 100644
index d52707b..0000000
--- a/src/group.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import { getCourseParticipantsUrl } from "./utils-ENV.js";
-import { fetchStudents } from "./student-ENV.js";
-
-/**
- * Class to represent a Group of Students.
- */
-class Group {
- constructor(name, students) {
- this.name = name;
- if (!students) {
- this.students = [];
- }
- }
-
- setStudents(students) {
- this.students = students;
- }
-
- addStudent(student) {
- this.students.push(student);
- }
-
- async getStudents() {
- return this.students;
- }
-}
-
-const groups = new Map();
-
-function buildGroups(students) {
- for (const student of students) {
- let group = groups.get(student.group);
-
- if (!group) {
- group = new Group(student.group);
- groups.set(student.group, group);
- }
-
- group.addStudent(student);
- }
-}
-
-/**
- * Fetch all Groups and return them as a map.
- */
-async function fetchGroups() {
- if (groups.size) {
- return groups;
- }
-
- // Fetch all students and build the groups from them
- const students = await fetchStudents();
- buildGroups(students);
- return groups;
-}
-
-export { Group, buildGroups, fetchGroups };
diff --git a/src/group.ts b/src/group.ts
new file mode 100644
index 0000000..e840ffd
--- /dev/null
+++ b/src/group.ts
@@ -0,0 +1,45 @@
+import type { Student } from "./student-common.ts";
+
+/**
+ * Class to represent a Group of Students.
+ */
+class Group {
+ name: string;
+ students: Array<Student> = [];
+
+ constructor(name: string, students?: Array<Student>) {
+ this.name = name;
+ if (typeof students !== "undefined") {
+ this.students = students;
+ }
+ }
+
+ setStudents(students: Array<Student>) {
+ this.students = students;
+ }
+
+ addStudent(student: Student) {
+ this.students.push(student);
+ }
+
+ async getStudents() {
+ return this.students;
+ }
+}
+
+const groups = new Map();
+
+function buildGroups(students: Array<Student>) {
+ for (const student of students) {
+ let group = groups.get(student.group);
+
+ if (!group) {
+ group = new Group(student.group);
+ groups.set(student.group, group);
+ }
+
+ group.addStudent(student);
+ }
+}
+
+export { Group, buildGroups, groups as _groups };
diff --git a/src/mass-graduation.js b/src/mass-graduation.js
index 0a69cd1..b9240f0 100644
--- a/src/mass-graduation.js
+++ b/src/mass-graduation.js
@@ -4,9 +4,9 @@ import {
submitGraduationChange,
Observation,
submitObservation
-} from "./graduation-ENV.js";
+} from "./graduation-ENV.ts";
-import { fetchGroups } from "./group.js";
+import { fetchGroups } from "./group-fetch.ts";
import { initSelection } from "./selection.js";
import { getName, DATETIME_FMT, datetimeStringNow } from "./utils-ENV.js";
diff --git a/src/mock.js b/src/mock.js
deleted file mode 100644
index 33d1a1a..0000000
--- a/src/mock.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import { Graduation, GraduationChange } from "./graduation-common.js";
-import { Student } from "./student-common.js";
-import { buildGroups } from "./group.js";
-
-const students = [
- new Student(1, 'foo bar', 'test'),
- new Student(2, 'maxi mustermensch', 'test'),
- new Student(3, 'b b b', '5a')
-];
-
-// Build the groups from our mock students
-buildGroups(students);
-
-function fetchStudentsMock() {
- return new Promise((resolve) => { resolve(students); });
-}
-
-function fetchSubjectsMock() {
- return new Promise((resolve) => {
- resolve(['Mathe', 'Deutsch', 'Englisch']);
- });
-}
-
-function fetchProgressMock(subject) {
- return new Promise((resolve) => {
- if (subject === "Mathe") {
- resolve([40, 10, 3]);
- } else if (subject === "Deutsch") {
- resolve([10, 2, 0]);
- } else if (subject === "Englisch") {
- resolve([50, 25, 10]);
- } else {
- resolve([0, 0, 0]);
- }
- });
-}
-
-const graduationChanges = {};
-for (const s of students) {
- switch (s.name) {
- case "foo bar":
- graduationChanges[s.name] = [
- GraduationChange.newGreenPointChange("Gute Arbeit", "Florian Fischer"),
- GraduationChange.newOrangePointChange("", "Florian Fischer"),
- ];
- break;
- case "maxi mustermensch":
- graduationChanges[s.name] = [
- GraduationChange.newGreenPointChange("", "Florian Fischer"),
- GraduationChange.newGreenPointChange("", "Florian Fischer"),
- new GraduationChange(new Date(), "ObservationStart", "", "Florian Fischer", ""),
- ];
- break;
- case "b b b":
- graduationChanges[s.name] = [
- GraduationChange.newRedPointChange("", "Florian Fischer"),
- ];
- }
- // Build the student's graduation
- Graduation.from(s, graduationChanges[s.name]);
-}
-
-function fetchGraduationMock(s) {
- return new Promise((resolve) => {
- const changes = graduationChanges[s.name];
- if (!changes) {
- resolve(null);
- } else {
- resolve(Graduation.from(s, changes));
- }
- });
-}
-
-function fetchPublicGraduationMock(s) {
- return new Promise((resolve) => {
- const changes = graduationChanges[s.name];
- if (!changes) {
- resolve(null);
- } else {
- resolve(Graduation.from(s, changes.filter((change) => change.isPublic())));
- }
- });
-}
-
-function submitGraduationChangeMock(s, c) {
- console.assert(s.graduation, "no graduation");
- s.graduation.addChange(c);
-}
-
-function submitObservationMock(s, o) {
- console.assert(s.graduation, "no graduation");
- console.assert(s.graduation.observation, "no observation period");
- s.graduation.observation.addObservation(o);
-}
-
-export {
- fetchStudentsMock,
- fetchSubjectsMock,
- fetchProgressMock,
- fetchGraduationMock,
- fetchPublicGraduationMock,
- submitGraduationChangeMock,
- submitObservationMock,
-};
diff --git a/src/mock.ts b/src/mock.ts
new file mode 100644
index 0000000..220b1e3
--- /dev/null
+++ b/src/mock.ts
@@ -0,0 +1,81 @@
+import { Graduation, GraduationChange, Observation } from "./graduation-common.ts";
+import { Student } from "./student-common.ts";
+import { buildGroups } from "./group.ts";
+
+const students = [
+ new Student(1, 'foo bar', 'test'),
+ new Student(2, 'maxi mustermensch', 'test'),
+ new Student(3, 'b b b', '5a')
+];
+
+// Build the groups from our mock students
+buildGroups(students);
+
+async function fetchStudentsMock(_id?: number, _name?: string, _group?: string) {
+ return students;
+}
+
+async function fetchSubjectsMock() {
+ return ['Mathe', 'Deutsch', 'Englisch'];
+}
+
+function fetchProgressMock(subject: string) {
+ return new Promise((resolve) => {
+ if (subject === "Mathe") {
+ resolve([40, 10, 3]);
+ } else if (subject === "Deutsch") {
+ resolve([10, 2, 0]);
+ } else if (subject === "Englisch") {
+ resolve([50, 25, 10]);
+ } else {
+ resolve([0, 0, 0]);
+ }
+ });
+}
+
+const graduations = new Map();
+for (const s of students) {
+ switch (s.name) {
+ case "foo bar":
+ graduations.set(s, Graduation.from(s, [
+ GraduationChange.newGreenPointChange("Gute Arbeit", "Florian Fischer"),
+ GraduationChange.newOrangePointChange("", "Florian Fischer"),
+ ]));
+ break;
+ case "maxi mustermensch":
+ graduations.set(s, Graduation.from(s, [
+ GraduationChange.newGreenPointChange("", "Florian Fischer"),
+ GraduationChange.newGreenPointChange("", "Florian Fischer"),
+ new GraduationChange(new Date(), s.id, "ObservationStart", "", "Florian Fischer", ""),
+ ]));
+ break;
+ case "b b b":
+ graduations.set(s, Graduation.from(s, [
+ GraduationChange.newRedPointChange("", "Florian Fischer"),
+ ]));
+ }
+}
+
+async function fetchGraduationMock(s: Student) {
+ return graduations.get(s);
+}
+
+function submitGraduationChangeMock(s: Student, c: GraduationChange) {
+ if (s.graduation === null) throw new Error("no graduation");
+ s.graduation.addChange(c);
+}
+
+function submitObservationMock(s: Student, o: Observation) {
+ if (s.graduation === null) throw new Error("no graduation");
+ if (s.graduation.observation === null) throw new Error("no observation period");
+ s.graduation.observation.addObservation(o);
+}
+
+export {
+ fetchStudentsMock,
+ fetchSubjectsMock,
+ fetchProgressMock,
+ fetchGraduationMock,
+ submitGraduationChangeMock,
+ submitObservationMock,
+};
diff --git a/src/student-common.js b/src/student-common.js
deleted file mode 100644
index 2dfa35d..0000000
--- a/src/student-common.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { getCourseId, getUserIconUrl, getCourseParticipantsUrl } from "./utils-ENV.js";
-import { fetchGraduation } from "./graduation-common.js";
-/**
- * Class to represent Students.
- */
-class Student {
- graduation = null;
-
- constructor(id, name, group) {
- this.id = id;
- this.name = name;
- this.group = group;
- }
-
- async getIconUrl() {
- return await getUserIconUrl(this.id);
- }
-
- getGroup() {
- return this.group;
- }
-
- async getGraduation() {
- if(!this.graduation) {
- // fetchGraduation(this) sets this.graduation
- await fetchGraduation(this);
- }
- return this.graduation;
- }
-
- static async fetchById(id) {
- let name = "";
- let group = "";
-
- const res = await fetch(`/user/view.php?id=${id}&course=${getCourseId()}`);
- const doc = new DOMParser().parseFromString(await res.text(), "text/html");
-
- name = doc.querySelector("div.page-header-headings > h2").textContent;
-
- const cards = doc.querySelectorAll("div.card-body dl");
- for (const card of cards) {
- if (card.querySelector("dt").textContent !== "Gruppe")
- continue;
- group = card.querySelector("dd > a").textContent;
- break;
- }
-
- return new Student(id, name, group);
- }
-}
-
-// Cache the fetched students.
-let students = [];
-
-const idRegex = /id=(\d+)/;
-async function _fetchStudents() {
- const res = await fetch(getCourseParticipantsUrl());
- const doc = new DOMParser().parseFromString(await res.text(), "text/html");
- const table = doc.querySelector("tbody");
- let fetchedStudents = [];
- table.querySelectorAll("tr").forEach(row => {
- if (row.classList.contains("emptyrow"))
- return;
-
- const roleCell = row.querySelector(".c4");
- // TODO(muhq): Is this locale dependent?
- if (roleCell.innerHTML.includes("Lehrer")) {
- return;
- }
-
- const nameCell = row.querySelector(".c1");
- let name;
- if (nameCell.firstChild.firstChild.tagName == "SPAN")
- name = nameCell.firstChild.firstChild.title;
- else
- name = nameCell.innerText;
-
- const profileUrl = nameCell.querySelector("a.aabtn").href;
- const match = idRegex.exec(profileUrl);
- const id = match[1];
-
- const groupCell = row.querySelector(".c5");
- const group = groupCell.querySelector("a.quickeditlink").textContent;
-
- fetchedStudents.push(new Student(id, name, group));
- });
- return fetchedStudents;
-}
-
-async function fetchStudents(idFilter, nameFilter, groupFilter) {
- if (!students.size) {
- const studentsPromise = _fetchStudents()
- const storedStudents = localStorage.getItem("students");
- if (storedStudents) {
- studentsPromise.then((fetched) => localStorage.setItem("students", JSON.stringify(fetched)));
- for (const o of JSON.parse(storedStudents)) {
- students.push(new Student(o.id, o.name, o.group));
- }
- } else {
- students = await studentsPromise;
- localStorage.setItem("students", JSON.stringify(students));
- }
- }
-
- return students.filter((s) => (!idFilter || s.id === idFilter) &&
- (!nameFilter || s.name.includes(nameFilter)) &&
- (!groupFilter || s.group.includes(groupFilter)));
-}
-
-async function fetchStudent(idFilter, nameFilter, groupFilter) {
- const students = await fetchStudents(idFilter, nameFilter, groupFilter);
- return students[0];
-}
-
-export { Student, fetchStudents, fetchStudent };
diff --git a/src/student-common.ts b/src/student-common.ts
new file mode 100644
index 0000000..a76a816
--- /dev/null
+++ b/src/student-common.ts
@@ -0,0 +1,42 @@
+import type { Graduation } from "./graduation-common.ts";
+/**
+ * Class to represent Students.
+ */
+export class Student {
+ id: number;
+ name: string;
+ group: string;
+ graduation: Graduation | null = null;
+
+ constructor(id: number, name: string, group: string) {
+ this.id = id;
+ this.name = name;
+ this.group = group;
+ }
+
+ getGroup() {
+ return this.group;
+ }
+
+ /**
+ static async fetchById(id: number) {
+ let name = "";
+ let group = "";
+
+ const res = await fetch(`/user/view.php?id=${id}&course=${getCourseId()}`);
+ const doc = new DOMParser().parseFromString(await res.text(), "text/html");
+
+ name = doc.querySelector("div.page-header-headings > h2")!.textContent;
+
+ const cards = doc.querySelectorAll("div.card-body dl");
+ for (const card of cards) {
+ if (card.querySelector("dt")!.textContent !== "Gruppe")
+ continue;
+ group = card.querySelector("dd > a")!.textContent;
+ break;
+ }
+
+ return new Student(id, name, group);
+ }
+ */
+}
diff --git a/src/student-mock.js b/src/student-mock.js
deleted file mode 100644
index 7c79e12..0000000
--- a/src/student-mock.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import { Student } from "./student-common.js";
-import { fetchStudentsMock as fetchStudents } from "./mock.js";
-
-async function fetchStudent(idFilter, nameFilter, groupFilter) {
- const students = await fetchStudents(idFilter, nameFilter, groupFilter);
- return students[0];
-}
-
-export { Student, fetchStudents, fetchStudent };
diff --git a/src/student-mock.ts b/src/student-mock.ts
new file mode 100644
index 0000000..93b45b0
--- /dev/null
+++ b/src/student-mock.ts
@@ -0,0 +1,25 @@
+import type { Graduation } from "./graduation-common.ts";
+import { Student } from "./student-common.ts";
+import { fetchStudentsMock as fetchStudents, fetchGraduationMock } from "./mock.ts";
+
+declare module "./student-common.ts" {
+ interface Student {
+ getGraduation(): Promise<Graduation>;
+ getIconUrl(): Promise<string>;
+ }
+}
+Student.prototype.getGraduation = function () {
+ return fetchGraduationMock(this);
+};
+
+Student.prototype.getIconUrl = async function () {
+ return "";
+};
+
+
+async function fetchStudent(_idFilter?: number, _nameFilter?: string, _groupFilter?: string) {
+ const students = await fetchStudents();
+ return students[0];
+}
+
+export { Student, fetchStudents, fetchStudent };
diff --git a/src/student-prod.js b/src/student-prod.js
deleted file mode 100644
index 9c8d7cb..0000000
--- a/src/student-prod.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import { Student, fetchStudent, fetchStudents } from "./student-common.js";
-export { Student, fetchStudents, fetchStudent };
diff --git a/src/student-prod.ts b/src/student-prod.ts
new file mode 100644
index 0000000..24ac869
--- /dev/null
+++ b/src/student-prod.ts
@@ -0,0 +1,90 @@
+import type { Graduation } from "./graduation-common.ts";
+import { Student } from "./student-common.ts";
+import { getUserIconUrl, getCourseParticipantsUrl } from "./utils-prod.js"
+import { fetchGraduation } from "./graduation-prod.ts";
+
+declare module "./student-common.ts" {
+ interface Student {
+ getGraduation(): Promise<Graduation>;
+ getIconUrl(): Promise<string>;
+ }
+}
+Student.prototype.getGraduation = function () {
+ return fetchGraduation(this);
+};
+
+Student.prototype.getIconUrl = function () {
+ return getUserIconUrl(this.id);
+};
+
+// Cache the fetched students.
+let students: Array<Student> | null = null;
+
+const idRegex = /id=(\d+)/;
+async function _fetchStudents() {
+ const res = await fetch(getCourseParticipantsUrl());
+ const doc = new DOMParser().parseFromString(await res.text(), "text/html");
+ const table = doc.querySelector("tbody")!;
+ const fetchedStudents: Array<Student> = [];
+ table.querySelectorAll("tr").forEach(row => {
+ if (row.classList.contains("emptyrow"))
+ return;
+
+ const roleCell = row.querySelector(".c4")!;
+ // TODO(muhq): Is this locale dependent?
+ if (roleCell.innerHTML.includes("Lehrer")) {
+ return;
+ }
+
+ const nameCell = row.querySelector<HTMLElement>(".c1")!;
+ let name;
+ const c = nameCell.firstChild!.firstChild! as HTMLElement;
+ if (c.tagName == "SPAN")
+ name = c.title;
+ else
+ name = nameCell.innerText;
+
+ const profileUrl = nameCell.querySelector<HTMLAnchorElement>("a.aabtn")!.href;
+ const match = idRegex.exec(profileUrl);
+ if (match === null) {
+ console.error("Profile Regex did not match");
+ return;
+ }
+ const id = match[1];
+
+ const groupCell = row.querySelector(".c5")!;
+ const group = groupCell.querySelector<HTMLElement>("a.quickeditlink")!.textContent;
+
+ fetchedStudents.push(new Student(Number(id), name, group));
+ });
+ return fetchedStudents;
+}
+
+async function fetchStudents(idFilter?: number, nameFilter?: string, groupFilter?: string) {
+ if (students === null) {
+ const studentsPromise = _fetchStudents();
+ const storedStudents = localStorage.getItem("students");
+ if (storedStudents) {
+ studentsPromise.then((fetched) => localStorage.setItem("students", JSON.stringify(fetched)));
+ students = [];
+ for (const o of JSON.parse(storedStudents)) {
+ students.push(new Student(o.id, o.name, o.group));
+ }
+ } else {
+ students = await studentsPromise;
+ localStorage.setItem("students", JSON.stringify(students));
+ }
+ }
+
+ return students.filter((s) =>
+ (typeof idFilter === "undefined" || s.id === idFilter) &&
+ (typeof nameFilter === "undefined" || s.name.includes(nameFilter)) &&
+ (typeof groupFilter === "undefined" || s.group.includes(groupFilter)));
+}
+
+async function fetchStudent(idFilter: number, nameFilter: string, groupFilter: string) {
+ const students = await fetchStudents(idFilter, nameFilter, groupFilter);
+ return students[0];
+}
+
+export { Student, fetchStudents, fetchStudent };
diff --git a/src/untyped.d.ts b/src/untyped.d.ts
new file mode 100644
index 0000000..bae3ce7
--- /dev/null
+++ b/src/untyped.d.ts
@@ -0,0 +1,4 @@
+declare module "*.png" {
+ let data: string;
+ export default data;
+}
diff --git a/src/utils-common.ts b/src/utils-common.ts
index 89a8ddd..f3ad944 100644
--- a/src/utils-common.ts
+++ b/src/utils-common.ts
@@ -13,7 +13,7 @@ export function datetimeStringNow() {
}
// TODO(muhq): Cache the retrieved URL
-export async function getUserIconUrl(userId: number) {
+export async function getUserIconUrl(userId: number): Promise<string> {
const userRes = await fetch(`https://lernplattform.bycs.de/user/profile.php?id=${userId}`)
const userDoc = new DOMParser().parseFromString(await userRes.text(), 'text/html')
const userpicture = userDoc.querySelector(".userpicture");
@@ -21,5 +21,5 @@ export async function getUserIconUrl(userId: number) {
return "";
}
- return (userpicture as HTMLImageElement).src;
+ return (userpicture as HTMLImageElement).src ?? "";
}
diff --git a/src/utils-mock.js b/src/utils-mock.js
index e64b59d..ab87ddc 100644
--- a/src/utils-mock.js
+++ b/src/utils-mock.js
@@ -1,5 +1,11 @@
-import { fetchStudent } from "./student-ENV.js";
-export { DATE_FMT, DATETIME_FMT, dateStringNow, datetimeStringNow } from "../dist/src/utils-common.js";
+import { fetchStudentsMock } from "./mock.ts";
+export {
+ DATE_FMT,
+ DATETIME_FMT,
+ dateStringNow,
+ datetimeStringNow,
+ getUserIconUrl
+} from "./utils-common.ts";
export function isTeacher() {
return !(new URLSearchParams(document.location.search)).has("student");
@@ -11,9 +17,9 @@ export function getFirstAndLastname() {
const params = new URLSearchParams(document.location.search);
if (params.has("firstname"))
- firstname = params.get("firstname")
+ firstname = params.get("firstname");
if (params.has("lastname"))
- lastname = params.get("lastname")
+ lastname = params.get("lastname");
return [firstname, lastname];
}
@@ -23,9 +29,9 @@ export function getName() {
const params = new URLSearchParams(document.location.search);
if (params.has("firstname"))
- name = params.get("firstname")
+ name = params.get("firstname");
if (params.has("lastname"))
- name += params.get("lastname")
+ name += params.get("lastname");
return name;
}
@@ -33,9 +39,16 @@ export function getName() {
export function getUserId() {
const name = getName();
- return fetchStudent(null, name).id;
+ return fetchStudentsMock.filter((s) => s.name === name )[0].id;
}
export function getCourseId() {
return "1337";
}
+
+export function getCourseParticipantsUrl() {
+ console.error("Try to get mocked participations URL");
+ return "";
+}
+
+
diff --git a/src/utils-prod.js b/src/utils-prod.js
index efcd8e3..f7c89e4 100644
--- a/src/utils-prod.js
+++ b/src/utils-prod.js
@@ -1,4 +1,10 @@
-export { DATE_FMT, DATETIME_FMT, dateStringNow, datetimeStringNow, getUserIconUrl } from "./utils-common.ts";
+export {
+ DATE_FMT,
+ DATETIME_FMT,
+ dateStringNow,
+ datetimeStringNow,
+ getUserIconUrl
+} from "./utils-common.ts";
export function isTeacher() {
return Boolean(document.querySelector('.editmode-switch-form'))
@@ -34,3 +40,4 @@ export function getCourseId() {
export function getCourseParticipantsUrl() {
return `https://lernplattform.bycs.de/user/index.php?id=${getCourseId()}&perpage=5000`;
}
+