Files
LehrerApp/docs/Datenmodell.md
T
adminandClaude Sonnet 5 dc2f58c4dc feat: Kompetenzzuordnungen von Freitext-Code auf stabile CompetencyItem.Id umgestellt (TODO 16.5)
Unit/Lesson/ExamTask/ParticipationSession referenzierten Kompetenzen bisher per Freitext-Code,
case-insensitiv gegen den Katalog gematcht - fragil bei Umbenennungen und seit der
Bildungsplan-Versionierung nicht mehr eindeutig (zwei Versionen können denselben Code vergeben).
Neuer Werttyp CompetencyReference (ItemId + Code/Description-Momentaufnahme) löst das: solange die
Id noch im Katalog auflösbar ist, zählt die live Fassung (Rename wirkt sofort), sonst bleibt die
gespeicherte historische Fassung sichtbar statt spurlos zu verschwinden.

Migration (Schema-Version 6) löst bestehende Codes gegen den Katalog auf; die zuvor dreifach
duplizierte Kompetenz-Tag-UI ist jetzt in CompetencyTagGroupBuilder konsolidiert.
add_lesson_competency/remove_lesson_competency nehmen jetzt itemId statt code und validieren gegen
den Katalog (vorher ungeprüft).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 00:41:54 +02:00

12 KiB

Datenmodell und Begriffe

Dieses Dokument beschreibt die fachliche Bedeutung der zentralen Datensätze. Es soll verhindern, dass technisch ähnliche Felder als unterschiedliche Sachverhalte interpretiert oder dieselben Informationen mehrfach gespeichert werden.

Lerngruppe (LearningGroup)

Eine Lerngruppe ist die konkrete Unterrichtsgruppe eines Fachs in genau einem Schuljahr. Sie kann als Klasse oder Kurs organisiert sein.

  • Name: frei gewählte Bezeichnung, zum Beispiel 5b NAT (BEN) oder Mathe G
  • SubjectId: Verweis auf das unterrichtete Fach
  • SchoolYear: Schuljahr dieser konkreten Lerngruppe
  • GradeLevel: Klassen- beziehungsweise Jahrgangsstufe

Der Fachname wird ausschließlich im Subject-Stammdatensatz gepflegt. Die Lerngruppe speichert keine zweite Kopie des Fachnamens.

Gruppenzuordnung (GroupMembership)

Eine Gruppenzuordnung verbindet einen Schüler mit einer Lerngruppe. Sie ist keine Aufnahme oder Einschreibung an der Schule.

  • StudentId: Schüler
  • GroupId: Lerngruppe
  • AddedOn: Tag, an dem die Zuordnung in der App angelegt wurde
  • Period: ganzes Schuljahr, erstes Halbjahr, zweites Halbjahr oder eigener Zeitraum
  • JoinedAt / LeftAt: Grenzen eines eigenen Teilnahmezeitraums
  • Niveau: optionale Niveaudifferenzierung

Das Schuljahr wird über die Lerngruppe ermittelt und deshalb nicht zusätzlich in der Gruppenzuordnung gespeichert. Für eine spätere echte Schulaufnahme wäre ein eigenes Feld wie Student.SchoolEntryDate zu verwenden.

Pro Kombination aus Schüler und Lerngruppe darf es höchstens eine Zuordnung geben.

Bildungsplan-Version (CompetencyCatalogVersion)

Bildet ab, dass für ein Fach mehrere Kompetenzkataloge parallel gültig sein können — typischerweise weil ein neuer Bildungsplan klassenstufenweise hochwächst (z.B. beginnend in Klasse 5, ein Jahrgang pro Schuljahr) und deshalb über mehrere Schuljahre hinweg sowohl der alte als auch der neue Plan gleichzeitig unterrichtet werden.

  • SubjectId: Fach, zu dem die Version gehört
  • Name: frei gewählte Bezeichnung, z.B. Bildungsplan 2016 oder Bildungsplan 2026
  • IsDefault: je Fach höchstens eine Version — Kurse ohne explizite Wahl (siehe LearningGroup.CompetencyCatalogVersionId) nutzen sie. CompetencyCatalogVersionRepository.Save nimmt beim Setzen automatisch die bisherige Standardversion desselben Fachs zurück.

CompetencyDomain.VersionId verweist auf die Version, zu der ein Kompetenzbereich gehört — derselbe Katalog-Slot (Fach + Klassenstufe) kann also mehrfach existieren, einmal je Version. LearningGroup.CompetencyCatalogVersionId (Guid?) wählt je Kurs/Klasse, welche Version gilt. Analog zum Verhältnis von GroupMembership zu LearningGroup.SchoolYear (siehe oben) ist dieses Feld bewusst nullable mit einer Sonderbedeutung: null heißt nicht "keine Version", sondern "die Standardversion des Fachs" — so brauchen bestehende Kurse und Kompetenzkataloge beim Einführen der Versionierung keine Migration außer dem einmaligen Anlegen je einer Standardversion pro Fach (LiteDbContext.MigrateCompetencyCatalogVersions, Schema-Version 5).

CompetencyCatalogVersionRepository.Delete blockiert (statt in den Papierkorb zu verschieben, siehe 14.2), wenn die Version noch von einem Kompetenzbereich oder einem Kurs referenziert wird, oder wenn sie die Standardversion ihres Fachs ist — erst eine andere Version des Fachs muss zum Standard werden, damit nie ein Fach ganz ohne Standardversion dasteht.

Bewusst nicht Teil dieser Versionierung: die Verknüpfung von Kompetenzcodes zu Unterrichtseinheiten, Einzelstunden und Klausuraufgaben (Unit.Competencies, Lesson.Competencies, ExamTask.CompetencyCodes) bleibt Freitext-Code-basiert, nicht auf CompetencyItem.Id umgestellt — ein vorbestehendes, unabhängiges Thema (siehe TODO 16.5).

Gewichtungsschema (GradingScheme)

Legt die prozentuale Gewichtung von Klausuren, Mitarbeit und sonstigen Leistungen für die Zeugnisnote fest (Kapitel 2.3/2.4). Ein Datensatz ist entweder:

  • gruppenspezifisch (GroupId gesetzt, GroupType leer), oder
  • eine Voreinstellung je Gruppentyp (GroupType gesetzt, GroupId leer, in den Einstellungen gepflegt).

Bei der Zeugnisnotenberechnung wird zuerst nach einem gruppenspezifischen Schema gesucht, sonst nach der Voreinstellung des Gruppentyps, sonst greift ein fest codierter Fallback (50/40/10).

Zeugnisnote (ReportGrade)

Eine Zeugnisnote gehört zu genau einem Schüler, einer Lerngruppe und einem Zeitraum (Period: "Gesamtes Schuljahr" / "1. Halbjahr" / "2. Halbjahr" — freier Text, keine Verknüpfung zu MembershipPeriod). CalculatedValue ist das zuletzt berechnete Ergebnis, OverrideValue überschreibt es bei pädagogischem Ermessen und erfordert OverrideReason. Nach dem Festschreiben (IsLocked) wird der Datensatz bei einer Neuberechnung nicht mehr verändert.

Bewusst gespeicherte Momentaufnahmen

Einige berechnete Werte bleiben absichtlich gespeichert:

  • ExamResult.TotalPoints und ExamResult.Grade halten das zuletzt berechnete Klausurergebnis fest.
  • Exam.GradingKey hält den für die konkrete Klausur verwendeten Notenschlüssel fest und ist unabhängig von später geänderten Vorlagen.
  • Exam.Tasks hält die Aufgabenstruktur der konkreten Klausur fest.
  • CompetencyReference (Unit.Competencies, Lesson.Competencies, ExamTask.Competencies, ParticipationSession.Competencies) verweist über ItemId auf ein CompetencyItem, hält aber zusätzlich Code/Description zum Zuordnungszeitpunkt fest (TODO 16.5, abgelöst von einer reinen Freitext-Code-Zuordnung). Solange ItemId noch im Katalog auflösbar ist, zeigt die Anwendung die live Fassung (ein Rename im Katalog wirkt sich überall sofort aus); wird das Item gelöscht, bleibt die hier gespeicherte historische Fassung lesbar statt spurlos zu verschwinden. ItemId == Guid.Empty markiert eine nie aufgelöste Referenz (z.B. aus einem plattformübergreifenden PlanningExchangeService-Import, der bewusst keine installationsfremden IDs überträgt) — Code/Description sind dann die einzige Information. CompetencyRating.ItemId (Mitarbeit, je Schüler) referenziert ebenso per Id, aber ohne eigenen Snapshot — die Identität kommt aus der Zuordnungsliste der zugehörigen ParticipationSession, die bereits eine Momentaufnahme trägt.

Änderungen an Aufgaben oder Notenschlüssel müssen die betroffenen Ergebnisse kontrolliert neu berechnen. Diese Werte sind daher fachliche Momentaufnahmen und nicht bloß unkontrollierte Kopien.

Bewusste Denormalisierung

Lesson.GroupId bleibt zusätzlich zu Lesson.UnitId gespeichert. Dadurch kann der häufige Kalenderzugriff auf alle Stunden einer Lerngruppe direkt indiziert werden. Beim späteren Ausbau der Unterrichtsplanung muss sichergestellt werden, dass Lesson.GroupId mit der Lerngruppe der zugehörigen Einheit übereinstimmt.

Umgesetzt in PlanningTabViewModel (Kapitel 4.1/4.2): Beim Kopieren einer Einheit als Vorlage in eine andere Gruppe (4.1.4) wird Lesson.GroupId auf jeder neu erzeugten Stunde explizit auf die Zielgruppe gesetzt, nicht von der Quell-Lesson übernommen. Beim Verschieben einer Stunde inkl. Nachrücken der Folgestunden (4.2.4) ändert sich ausschließlich Lesson.Date — UnitId/GroupId bleiben unangetastet.

Stundenverlaufsplan (Lesson.Phases)

Eine Stunde hat statt eines einzelnen Phase-Textfelds plus Methoden-/Materialien-Listen eine geordnete Liste Lesson.Phases: List<LessonPhaseStep> (Name, Dauer in Minuten, Tätigkeit, Material, Kurzsymbol Shorthand). DurationMinutes ist die primäre, vom Nutzer gepflegte Größe; die im Editor angezeigte Uhrzeit je Phase ist rein abgeleitet (kumulierte Dauer ab Lesson.StartTime, sofern gesetzt) und wird nirgends persistiert — es gibt also keine Konsistenzpflicht zwischen gespeicherter Dauer und einer gespeicherten Uhrzeit, weil Letztere gar nicht gespeichert wird.

Shorthand ist bewusst ein einzelnes Freitextfeld statt einer erzwungenen Von/Nach-Struktur: in der Praxis ist es mal ein Materialfluss-Pfeil ("AB001->S"), mal nur eine Sozialform ohne Pfeil ("Plenum", "LDE"). Der Kürzel-Katalog (ShorthandCode, Einstellungen) und bereits in anderen Stunden verwendete Werte dienen nur als Autovervollständigungs-Vorschläge, erzwingen aber keine Struktur.

AlternativePathId (Guid?, null = Hauptweg) verweist auf einen Eintrag im eigenständigen Katalog AlternativeLessonPath (Name + optionale Description, verwaltet über IAlternativeLessonPathRepository) — anders als Shorthand bewusst kein Freitext direkt am Phasen-Datensatz: die Zuordnung muss zuverlässig gruppierbar sein (LessonViewerViewModel gruppiert danach für die Anzeige) und soll eine wiederverwendbare Erklärung tragen können ("wann nimmt man diesen Weg"), was ein reines Freitextfeld nicht sauber leisten kann. Kein Fork-Punkt-Bezug zum Hauptweg: jede Gruppe wird beim Anzeigen unabhängig ab Lesson.StartTime durchgerechnet, nicht ab einer gemeinsamen Verzweigungsstelle.

LiteDbContext-Schema-Version 4 führt drei aufeinanderfolgende, unabhängig versionierte Migrationsschritte für dieses Feld bzw. seine Vorstufen:

  • v1→v2 (MigrateLessonPhases()): führt bereits gespeicherte alte Stunden (einzelnes Phase-Feld, Methods/Materials-Listen) verlustfrei in eine einzige synthetisierte LessonPhaseStep-Zeile zusammen (Name = altes Phase, Activity = alte Methods verbunden, Material = alte Materials verbunden, DurationMinutes = 0 da unbekannt).
  • v2→v3 (MigrateLessonShorthand()): das ursprünglich als Von/Nach-Paar (ShorthandFrom/ ShorthandTo) modellierte Kurzsymbol wird auf das einzelne Freitextfeld zusammengeführt (beide gesetzt → "Von->Nach", nur eines gesetzt → dieser Einzelwert).
  • v3→v4 (MigrateLessonAlternativePaths()): das ursprünglich als Freitext (AlternativePath, string) modellierte Kennzeichen für den alternativen Ablauf wird pro distinktem Namen zu einem AlternativeLessonPath-Katalogeintrag zusammengeführt (derselbe Name über mehrere Stunden hinweg referenziert denselben, wiederverwendeten Eintrag) und die Phase auf AlternativePathId umgestellt.

Alle drei Migrationen lesen dafür die rohe BsonDocument-Repräsentation der lessons-Collection statt der typisierten Lessons-Collection — nach jeder Modelländerung kennt die typisierte Lesson-Klasse die alten Feldnamen nicht mehr, ein Zugriff darüber hätte sie beim Deserialisieren bereits verworfen, bevor sie gelesen werden können.

LessonPhaseStep.MaterialPrompt (4.5.36, Nachtrag zu 4.5.20) ist kein von Hand gepflegtes Feld: AiPlanningService.ApplyResponse befüllt es automatisch mit dem vollständigen, lokal erzeugten Prompt (BuildMaterialPrompt), sobald die KI beim letzten "Übernehmen" für diese Phase einen Medienvorschlag gemacht hatte — ursprünglich (4.5.20) bewusst NICHT persistiert, weil der Vorschlag nur für die Review-Anzeige gedacht war; Nutzer-Feedback nach erstem Einsatz war, dass der Prompt auch nach dem Übernehmen noch abrufbar bleiben soll. Kein neues "Material"-Domänenmodell dafür (das gibt es weiterhin nicht, siehe 4.5.26) — der Prompt hängt einfach als zusätzliches Freitextfeld an der bereits bestehenden LessonPhaseStep-Zeile.

Eindeutige Schlüssel

Die Datenbank schützt folgende Kombinationen mit eindeutigen Indizes:

  • Gruppenzuordnung: StudentId + GroupId
  • Klausurergebnis: ExamId + StudentId
  • Mitarbeitseintrag: SessionId + StudentId
  • Zeugnisnote: StudentId + GroupId + Period
  • Fach: normalisierter Fachname

Altdaten werden beim Öffnen der Datenbank automatisch migriert. Die Migration verknüpft bisherige Fachtexte mit den Fachstammdaten, benennt die bisherige enrollments-Collection in group_memberships um und entfernt daraus das doppelt gespeicherte Schuljahr.