CI / build-and-test (push) Canceled after 0s
Ein Zwischenstand ist ein Durchgang aus Stichtag, Anlass und Schülerauswahl (ganze Gruppe, Teilgruppe oder Einzelfall, mit Schnellfilter für gefährdete Schüler). Der Zeugnisnoten-Assistent geht die gewählten Schüler im Zwischenstand-Modus durch und notiert vorläufige schriftliche, mündliche und Gesamtnote samt Notiz. Gespeichert wird das als eigenes Modell InterimGrade, getrennt von der Zeugnisnote. Die Berechnung endet am Stichtag und rechnet den laufenden Mitarbeits-Abschnitt mit ein. Zwischenstände erscheinen im Assistenten als Liste und Marker im Verlauf sowie als graue Spalte in der Notenübersicht (nicht in "Gesamt", nicht im PDF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
244 lines
15 KiB
Markdown
244 lines
15 KiB
Markdown
# 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.
|
|
|
|
Die beiden Halbjahre werden jeweils aus den Einzelleistungen ihres Zeitraums berechnet und sind
|
|
voneinander unabhängig. "Gesamtes Schuljahr" wird dagegen aus den Halbjahresnoten verrechnet
|
|
(gleich gewichtet), nicht aus allen Einzelleistungen des Jahres.
|
|
|
|
Vier Felder sind nur am Jahres-Datensatz einer 10. Klasse belegt und halten Prüfungsnoten fest,
|
|
soweit die Prüfung in diesem Fach abgelegt wurde:
|
|
|
|
- `WrittenExamGrade` / `OralExamGrade`: schriftliche bzw. mündliche Abschlussprüfung. Ein Fach
|
|
hat höchstens eine von beiden. Die Prüfungsleistung geht mit einem Drittel in die Endnote ein,
|
|
die unterrichtliche Leistung (beide Halbjahre) mit zwei Dritteln — § 14 Abs. 3 der Verordnung
|
|
über die Sekundarstufe I der Oberschule.
|
|
- `AdditionalOralExamGrade`: zusätzliche mündliche Prüfung ("Nachprüfung"). Im schriftlichen
|
|
Prüfungsfach wird sie 1:1 mit der schriftlichen Prüfung zur Prüfungsleistung verrechnet
|
|
(§ 13 Abs. 2 PrüfV Sek. I). In einem Fach ohne Abschlussprüfung ist sie selbst die
|
|
Prüfungsleistung, also 2:1 (§ 13 Abs. 3 PrüfV Sek. I).
|
|
- `ProjectExamGrade`: Projektprüfung, zählt wie ein weiteres Halbjahr. Das ist die Praxis der
|
|
Schule; der Wortlaut der Verordnung (§ 4a Abs. 3: fließt in die Halbjahresnote des
|
|
zugeordneten Fachs ein) wird bewusst nicht nachgebildet.
|
|
|
|
Die Rundungsvorgaben der Verordnungen sind bewusst nicht umgesetzt: Noten sind
|
|
Leistungsbewertungen, die Entscheidung zwischen zwei Noten ist pädagogisch. Eine Prüfungsleistung
|
|
zwischen zwei Noten geht ungerundet in die Rechnung ein; für die Endnote gilt die von der
|
|
Lehrkraft gewählte Rundung, und der Assistent weist darauf hin, wenn der Wert genau zwischen
|
|
zwei Noten liegt. Stand der Verordnungen: Oberschulverordnung in der Fassung von Juni 2025.
|
|
|
|
## Zwischenstand (`InterimGrade`)
|
|
|
|
Ein Zwischenstand ist eine vorläufige Note zu einem Stichtag (`Date`), wie sie zu einem Anlass
|
|
(`Occasion`, freier Text wie "Sprechtag" oder "Elternanfrage: Frau Muster") genannt wurde. Er ist
|
|
bewusst kein Zustand der Zeugnisnote, sondern ein eigener Datensatz: es kann mehrere je Halbjahr
|
|
geben, und keiner beeinflusst `ReportGrade`. Zwischenstände einer Lerngruppe mit gleichem `Date`
|
|
und `Occasion` bilden zusammen einen Durchgang — es gibt dafür keinen eigenen Datensatz.
|
|
|
|
`WrittenValue`, `OralValue` und `OverallValue` sind die tatsächlich genannten Noten, nicht der
|
|
berechnete Vorschlag; die Lehrkraft kann sie beim Notieren anpassen. `OralValue` meint die
|
|
Mitarbeit inklusive des am Stichtag laufenden, noch nicht abgeschlossenen Abschnitts.
|
|
|
|
## 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.
|
|
- `ExamTask.Variant` ist bei gemeinsamen Aufgaben null, sonst `A` oder `B`.
|
|
`ExamResult.Variant` wählt die Fassung je Schüler. `ExamTask.IsBonus` nimmt die
|
|
Maximalpunkte aus dem Grundwert des Notenschlüssels; erreichte Bonuspunkte zählen
|
|
weiterhin zum Ergebnis.
|
|
- `Exam.RepeatOfExamId` verweist auf die ursprüngliche Arbeit derselben Gruppe.
|
|
`RepeatGradeRule` legt fest, ob die bessere, die wiederholte oder die erste Note
|
|
zählt. Beide Ergebnisse bleiben sichtbar, gehen aber als ein Wert in die
|
|
berechneten Durchschnitts- und Zeugnisnoten ein.
|
|
- `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.
|