This commit is contained in:
1 parent
e4a81e8974
commit
a3cd57e5d8
7 files changed
+73
-2
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user