Fix class teacher full-day absence classification
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
admin committed 2026-10-08 11:20:17 +02:00
1 parent e4a81e8974
commit a3cd57e5d8
7 files changed
+73 -2

No files matched your search

+1
View File
@@ -71,6 +71,7 @@ public enum UntisCacheKind { Absences, ClassRegister, Roster }
/// </summary>
public class UntisCacheFetchState
{
public int AbsenceReportVersion { get; set; }
public Guid Id { get; set; } = Guid.NewGuid();
public string ClassName { get; set; } = "";
public UntisCacheKind Kind { get; set; }
@@ -367,6 +367,24 @@ public sealed class ClassTeacherViewModelsTests
Assert.False(andere.CountsAsFullDay);
}
[Fact]
public void GroupByStudentAndDay_ZweiFehlstundenBleibenTeilfehlzeit()
{
var entries = new[]
{
new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260824, 1, 45, "HED", "Che", null,
null, 1, null, true, null, 3, "nicht entsch.", false),
new UntisClassAbsenceEntryDto("Muster Max", 1, "6a", 20260824, 1, 45, "PAN", "Kunst", null,
null, 2, null, true, null, 4, "nicht entsch.", false),
};
var row = Assert.Single(ClassAbsenceDaySummaryRow.GroupByStudentAndDay(entries));
Assert.False(row.CountsAsFullDay);
Assert.True(row.IsPartialAbsence);
Assert.Equal("2 Fehlstunden", row.KindLabel);
}
[Fact]
public void GroupByStudentAndDay_TrenntGleichenNamenAnUnterschiedlichenTagen()
{
@@ -187,6 +187,45 @@ public sealed class UntisReportCacheServiceTests
Assert.Equal(4, handler.Requests.Count); // keine weiteren Anfragen - aus dem Cache bedient
}
[Fact]
public async Task GetAbsencesAsync_AlterHistorischerBerichtWirdErsetztUndDanachGecacht()
{
var day = DateOnly.FromDateTime(DateTime.Today).AddDays(-30);
var csv = Encoding.UTF8.GetBytes(
"Schüler*innen\tKlasse\tDatum\tFehlstd.\tFehlmin.\tFehltage\r\n" +
$"Muster Max\t10c\t{day:dd.MM.yy}\t2\t90\t0\r\n");
var schoolyears = "{\"result\":[{\"id\":7,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}";
var handler = new QueueHandler(
Json("{\"result\":{\"sessionId\":\"s\"}}"), Json(schoolyears), Json(schoolyears),
Json("{\"result\":[{\"id\":874,\"name\":\"10c\"}]}"),
Json("{\"data\":{\"finished\":true,\"error\":false,\"reportParams\":\"get=rpt.tmp&name=AbsencePerStudent&format=csv\"}}"),
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(csv) },
Json("{\"result\":{}}"));
await using var untis = new WebUntisIntegrationService(new HttpClient(handler), TestSupport.BuildWebUntisSettingsService());
await untis.ConnectAsync(new WebUntisCredentials("bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim"));
var absences = new FakeUntisAbsenceCache();
absences.InsertRange([new UntisAbsenceCacheEntry
{
ClassName = "10c", StudentName = "Muster Max", Date = Int(day),
AbsentPeriods = 2, CountsAsFullDay = true,
}]);
var states = new FakeUntisCacheFetchStates();
states.Save(new UntisCacheFetchState
{
ClassName = "10c", Kind = UntisCacheKind.Absences,
HotWindowFetchedAt = DateTime.UtcNow, ColdCoverageStartDate = Int(day),
});
var cache = new UntisReportCacheService(untis, absences, new FakeUntisClassRegisterCache(),
new FakeUntisStudentRosterCache(), states);
var first = Assert.Single(await cache.GetAbsencesAsync("10c", day, day));
Assert.False(first.CountsAsFullDay);
Assert.Equal(2, first.AbsentPeriods);
var requestCount = handler.Requests.Count;
Assert.False(Assert.Single(await cache.GetAbsencesAsync("10c", day, day)).CountsAsFullDay);
Assert.Equal(requestCount, handler.Requests.Count);
}
private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json"),
@@ -34,6 +34,7 @@ public sealed class UntisReportCacheService(
{
private static readonly TimeSpan HotWindowRefreshInterval = TimeSpan.FromHours(1);
private const int HotWindowDays = 14;
private const int AbsenceReportVersion = 1;
/// Roster hat anders als Fehlzeiten/Klassenbuch keine Historie (nur "aktueller Stand einer
/// Klasse") - deshalb ohne heißes/kaltes Fenster, nur "gilt der letzte Abruf noch als frisch".
@@ -66,6 +67,10 @@ public sealed class UntisReportCacheService(
DateOnly start, DateOnly end, bool forceRefresh = false, CancellationToken token = default)
{
var state = fetchState.Get(className, UntisCacheKind.Absences);
// Ältere Berichte haben Tage mit einzelnen Fehlstunden als Fehltage geliefert.
// Auch die dauerhaft gecachte Historie muss mit der Trennung neu geladen werden.
if (state is not null && state.AbsenceReportVersion != AbsenceReportVersion)
state = new UntisCacheFetchState { Id = state.Id, ClassName = className, Kind = UntisCacheKind.Absences };
var plan = Plan(state, start, end, Today(), forceRefresh, DateTime.UtcNow);
if (plan.RefreshHotWindow)
@@ -76,7 +81,7 @@ public sealed class UntisReportCacheService(
if (plan.FetchColdRange)
{
var older = await untis.GetClassAbsencesAsync(className, plan.ColdRangeStart, plan.ColdRangeEnd, token);
absenceCache.InsertRange(older.Select(ToEntry));
absenceCache.ReplaceRange(className, Int(plan.ColdRangeStart), Int(plan.ColdRangeEnd), older.Select(ToEntry));
}
if (plan.RefreshHotWindow || plan.FetchColdRange)
fetchState.Save(NextState(state, className, UntisCacheKind.Absences, plan));
@@ -137,6 +142,7 @@ public sealed class UntisReportCacheService(
Id = previous?.Id ?? Guid.NewGuid(),
ClassName = className,
Kind = kind,
AbsenceReportVersion = kind == UntisCacheKind.Absences ? AbsenceReportVersion : 0,
HotWindowFetchedAt = plan.RefreshHotWindow ? DateTime.UtcNow : previous?.HotWindowFetchedAt,
ColdCoverageStartDate = plan.ResultingColdCoverageStartDate,
};
@@ -293,6 +293,7 @@ public sealed class WebUntisClientTests
Assert.Contains("reports.do?name=ClassregEventPerStudent", handler.Requests[1].Uri);
Assert.Contains("klasseOrStudentgroupId=-1", handler.Requests[1].Uri);
Assert.Contains("studentId=-1", handler.Requests[1].Uri);
Assert.Contains("&separateAbsentDays=true&", handler.Requests[1].Uri);
Assert.Contains("rpt_sd=20260824", handler.Requests[1].Uri);
Assert.Contains("rpt_ed=20260828", handler.Requests[1].Uri);
Assert.EndsWith("reports.do?get=rpt2.tmp&name=ClassregEventPerStudent&format=csv", handler.Requests[2].Uri);
+1 -1
View File
@@ -179,7 +179,7 @@ public sealed partial class WebUntisClient : IAsyncDisposable
"&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" +
"&classExcuseStatusId=-1&_absClassSorted=on&separateAbsentDays=true&_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" +
+6
View File
@@ -1621,6 +1621,12 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk
Auch hier keine externe Schülerkennung, nur der Name — Namensabgleich soll deshalb zusätzlich auf
Klasse+Fach aus der Zeile scopen (→ genau eine lokale Lerngruppe), nicht global über alle Schüler.
Abgleichsdialog (Diff gegen `Documentation`, Übernehmen in beide Richtungen) noch nicht gebaut.
- [x] **Klassenlehrer — einzelne Fehlstunden als ganze Fehltage (Oktober 2026):**
Beim Berichtabruf ist `separateAbsentDays=true` jetzt tatsächlich aktiviert; der bisherige
Parameter `_separateAbsentDays=on` allein aktivierte die Checkbox nicht. Alte Fehlzeiten-Caches
werden über eine Berichtversion neu geladen und auch im historischen Bereich ersetzt.
Regressionen prüfen den Abrufparameter, zwei Fehlstunden sowie das Ersetzen alter Cacheeinträge.
- [x] **"Klassenlehrer"-Feature — Datenschicht (August 2026):** Zwei WebUntis-Berichte liefern, was
für einen Klassenlehrer-Überblick fehlte. `WebUntisIntegrationService.GetForeignClassRegisterEventsAsync`
ist das Spiegelbild von `GetOwnClassRegisterEventsAsync`: derselbe "-alle-"-Rohbericht