diff options
Diffstat (limited to 'src/graduation-common.js')
| -rw-r--r-- | src/graduation-common.js | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/src/graduation-common.js b/src/graduation-common.js new file mode 100644 index 0000000..0c28465 --- /dev/null +++ b/src/graduation-common.js @@ -0,0 +1,396 @@ +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, +}; |
