Klassenbucheinträge abgleich und poll

This commit is contained in:
2026-08-25 09:31:41 +02:00
parent 87a7badb44
commit 7fa8a93c82
14 changed files with 584 additions and 9 deletions
@@ -0,0 +1,53 @@
using System.Globalization;
namespace LehrerApp.WebUntis;
public static class WebUntisClassRegisterEventReportParser
{
public static IReadOnlyList<UntisClassRegisterEventReportEntry> Parse(string content)
{
var rows = TabSeparatedTextReader.ParseRows(content, '\t');
if (rows.Count == 0) return [];
var headers = rows[0]
.Select((header, index) => (index == 0 ? header.TrimStart('\uFEFF') : header).Trim())
.ToArray();
var result = new List<UntisClassRegisterEventReportEntry>();
foreach (var row in rows.Skip(1))
{
if (row.All(string.IsNullOrWhiteSpace)) continue;
var values = new Dictionary<string, string?>(StringComparer.Ordinal);
for (var index = 0; index < headers.Length; index++)
values[headers[index]] = index < row.Count ? row[index].Trim() : null;
result.Add(new UntisClassRegisterEventReportEntry(
Get(values, "Klasse")?.Trim() ?? "",
GermanShortDate(Get(values, "Datum")) ?? throw new InvalidDataException(
"Ungültiges Datum in Spalte \"Datum\"."),
Optional(Get(values, "Fach")),
Get(values, "Name")?.Trim() ?? "",
Optional(Get(values, "Benutzer")),
Optional(Get(values, "Eintragskategorie")),
Optional(Get(values, "Kategoriegruppe")),
Optional(Get(values, "Text"))));
}
return result;
}
private static string? Get(IReadOnlyDictionary<string, string?> values, string key) =>
values.TryGetValue(key, out var value) ? value : null;
private static string? Optional(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private static int? GermanShortDate(string? value)
{
if (!DateOnly.TryParseExact(value?.Trim(), "dd.MM.yy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var date))
return null;
return date.Year * 10_000 + date.Month * 100 + date.Day;
}
}
+27
View File
@@ -148,6 +148,33 @@ public sealed class WebUntisClient : IAsyncDisposable
return WebUntisLessonAbsenceParser.Parse(reportText);
}, cancellationToken);
// Undokumentierter interner Bericht der WebUntis-WebApp für Klassenbucheinträge ("-alle-"-Bereich,
// damit auch Einträge zu fremden Klassen aus dem eigenen Unterricht mit dabei sind - die
// Oberfläche bietet sonst nur "eigene Klasse" oder "-alle-" an). klasseOrStudentgroupId/
// studentId=-1 entspricht "-alle-" und wird lokal auf eigene Lerngruppen gefiltert.
// Query-Parameter 1:1 aus einem echten Browser-Request übernommen (bis auf das nicht benötigte
// selectedDateRange/_csrf, siehe GetLessonAbsencesAsync). Anders als AbsencePerLesson liefert
// dieser Bericht keine externe Schülerkennung, nur den Namen (siehe UntisClassRegisterEventReportEntry).
public Task<IReadOnlyList<UntisClassRegisterEventReportEntry>> GetClassRegisterEventsReportAsync(
int startDate, int endDate, CancellationToken cancellationToken) => WithSessionAsync(async sessionId =>
{
var query = "name=ClassregEventPerStudent&format=csv&klasseOrStudentgroupId=-1&studentId=-1" +
"&calendarChange=0&withAbsences=true&_withAbsences=on&withLateness=true&_withLateness=on" +
"&excludeNotCountingAbsences=true&_excludeNotCountingAbsences=on&_allStudents=on" +
"&absStudGroup=1&studExcuseStatusId=-1&_withoutPageBreaksAbsPerStudent=on&absClassGroup=1" +
"&classExcuseStatusId=-1&_absClassSorted=on&_separateAbsentDays=on&_filterAbsencesKlasse=on" +
"&absSubjectGroup=7&absSubjectId=-1&absSubjectGroupGroup=7&subjectGroupId=-1&_totalDays=on" +
"&absentPeriodsLimit=16&reportWeeks=4&dayLimit=3&excuseGroup=1&_groupPerWeek=on" +
"&_excuseStatusAll=on&studEventReasonId=-1&classEventReasonId=-1&_evntClassSorted=on" +
"&_withoutPageBreaks=on&withStudentAbsences=true&_withStudentAbsences=on" +
"&examinationTypeId=-1&teachingMethodId=-1&examTypeId=-1&_usingMarkNames=on" +
$"&rpt_sd={startDate}&rpt_ed={endDate}&rpt_syid=-1&rpt_drdtype=CUSTOM";
var reportData = await RequestReportAsync(sessionId, query, cancellationToken)
?? await PollReportAsync(sessionId, cancellationToken);
var reportText = await FetchReportTextAsync(sessionId, reportData, cancellationToken);
return WebUntisClassRegisterEventReportParser.Parse(reportText);
}, cancellationToken);
public Task<IReadOnlyList<UntisSubstitution>> GetSubstitutionsAsync(int startDate, int endDate,
int? departmentId, CancellationToken cancellationToken) => WithSessionAsync(async sessionId =>
{
+12
View File
@@ -147,6 +147,18 @@ public sealed record UntisLessonAbsence(
string? HandledOn,
bool Counts);
// "-alle-"-Bericht ohne externe Schülerkennung - anders als bei AbsencePerLesson bleibt für den
// Abgleich mit lokalen Schülern nur der Name (siehe Namens-Fallback im Desktop-Abgleich).
public sealed record UntisClassRegisterEventReportEntry(
string ClassName,
int Date,
string? Subject,
string StudentName,
string? TeacherUsername,
string? CategoryName,
string? CategoryGroup,
string? Text);
public sealed record UntisClassRegisterEntry(
int? StudentKey,
string? Surname,