fix: WebUntis-Vertretungen für kombinierte Gruppen und Karteileichen
- ClassToken-Vergleich normalisiert jetzt symmetrisch (beide Seiten) und ExtractClassTokens akzeptiert ";" und "," als Trennzeichen zwischen Klassen, da nie an einem echten kombinierten Termin verifiziert wurde, welches WebUntis tatsächlich verwendet. - Abweichende Vertretungen tragen jetzt den genauen Vergleichsgrund (Fach/Klasse, roh vs. erwartet) in ihrer Beschreibung. - Automatisch erzeugte Vertretungen, die bei einem späteren Poll nicht mehr abweichen, werden jetzt aktiv wieder entfernt statt als Karteileichen stehen zu bleiben (UntisDiffResult.SubstitutionExternalIdsToDelete). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,15 @@ public sealed class UntisDiffResult
|
||||
/// Verschwundene, bereits als Ausfall verarbeitete Zeilen — werden aus dem Snapshot entfernt,
|
||||
/// damit derselbe Ausfall nicht bei jedem weiteren Poll erneut erkannt wird.
|
||||
public List<Guid> SnapshotIdsToDelete { get; init; } = [];
|
||||
/// ExternalIds automatisch erzeugter SubstitutionEntry-Zeilen, die NICHT (mehr) gebraucht
|
||||
/// werden — z.B. weil eine früher erkannte Abweichung sich (durch einen Bugfix oder eine
|
||||
/// erneute Zuordnung) als nicht mehr abweichend herausstellt, oder eine zuvor als fehlend
|
||||
/// gemeldete Stunde jetzt wieder im Feed auftaucht. Nutzer-Feedback: "Es ist immer noch so"
|
||||
/// (nachdem der eigentliche Vergleichsfehler bereits behoben war) — Ursache: einmal erzeugte
|
||||
/// automatische Einträge wurden nie wieder entfernt, selbst wenn der Vergleich sie beim
|
||||
/// nächsten Poll nicht mehr als Abweichung einstufte; sie blieben als Karteileichen stehen.
|
||||
/// Existiert kein Eintrag mit dieser ExternalId, ist das Löschen ein no-op (siehe UntisSyncService).
|
||||
public List<string> SubstitutionExternalIdsToDelete { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -46,6 +55,7 @@ public class UntisDiffService
|
||||
|
||||
var substitutions = new List<SubstitutionEntry>();
|
||||
var snapshotToSave = new List<UntisSnapshotEntry>();
|
||||
var externalIdsToDelete = new List<string>();
|
||||
var newEventsByUid = newEvents.ToDictionary(e => e.Uid);
|
||||
|
||||
foreach (var evt in newEvents)
|
||||
@@ -70,8 +80,10 @@ public class UntisDiffService
|
||||
|
||||
if (mapping.Kind == SubstitutionKind.Lesson)
|
||||
{
|
||||
if (HasDeviated(evt, mapping))
|
||||
substitutions.Add(BuildChangedLesson(evt, mapping));
|
||||
if (DeviationReason(evt, mapping) is { } reason)
|
||||
substitutions.Add(BuildChangedLesson(evt, mapping, reason));
|
||||
else
|
||||
externalIdsToDelete.Add(evt.Uid); // war ggf. vorher fälschlich als Vertretung erkannt
|
||||
}
|
||||
else if (!regularDutyKeys.Contains((mapping.Weekday, mapping.AfterPeriod!.Value)))
|
||||
{
|
||||
@@ -81,6 +93,10 @@ public class UntisDiffService
|
||||
// davon.
|
||||
substitutions.Add(BuildSupervisionNote(evt.Date, mapping, evt.Uid, "Zusätzliche Aufsicht laut WebUntis."));
|
||||
}
|
||||
else
|
||||
{
|
||||
externalIdsToDelete.Add(evt.Uid); // jetzt als reguläre Aufsicht erkannt, keine Meldung mehr nötig
|
||||
}
|
||||
}
|
||||
|
||||
// Aufsichten: weiterhin rein reaktiv gegen den letzten Snapshot (kein fester wöchentlicher
|
||||
@@ -123,7 +139,13 @@ public class UntisDiffService
|
||||
{
|
||||
if (date.DayOfWeek != mapping.Weekday) continue;
|
||||
if (freeDateSet.Contains(date)) continue;
|
||||
if (eventDates.Contains((date, mapping.StartTime))) continue;
|
||||
if (eventDates.Contains((date, mapping.StartTime)))
|
||||
{
|
||||
// War evtl. an einem früheren Poll (noch) als fehlend gemeldet - jetzt
|
||||
// wieder im Feed vorhanden, die alte Meldung räumt sich damit selbst ab.
|
||||
externalIdsToDelete.Add(BuildMissingExternalId(date, mapping));
|
||||
continue;
|
||||
}
|
||||
|
||||
substitutions.Add(BuildCancelledLesson(date, mapping, BuildMissingExternalId(date, mapping)));
|
||||
}
|
||||
@@ -133,7 +155,7 @@ public class UntisDiffService
|
||||
return new UntisDiffResult
|
||||
{
|
||||
SubstitutionsToSave = substitutions, SnapshotToSave = snapshotToSave,
|
||||
SnapshotIdsToDelete = snapshotIdsToDelete,
|
||||
SnapshotIdsToDelete = snapshotIdsToDelete, SubstitutionExternalIdsToDelete = externalIdsToDelete,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -141,12 +163,31 @@ public class UntisDiffService
|
||||
// manuelle Verknüpfung ist aber jetzt scheinbar vergessen" — Ursache: ClassToken wird beim
|
||||
// Bestätigen kompakt ohne Leerzeichen gespeichert ("10a;10b;10c"), WebUntis trennt die
|
||||
// Klassen in DESCRIPTION aber mit "; " (Semikolon + Leerzeichen), z.B. "10a; 10b; 10c;
|
||||
// Gastro HED" — ein reiner Teilstring-Vergleich schlug für JEDE kombinierte/differenzierte
|
||||
// Gruppe (mehr als ein Klassen-Token) strukturell fehl, unabhängig von der tatsächlich
|
||||
// gewählten Gruppe. Fix: beide Seiten vor dem Vergleich von Leerzeichen befreien (gleiches
|
||||
// Prinzip wie UntisMatchingService.Normalize für den Gruppennamen-Abgleich).
|
||||
private static bool HasDeviated(UntisIcsEvent evt, UntisSlotMapping mapping) =>
|
||||
evt.Summary != mapping.Summary || !RemoveWhitespace(evt.Description).Contains(mapping.ClassToken);
|
||||
// Gastro HED". Ein erster Fix entfernte Leerzeichen nur auf der evt.Description-Seite — blieb
|
||||
// aber weiterhin falsch, wenn mapping.ClassToken selbst ein eingebettetes Leerzeichen enthält
|
||||
// (z.B. wenn die reale DESCRIPTION mehrere Klassen NICHT mit ";", sondern mit "," trennt:
|
||||
// ExtractClassTokens fand dann kein ";" und behandelte "10a, 10b, 10c" als EIN Token mit
|
||||
// Leerzeichen/Kommas statt drei getrennte - ein Vergleich gegen die leerzeichenbereinigte
|
||||
// Description konnte dieses Token dann NIE mehr finden, unabhängig von der Auswahl im Dialog).
|
||||
// Fix: beide Seiten symmetrisch von Leerzeichen befreien (gleiches Prinzip wie
|
||||
// UntisMatchingService.Normalize für den Gruppennamen-Abgleich), plus ExtractClassTokens
|
||||
// akzeptiert jetzt sowohl ";" als auch "," als Trennzeichen zwischen Klassen.
|
||||
//
|
||||
// Trotzdem meldet der Nutzer weiterhin falsche Vertretungen für genau diese Art Muster
|
||||
// ("Immer die Kurse mit Lerngruppen, die aus mehreren Klassen zusammengesetzt sind") - der
|
||||
// genaue Grund lässt sich ohne Einblick in die echten DESCRIPTION-Werte aus seinem Feed nicht
|
||||
// mehr blind erraten. Statt eines weiteren ungetesteten Rateversuchs schreibt eine abweichende
|
||||
// Vertretung deshalb jetzt den GENAUEN Vergleich (roher evt.Summary/evt.Description gegen den
|
||||
// gespeicherten mapping.Summary/ClassToken) in ihre eigene Beschreibung - der Nutzer kann die
|
||||
// Ursache dann direkt im Stundenplan ablesen, ohne die iCal-URL erneut teilen zu müssen.
|
||||
private static string? DeviationReason(UntisIcsEvent evt, UntisSlotMapping mapping)
|
||||
{
|
||||
if (evt.Summary != mapping.Summary)
|
||||
return $"Fach weicht ab: WebUntis meldet „{evt.Summary ?? "(leer)"}“, erwartet war „{mapping.Summary ?? "(leer)"}“.";
|
||||
if (!RemoveWhitespace(evt.Description).Contains(RemoveWhitespace(mapping.ClassToken)))
|
||||
return $"Klasse weicht ab: WebUntis-Beschreibung „{evt.Description}“ enthält nicht den erwarteten Klassen-Token „{mapping.ClassToken}“.";
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string RemoveWhitespace(string value) =>
|
||||
new(value.Where(c => !char.IsWhiteSpace(c)).ToArray());
|
||||
@@ -157,11 +198,11 @@ public class UntisDiffService
|
||||
Description = "Automatisch über WebUntis-Abgleich erkannt.", ExternalId = externalId,
|
||||
};
|
||||
|
||||
private static SubstitutionEntry BuildChangedLesson(UntisIcsEvent evt, UntisSlotMapping mapping) => new()
|
||||
private static SubstitutionEntry BuildChangedLesson(UntisIcsEvent evt, UntisSlotMapping mapping, string reason) => new()
|
||||
{
|
||||
Date = evt.Date, Kind = SubstitutionKind.Lesson, PeriodNumber = mapping.PeriodNumber,
|
||||
GroupId = mapping.GroupId, GroupLabel = mapping.ClassToken,
|
||||
Description = $"WebUntis: {evt.Summary ?? "?"} · {evt.Location} (regulär: {mapping.Summary ?? "?"})",
|
||||
Description = $"WebUntis: {evt.Summary ?? "?"} · {evt.Location} (regulär: {mapping.Summary ?? "?"}) — {reason}",
|
||||
ExternalId = evt.Uid,
|
||||
};
|
||||
|
||||
|
||||
@@ -148,13 +148,19 @@ public class UntisMatchingService
|
||||
}
|
||||
|
||||
// "10a; 10b; 10c; Gastro HED" -> ["10a", "10b", "10c", "Gastro"] (Lehrkraft-Token entfernt).
|
||||
// Trennzeichen zwischen mehreren Klassen wurde nur an einem einzelnen echten Beispiel mit EINER
|
||||
// Klasse verifiziert (siehe Planungsdokument) - für kombinierte/differenzierte Gruppen ist nicht
|
||||
// sicher belegt, ob WebUntis ";" oder "," verwendet, deshalb werden beide akzeptiert statt sich
|
||||
// auf eine Annahme festzulegen.
|
||||
private static readonly char[] ClassTokenSeparators = [';', ','];
|
||||
|
||||
private static List<string> ExtractClassTokens(string description, string? teacherToken)
|
||||
{
|
||||
var withoutTeacher = teacherToken is not null && description.EndsWith(teacherToken)
|
||||
? description[..^teacherToken.Length].TrimEnd()
|
||||
: description;
|
||||
if (string.IsNullOrWhiteSpace(withoutTeacher)) return [];
|
||||
return withoutTeacher.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
return withoutTeacher.Split(ClassTokenSeparators, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
|
||||
}
|
||||
|
||||
private static int? ResolvePeriodNumber(TimeOnly startTime, PeriodScheduleService periodSchedule)
|
||||
|
||||
@@ -85,6 +85,40 @@ public sealed class UntisSyncServiceTests
|
||||
Assert.Single(substitutions.GetAll());
|
||||
}
|
||||
|
||||
// ── Regression: "Es ist immer noch so [...] Immer die Kurse mit Lerngruppen, die aus mehreren
|
||||
// Klassen zusammengesetzt sind" — nachdem der eigentliche Vergleichsfehler (ClassToken/
|
||||
// Trennzeichen) behoben war, blieben bereits erzeugte falsche Vertretungen trotzdem stehen:
|
||||
// ProcessIcsText schrieb bislang nur NEUE/weiterhin abweichende Kandidaten, räumte aber nie
|
||||
// eine zuvor automatisch erzeugte Zeile ab, sobald der nächste Poll gar keine Abweichung mehr
|
||||
// fand. Der Nutzer sah dadurch dieselbe (jetzt stale) Karteileiche, egal wie oft er neu
|
||||
// zuordnete oder pollte.
|
||||
|
||||
[Fact]
|
||||
public void ProcessIcsText_AbweichungLoestSichSpaeterAuf_EntferntDieZuvorErzeugteVertretung()
|
||||
{
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Monday, StartTime = new TimeOnly(7, 50), Summary = "SOL",
|
||||
ClassToken = "10c", GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
|
||||
});
|
||||
var substitutions = new FakeSubstitutionEntries();
|
||||
var service = BuildService(mappings: mappings, substitutions: substitutions);
|
||||
|
||||
// Poll 1: Fach weicht (noch) ab - Vertretung wird angelegt.
|
||||
var first = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "NAT", "10c HED"));
|
||||
Assert.Equal(1, first.SubstitutionCount);
|
||||
Assert.Single(substitutions.GetAll());
|
||||
|
||||
// Poll 2: derselbe Termin weicht jetzt nicht mehr ab (z.B. weil sich die WebUntis-Daten
|
||||
// geändert haben oder ein Vergleichsfehler zwischenzeitlich behoben wurde) - die zuvor
|
||||
// erzeugte Vertretung darf nicht als Karteileiche stehen bleiben.
|
||||
var second = service.ProcessIcsText(BuildIcs("1", "20260817T075000", "SOL", "10c HED"));
|
||||
|
||||
Assert.Equal(0, second.SubstitutionCount);
|
||||
Assert.Empty(substitutions.GetAll());
|
||||
}
|
||||
|
||||
// ── Regression: "Am 27.08. fällt eine Stunde NAT in der 8c aus. Dieser Ausfall steht nicht
|
||||
// im Plan [...] die Klasse ist dort weg" — end-to-end über ProcessIcsText, inkl. der neuen
|
||||
// Ferien-Ausschluss-Verdrahtung (ISchoolHolidayRepository -> UntisDiffService.freeDates).
|
||||
|
||||
@@ -124,6 +124,14 @@ public class UntisSyncService : IDisposable
|
||||
if (existing is not null) candidate.Id = existing.Id;
|
||||
_substitutions.Save(candidate);
|
||||
}
|
||||
// Zuvor automatisch erzeugte Einträge, die jetzt nicht (mehr) gebraucht werden (siehe
|
||||
// UntisDiffResult.SubstitutionExternalIdsToDelete) - existiert keiner mit dieser
|
||||
// ExternalId, ist das ein no-op.
|
||||
foreach (var externalId in diffResult.SubstitutionExternalIdsToDelete)
|
||||
{
|
||||
var stale = _substitutions.GetByExternalId(externalId);
|
||||
if (stale is not null) _substitutions.Delete(stale.Id);
|
||||
}
|
||||
foreach (var snapshot in diffResult.SnapshotToSave) _snapshots.Save(snapshot);
|
||||
foreach (var id in diffResult.SnapshotIdsToDelete) _snapshots.Delete(id);
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ public sealed class UntisDiffServiceTests
|
||||
Assert.Equal(SubstitutionKind.Lesson, entry.Kind);
|
||||
Assert.Equal("1", entry.ExternalId);
|
||||
Assert.Equal(1, entry.PeriodNumber);
|
||||
// Diagnose direkt in der Beschreibung (siehe DeviationReason) - damit der Nutzer im
|
||||
// Stundenplan selbst ablesen kann, welcher Vergleich genau fehlgeschlagen ist, ohne
|
||||
// Rohdaten aus dem Feed teilen zu müssen.
|
||||
Assert.Contains("Fach weicht ab", entry.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -64,7 +68,8 @@ public sealed class UntisDiffServiceTests
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Single(result.SubstitutionsToSave);
|
||||
var entry = Assert.Single(result.SubstitutionsToSave);
|
||||
Assert.Contains("Klasse weicht ab", entry.Description);
|
||||
}
|
||||
|
||||
// ── Regression: "Der Mathematik E-Kurs [...] ist ein Kurs aus den Klassen 10a, 10b und 10c
|
||||
@@ -105,6 +110,85 @@ public sealed class UntisDiffServiceTests
|
||||
Assert.Single(result.SubstitutionsToSave);
|
||||
}
|
||||
|
||||
// ── Regression, zweiter Anlauf: der erste Fix (Leerzeichen nur auf evt.Description-Seite
|
||||
// entfernt) reichte nicht, wenn mapping.ClassToken SELBST ein eingebettetes Leerzeichen trägt
|
||||
// — das passiert, wenn die reale DESCRIPTION Klassen nicht mit ";", sondern mit "," trennt:
|
||||
// ExtractClassTokens fand dann kein ";" und speicherte "10a, 10b, 10c" als EIN Token samt
|
||||
// Kommas/Leerzeichen statt drei getrennte Tokens. Ein Vergleich gegen die leerzeichenbereinigte
|
||||
// Description konnte dieses Token dann strukturell NIE finden, unabhängig von der im Dialog
|
||||
// gewählten Gruppe - genau das vom Nutzer beschriebene "immer noch nicht behoben".
|
||||
|
||||
[Fact]
|
||||
public void Diff_KombinierteGruppeMitKommaGetrenntemClassToken_ErzeugtKeineFalscheVertretung()
|
||||
{
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
// So sieht ClassToken aus, wenn ExtractClassTokens die Klassen mangels ";" nicht
|
||||
// aufsplitten konnte und "10a, 10b, 10c" als ein einziges Token übernommen hat.
|
||||
Weekday = DayOfWeek.Monday, StartTime = SlotStart, Summary = "Mat_E", ClassToken = "10a, 10b, 10c",
|
||||
GroupId = Guid.NewGuid(), PeriodNumber = 1, Confirmed = true,
|
||||
};
|
||||
var events = new List<UntisIcsEvent> { BuildEvent("1", summary: "Mat_E", description: "10a, 10b, 10c HED") };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
}
|
||||
|
||||
// ── Regression: "Es ist immer noch so [...] Immer die Kurse mit Lerngruppen, die aus
|
||||
// mehreren Klassen zusammengesetzt sind" — eine zuvor erkannte Abweichung, die beim nächsten
|
||||
// Poll nicht mehr besteht, muss die ursprünglich erzeugte Vertretung wieder aufräumen, statt
|
||||
// als Karteileiche stehen zu bleiben.
|
||||
|
||||
[Fact]
|
||||
public void Diff_UnveraendertesEreignis_MeldetDessenExternalIdZumAufraeumen()
|
||||
{
|
||||
var mapping = BuildMapping(Guid.NewGuid());
|
||||
var events = new List<UntisIcsEvent> { BuildEvent("1") };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday);
|
||||
|
||||
Assert.Contains("1", result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_AufsichtWirdZurRegulaerenDuty_MeldetExternalIdZumAufraeumen()
|
||||
{
|
||||
var mapping = BuildSupervisionMapping();
|
||||
var events = new List<UntisIcsEvent>
|
||||
{
|
||||
new() { Uid = "1", Date = Monday.AddDays(1), StartTime = mapping.StartTime, Status = "CONFIRMED" },
|
||||
};
|
||||
var duties = new List<SupervisionDuty> { new() { Weekday = mapping.Weekday, AfterPeriod = mapping.AfterPeriod!.Value } };
|
||||
|
||||
var result = new UntisDiffService().Diff(events, [], [mapping], Monday, existingSupervisionDuties: duties);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
Assert.Contains("1", result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_VormalsFehlendeStundeIstWiederImFeed_MeldetMissingExternalIdZumAufraeumen()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var mapping = new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Thursday, StartTime = new TimeOnly(9, 40), Summary = "NAT", ClassToken = "8c",
|
||||
GroupId = groupId, PeriodNumber = 3, Confirmed = true,
|
||||
};
|
||||
var thisThursday = Monday.AddDays(3);
|
||||
var currentEvents = new List<UntisIcsEvent>
|
||||
{
|
||||
new() { Uid = "regular-1", Date = thisThursday, StartTime = new TimeOnly(9, 40), EndTime = new TimeOnly(11, 10), Summary = "NAT", Description = "8c HED" },
|
||||
};
|
||||
|
||||
var result = new UntisDiffService().Diff(currentEvents, [], [mapping], Monday);
|
||||
|
||||
Assert.Empty(result.SubstitutionsToSave);
|
||||
var expectedMissingId = $"missing-{mapping.Id}-{thisThursday:yyyyMMdd}";
|
||||
Assert.Contains(expectedMissingId, result.SubstitutionExternalIdsToDelete);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Diff_StatusCancelled_ErzeugtAusfall()
|
||||
{
|
||||
|
||||
@@ -102,6 +102,29 @@ public sealed class UntisMatchingServiceTests
|
||||
Assert.True(match.IsConfident);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildMatches_KombinierteKlassenKommaGetrennt_WerdenEbenfallsAufgesplittet()
|
||||
{
|
||||
// Nicht empirisch belegt, ob WebUntis mehrere Klassen mit ";" oder "," trennt (das einzige
|
||||
// real verifizierte Beispiel hatte nur eine Klasse) - ExtractClassTokens darf sich deshalb
|
||||
// nicht auf ";" verlassen, sonst landet "10a, 10b, 10c" als EIN Token mit Leerzeichen statt
|
||||
// drei getrennten (siehe UntisDiffServiceTests: Diff_KombinierteGruppeMitKommaGetrenntemClassToken...).
|
||||
var groupA = new LearningGroup { Name = "10a" };
|
||||
var groupB = new LearningGroup { Name = "10b" };
|
||||
var events = new List<UntisIcsEvent>
|
||||
{
|
||||
Event(DayOfWeek.Wednesday, new TimeOnly(9, 40), new TimeOnly(11, 10), "Mat_E", "10a, 10b HED"),
|
||||
};
|
||||
var existingSlot = new TimetableSlot { GroupId = groupB.Id, Weekday = DayOfWeek.Wednesday, PeriodNumber = 3 };
|
||||
|
||||
var result = new UntisMatchingService().BuildMatches(events, [groupA, groupB], [existingSlot], BuildPeriodSchedule());
|
||||
|
||||
var match = Assert.Single(result.Matches);
|
||||
Assert.Equal(["10a", "10b"], match.Pattern.ClassTokens);
|
||||
Assert.Equal(groupB.Id, match.SuggestedGroupId);
|
||||
Assert.True(match.IsConfident);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildMatches_KeinePassendeGruppe_BleibtUnbestaetigt()
|
||||
{
|
||||
|
||||
@@ -1130,6 +1130,36 @@ wurde bei jedem Poll erneut als "abweichend" gemeldet.
|
||||
(`"10c"` in `"10c HED"`) waren nie betroffen, da dort kein Trennzeichen im Spiel ist; deshalb
|
||||
ist der Fehler dem Nutzer erst bei diesem speziellen Kurs aufgefallen.
|
||||
|
||||
**Nachtrag zu 4.3, sechzehnte Iteration (Bugfix: falsche Vertretungen für kombinierte Gruppen
|
||||
blieben trotz behobenem Vergleich stehen):** Nutzer-Feedback nach der fünfzehnten Iteration: "Es
|
||||
klappt nicht. [...] Die falschen Vertretungen stehen noch bei den Kursen. Immer die Kurse mit
|
||||
Lerngruppen, die aus mehreren Klassen zusammengesetzt sind." Zwei Ursachen, nacheinander gefunden:
|
||||
- Der Fix aus der fünfzehnten Iteration entfernte Leerzeichen nur auf der `evt.Description`-Seite,
|
||||
nicht auf `mapping.ClassToken` selbst — schlug also weiterhin fehl, sobald `ClassToken` ein
|
||||
eingebettetes Leerzeichen trägt (z.B. wenn `ExtractClassTokens` die Klassen mangels bekanntem
|
||||
Trennzeichen nicht aufsplitten konnte). Fix: `RemoveWhitespace` symmetrisch auf beide Seiten
|
||||
angewendet; `ExtractClassTokens` akzeptiert jetzt zusätzlich `,` als Trennzeichen (nicht nur
|
||||
`;`), da nie an einem echten kombinierten Termin verifiziert wurde, welches WebUntis tatsächlich
|
||||
verwendet. Zur Eingrenzung ohne weitere Rateversuche schreibt eine abweichende Vertretung jetzt
|
||||
außerdem den genauen fehlgeschlagenen Vergleich (roher Fach-/Klassen-Wert) in ihre eigene
|
||||
Beschreibung (`UntisDiffService.DeviationReason`).
|
||||
- Der eigentliche, tiefere Bug: der Nutzer bestätigte danach neu und der Vergleich lief
|
||||
nachweislich mit aktuellem Code (die neue Diagnose-Beschreibung erschien auf einer anderen,
|
||||
echten Abweichung), aber die falschen Vertretungen für Mathe-E-Kurs/Chemie G blieben ohne die
|
||||
neue Diagnose-Beschreibung stehen — sie waren Karteileichen aus einem Poll VOR dem Fix.
|
||||
`UntisDiffService.Diff` schrieb bislang ausschließlich neu erkannte/weiterhin bestehende
|
||||
Abweichungen; es gab keinen Pfad, der eine zuvor automatisch erzeugte `SubstitutionEntry`
|
||||
wieder entfernt, sobald ein späterer Poll dieselbe Zeile nicht mehr als Abweichung einstuft.
|
||||
Fix: `UntisDiffResult` bekommt `SubstitutionExternalIdsToDelete` — befüllt für (a) eine
|
||||
Unterrichtsstunde, die nicht mehr abweicht, (b) eine Aufsicht, die jetzt einer regulären
|
||||
`SupervisionDuty` entspricht, und (c) eine zuvor als fehlend gemeldete Stunde, die im Feed
|
||||
wieder auftaucht. `UntisSyncService.ProcessIcsText` löscht dafür den vorhandenen Eintrag über
|
||||
`ISubstitutionEntryRepository.GetByExternalId`/`Delete` (no-op, falls keiner existiert).
|
||||
- Damit räumen sich einmal fälschlich erzeugte automatische Vertretungen künftig von selbst auf,
|
||||
sobald der zugrundeliegende Vergleich beim nächsten Poll keine Abweichung mehr findet — nicht
|
||||
nur bei diesem konkreten Bugfix, sondern auch bei jeder künftigen Korrektur der Zuordnung durch
|
||||
den Nutzer selbst.
|
||||
|
||||
### 4.4 Wochen-/Tagesansicht
|
||||
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3
|
||||
("Heute"-Tab: Tagesliste unten angedockt, gruppenübergreifendes Wochenraster darüber, inkl.
|
||||
|
||||
Reference in New Issue
Block a user