fix: WebUntis-Schülerreport verkraftet fehlenden externKey

Klassenlehrer-Übersicht scheiterte komplett mit "Ungültige Zahl in Spalte
externKey", sobald ein Schüler der Klasse keinen gepflegten externen
Schlüssel hat (in echten Schuldaten vorkommend). externKey ist jetzt
optional statt Pflichtfeld (UntisStudent/UntisStudentDto.ExternKey -> int?);
betraf denselben Bericht wie der bestehende WebUntis-Klassenimport, der das
aber schon immer beim Weiterimport toleriert hatte.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 22:47:04 +02:00
co-authored by Claude Sonnet 5
parent 33c15ffe3e
commit 056e864edd
6 changed files with 34 additions and 4 deletions
@@ -22,7 +22,7 @@ public sealed record UntisTimetablePeriodDto(int Id, int Date, int StartTime, in
IReadOnlyList<UntisEntityDto> Subjects, IReadOnlyList<UntisEntityDto> Rooms); IReadOnlyList<UntisEntityDto> Subjects, IReadOnlyList<UntisEntityDto> Rooms);
public sealed record UntisStudentAddressDto(string? Email, string? Mobile, string? Phone, string? City, public sealed record UntisStudentAddressDto(string? Email, string? Mobile, string? Phone, string? City,
string? PostCode, string? Street); string? PostCode, string? Street);
public sealed record UntisStudentDto(int UntisId, int ExternKey, string ClassName, string? Name, string? LongName, public sealed record UntisStudentDto(int UntisId, int? ExternKey, string ClassName, string? Name, string? LongName,
string? ForeName, string DisplayName, string? Gender, int? BirthDate, string? BirthDateRaw, int? EntryDate, string? ForeName, string DisplayName, string? Gender, int? BirthDate, string? BirthDateRaw, int? EntryDate,
string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty, string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty,
string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL); string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL);
@@ -38,7 +38,7 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b
return students return students
.Select(s => .Select(s =>
{ {
var absence = absenceByKey.GetValueOrDefault(s.ExternKey) var absence = (s.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null)
?? absenceByName.GetValueOrDefault(s.DisplayName); ?? absenceByName.GetValueOrDefault(s.DisplayName);
return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null, return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null,
absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}", absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}",
@@ -51,6 +51,19 @@ public sealed class WebUntisStudentReportParserTests
Assert.Equal(20270731, student.ExitDate); Assert.Equal(20270731, student.ExitDate);
} }
[Fact]
public void Parse_ErlaubtFehlendenExternKey()
{
// Reale Schuldaten enthalten Schüler*innen ohne gepflegten externen Schlüssel (z.B. frisch
// angelegt) - das darf nicht den Abruf der gesamten Klassenliste zum Absturz bringen.
const string report = "id\texternKey\tklasse.name\r\n17\t\t10a\r\n";
var student = Assert.Single(WebUntisStudentReportParser.Parse(report));
Assert.Equal(17, student.UntisId);
Assert.Null(student.ExternKey);
}
[Fact] [Fact]
public void Parse_LehntUngueltigePflichtIdAb() public void Parse_LehntUngueltigePflichtIdAb()
{ {
+4 -1
View File
@@ -41,9 +41,12 @@ public sealed record UntisStudentAddress(
string? PostCode, string? PostCode,
string? Street); string? Street);
// ExternKey optional: reale Schuldaten enthalten Schüler*innen (z.B. frisch angelegt/übernommen)
// ohne gepflegten externen Schlüssel - ein fehlender Wert bei einer/einem soll nicht den Abruf der
// gesamten Klassenliste zum Absturz bringen (siehe WebUntisStudentReportParser).
public sealed record UntisStudent( public sealed record UntisStudent(
int UntisId, int UntisId,
int ExternKey, int? ExternKey,
string ClassName, string ClassName,
string? Name, string? Name,
string? LongName, string? LongName,
@@ -23,7 +23,7 @@ public static class WebUntisStudentReportParser
values[headers[index]] = index < row.Count ? row[index].Trim() : null; values[headers[index]] = index < row.Count ? row[index].Trim() : null;
var untisId = RequiredInt(Get(values, "id"), "id"); var untisId = RequiredInt(Get(values, "id"), "id");
var externalKey = RequiredInt(Get(values, "externKey"), "externKey"); var externalKey = OptionalInt(Get(values, "externKey"));
var name = Optional(Get(values, "name")); var name = Optional(Get(values, "name"));
var lastName = Optional(Get(values, "longName")); var lastName = Optional(Get(values, "longName"));
var firstName = Optional(Get(values, "foreName")); var firstName = Optional(Get(values, "foreName"));
@@ -73,6 +73,11 @@ public static class WebUntisStudentReportParser
? parsed ? parsed
: throw new InvalidDataException($"Ungültige Zahl in Spalte \"{field}\"."); : throw new InvalidDataException($"Ungültige Zahl in Spalte \"{field}\".");
private static int? OptionalInt(string? value) =>
int.TryParse(value?.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
? parsed
: null;
private static int? GermanDate(string? value) private static int? GermanDate(string? value)
{ {
if (!DateOnly.TryParseExact(value?.Trim(), ["dd.MM.yyyy", "yyyyMMdd"], CultureInfo.InvariantCulture, if (!DateOnly.TryParseExact(value?.Trim(), ["dd.MM.yyyy", "yyyyMMdd"], CultureInfo.InvariantCulture,
+9
View File
@@ -1328,6 +1328,15 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
Schülerdetail; ein Detail-Drill-down pro Schüler*in (Heatmap gefehlter Einzelstunden, Schülerdetail; ein Detail-Drill-down pro Schüler*in (Heatmap gefehlter Einzelstunden,
Verspätung/vorzeitiges Verlassen je Woche); eine KI-gestützte Zusammenfassung/Bewertung der Verspätung/vorzeitiges Verlassen je Woche); eine KI-gestützte Zusammenfassung/Bewertung der
Einträge (pro Schüler*in oder gesamt) mit Namens-Anonymisierung/Synonymen vor dem Versand. Einträge (pro Schüler*in oder gesamt) mit Namens-Anonymisierung/Synonymen vor dem Versand.
**Echter Fehler gefunden und behoben:** Die Übersicht rief für das Roster
`WebUntisIntegrationService.GetStudentsAsync` auf (den bestehenden, schon länger genutzten
Schülerlisten-Bericht) — `WebUntisStudentReportParser` behandelte die Spalte `externKey` bisher als
Pflichtfeld (`RequiredInt`) und ließ den kompletten Abruf mit "Ungültige Zahl in Spalte
\"externKey\"" scheitern, sobald auch nur eine/einer der Schüler*innen der Klasse keinen
gepflegten externen Schlüssel hat (in der Praxis vorkommend, z. B. frisch angelegt). `externKey`
ist jetzt optional (`UntisStudent.ExternKey`/`UntisStudentDto.ExternKey``int?`) — betraf auch
den bestehenden WebUntis-Klassenimport in der Kursansicht, der das aber schon immer über
`FirstNotEmpty` beim Weiterimport toleriert hatte und deshalb nicht auffiel.
### 4.4 Wochen-/Tagesansicht ### 4.4 Wochen-/Tagesansicht
- [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3 - [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3