aboutsummaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2026-07-12 09:33:58 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2026-07-12 09:33:58 +0200
commit1a291fb37fdf0e5a1b2a54825e5af7c5e986f00f (patch)
tree3349f89ce4412b63c403411045185c80ba50a6bf /js
parentb39d7dd42c75b096aa88ffc07ae78f1445c8e588 (diff)
downloadbycs-sol-1a291fb37fdf0e5a1b2a54825e5af7c5e986f00f.tar.gz
bycs-sol-1a291fb37fdf0e5a1b2a54825e5af7c5e986f00f.zip
implment observation period handling
Diffstat (limited to 'js')
-rw-r--r--js/config.js7
-rw-r--r--js/database.js10
-rw-r--r--js/graduation-common.js161
-rw-r--r--js/mock.js6
-rw-r--r--js/student-common.js22
5 files changed, 171 insertions, 35 deletions
diff --git a/js/config.js b/js/config.js
index f07c5ec..5fc456c 100644
--- a/js/config.js
+++ b/js/config.js
@@ -3,6 +3,12 @@ function getCourseId() {
return courseId;
}
+// TODO(muhq): retrieve when loading activities/DBs
+const cockpidId = 93017047;
+function getCockpitId() {
+ return cockpidId;
+}
+
function getCourseParticipantsUrl() {
return `https://lernplattform.bycs.de/user/index.php?id=${courseId}`;
}
@@ -17,6 +23,7 @@ async function getUserIconUrl(userId) {
export {
getCourseId,
+ getCockpitId,
getUserIconUrl,
getCourseParticipantsUrl
};
diff --git a/js/database.js b/js/database.js
index 7fd3d47..73c2d17 100644
--- a/js/database.js
+++ b/js/database.js
@@ -29,7 +29,7 @@ async function populateStudentDbs() {
const courseDoc = new DOMParser().parseFromString(await courseRes.text(), 'text/html');
let dbs = new Map();
- for (const link of courseDoc.querySelectorAll("div.activityname > a.aalink")) {
+ for (const link of courseDoc.querySelectorAll("div.activityname a.aalink")) {
const match = link.textContent.match(/(\d+)(Shadow|Public|Submit)/);
if (!match) continue;
const studentId = match[1];
@@ -82,7 +82,13 @@ async function loadFromDatabaseActivity(id, params) {
let entry = {};
row.querySelectorAll('.db-cell').forEach(field => {
const key = field.dataset.key;
- const value = field.innerText.trim();
+ let value;
+ // Moodle includes the full date in the title attribute of the included span
+ if (key === "date") {
+ value = field.querySelector("span").title;
+ } else {
+ value = field.innerText.trim();
+ }
entry[key] = value;
});
data.push(entry);
diff --git a/js/graduation-common.js b/js/graduation-common.js
index 3ab1fdd..1c68f38 100644
--- a/js/graduation-common.js
+++ b/js/graduation-common.js
@@ -22,11 +22,15 @@ class GraduationChange {
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,
@@ -34,9 +38,25 @@ class GraduationChange {
}
static fromDBEntry(e) {
- const {date, coach, json} = e;
+ const {coach, json} = e;
const change = JSON.parse(json);
- return new GraduationChange(new Date(date), change.change, change.reason, coach, change.context);
+ 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) {
@@ -53,14 +73,16 @@ class GraduationChange {
}
class Graduation {
- constructor(student, green, orange, red, level, stickers, observation, changes) {
+ 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;
@@ -71,41 +93,66 @@ class Graduation {
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":
- observationPeriod = new ObservationPeriod(change.date, null, []);
+ console.assert(openObservationRequest, "No request for observation start");
+ observationPeriod = new ObservationPeriod(change.date);
+ openObservationRequest = null;
break;
+
case "ObservationEnd":
- observationPeriod.end = change.date;
+ 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":
- const observation = Observation.fromContext(change.context);
- if (observationPeriod) {
- observationPeriod.addObservation(observation);
- } else {
- alert(`No active observation period`);
- }
+ 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, observationPeriod, changes);
+ return new Graduation(student, green, orange, red, level,
+ stickers,
+ openObservationRequest, observationPeriod, pastObservations,
+ changes);
}
addChange(change) {
@@ -120,7 +167,36 @@ class Graduation {
* Request observation
*/
async requestObservation() {
- await addEntryToStudentDatabase(this.student, "submit", {change: "ObservationRequest"});
+ 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));
}
/**
@@ -147,10 +223,14 @@ class Graduation {
}
class ObservationPeriod {
- constructor(start, end, observations) {
+ constructor(start) {
this.start = start;
- this.end = end;
- this.observations = observations;
+ this.observations = [];
+ }
+
+ finishPeriod(success) {
+ this.end = new Date();
+ this.success = success;
}
ended() {
@@ -158,18 +238,28 @@ class ObservationPeriod {
}
ready() {
- // TODO(muhq): add time contraint
- return this.observations > 10;
+ 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({"yes":0, "no":0});
+ 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])
- sums[i].yes++;
- else
- sums[i].no++;
+ if (observation.status[i]) {
+ if (observation.status[i] === "yes")
+ sums[i].yes++;
+ else
+ sums[i].no++;
+ }
}
}
return sums;
@@ -192,10 +282,14 @@ const OBSERVATION_TYPES = [
class Observation {
constructor(date, status) {
- console.assert(status.length != OBSERVATION_TYPES.length, "invalid observation 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))
+ }
}
async function fetchGraduationChangesFromDb(student, type) {
@@ -206,15 +300,23 @@ async function fetchGraduationChangesFromDb(student, type) {
const gradChange = GraduationChange.fromDBEntry(e);
changes.push(gradChange);
}
- return changes;
+ return changes.flat();
}
async function fetchGraduationChanges(student) {
- return fetchGraduationChangesFromDb(student, "shadow");
+ let changes = await Promise.all([
+ fetchGraduationChangesFromDb(student, "shadow"),
+ fetchGraduationChangesFromDb(student, "submit"),
+ ]);
+ return changes.flat();
}
async function fetchPublicGraduationChanges(student) {
- return fetchGraduationChangesFromDb(student, "public");
+ let changes = await Promise.all([
+ fetchGraduationChangesFromDb(student, "public"),
+ fetchGraduationChangesFromDb(student, "submit"),
+ ]);
+ return changes.flat();
}
async function fetchGraduation(student) {
@@ -248,8 +350,9 @@ async function submitGraduationChange(student, change) {
}
async function submitObservation(student, observation) {
- student.graduation.addObservation(observation);
- await addEntryToStudentDatabase(student, "shadow", JSON.stringify(observation));
+ console.assert(student.graduation.observation, "no active observation");
+ student.graduation.observation.addObservation(observation);
+ await submitGraduationChange(student, GraduationChange.newObservationChange(observation));
}
export {
diff --git a/js/mock.js b/js/mock.js
index 4bcd202..91b1525 100644
--- a/js/mock.js
+++ b/js/mock.js
@@ -83,13 +83,13 @@ function fetchPublicGraduationMock(s) {
}
function submitGraduationChangeMock(s, c) {
- assert(s.graduation, "no graduation")
+ console.assert(s.graduation, "no graduation")
s.graduation.addChange(c);
}
function submitObservationMock(s, o) {
- assert(s.graduation, "no graduation")
- assert(s.graduation.observation, "no observation period")
+ console.assert(s.graduation, "no graduation")
+ console.assert(s.graduation.observation, "no observation period")
s.graduation.observation.addObservation(o);
}
diff --git a/js/student-common.js b/js/student-common.js
index c89b818..778ff8d 100644
--- a/js/student-common.js
+++ b/js/student-common.js
@@ -1,4 +1,4 @@
-import { getUserIconUrl, getCourseParticipantsUrl } from "./config.js";
+import { getCourseId, getUserIconUrl, getCourseParticipantsUrl } from "./config.js";
import { fetchGraduation } from "./graduation-common.js";
/**
* Class to represent Students.
@@ -27,6 +27,26 @@ class Student {
}
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);
+ }
}
async function fetchStudent(idFilter, nameFilter, groupFilter) {