fix: Klassenlehrer-Übersicht rief Schülerreport bislang live statt gecacht ab

Die Übersicht wirkte beim Öffnen spürbar zeitverzögert, obwohl Fehlzeiten
und Klassenbuch schon lokal gecacht waren (Nutzer-Feedback: "fühlt sich an
wie ein Live-Pull mit CSV-Parsing"). Grund: genau das passierte - der
Schülerreport (Namen fürs Roster) lief komplett am Cache vorbei bei jedem
Öffnen live gegen WebUntis.

Neues UntisStudentRosterCacheEntry/UntisStudentRosterCacheRepository
(gleiches "kein db.OnChange"-Prinzip wie die bestehenden Caches) plus
UntisReportCacheService.GetStudentRosterAsync - ohne heißes/kaltes Fenster,
da eine Klassenliste keine Historie hat, nur dieselbe Stundenschwelle
"gilt der letzte Abruf noch als frisch".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:18:53 +02:00
co-authored by Claude Sonnet 5
parent 69cc16bbd2
commit 0c8bb58e1b
12 changed files with 149 additions and 14 deletions
@@ -1,3 +1,4 @@
using LehrerApp.Core.Models;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.ClassTeacher;
using Xunit;
@@ -125,10 +126,6 @@ public sealed class ClassTeacherViewModelsTests
Assert.True(ben.HasRecentClassRegisterEntry);
}
private static UntisStudentDto Student(int? externKey, string displayName) => new(
UntisId: externKey ?? 0, ExternKey: externKey, ClassName: "6a", Name: displayName, LongName: null,
ForeName: null, DisplayName: displayName, Gender: null, BirthDate: null, BirthDateRaw: null,
EntryDate: null, EntryDateRaw: null, ExitDate: null, ExitDateRaw: null, Text: null,
MedicalReportDuty: null, Schulpflicht: null, Majority: null,
Address: new UntisStudentAddressDto(null, null, null, null, null, null), AttributeIL: null);
private static UntisStudentRosterCacheEntry Student(int? externKey, string displayName) =>
new() { ClassName = "6a", ExternKey = externKey, DisplayName = displayName };
}
+12
View File
@@ -500,6 +500,18 @@ public class FakeUntisClassRegisterCache : IUntisClassRegisterCacheRepository
public void InsertRange(IEnumerable<UntisClassRegisterCacheEntry> entries) => _all.AddRange(entries);
}
public class FakeUntisStudentRosterCache : IUntisStudentRosterCacheRepository
{
private readonly List<UntisStudentRosterCacheEntry> _all = [];
public List<UntisStudentRosterCacheEntry> GetByClass(string className) =>
_all.Where(e => e.ClassName == className).ToList();
public void ReplaceAll(string className, IEnumerable<UntisStudentRosterCacheEntry> entries)
{
_all.RemoveAll(e => e.ClassName == className);
_all.AddRange(entries);
}
}
public class FakeUntisCacheFetchStates : IUntisCacheFetchStateRepository
{
private readonly List<UntisCacheFetchState> _all = [];
@@ -144,8 +144,9 @@ public sealed class UntisReportCacheServiceTests
var absenceCache = new FakeUntisAbsenceCache();
var classRegisterCache = new FakeUntisClassRegisterCache();
var rosterCache = new FakeUntisStudentRosterCache();
var fetchStates = new FakeUntisCacheFetchStates();
var cache = new UntisReportCacheService(untis, absenceCache, classRegisterCache, fetchStates);
var cache = new UntisReportCacheService(untis, absenceCache, classRegisterCache, rosterCache, fetchStates);
var today = DateOnly.FromDateTime(DateTime.Today);
var first = await cache.GetClassRegisterEventsAsync("10c", today, today);
@@ -157,6 +158,35 @@ public sealed class UntisReportCacheServiceTests
Assert.Equal(4, handler.Requests.Count); // keine weiteren Anfragen - aus dem Cache bedient
}
[Fact]
public async Task GetStudentRosterAsync_ZweiterAufrufBinnenEinerStunde_RuftWebUntisNichtErneutAb()
{
var csv = Encoding.UTF8.GetBytes(
"id\texternKey\tklasse.name\tname\tlongName\tforeName\r\n" +
"1\t9001\t10c\tMUST\tMustermann\tMax\r\n");
var handler = new QueueHandler(
Json("{\"result\":{\"sessionId\":\"s\"}}"),
Json("{\"result\":[{\"id\":1,\"name\":\"2026/27\",\"startDate\":20260801,\"endDate\":20270731}]}"),
Json("{\"data\":{\"finished\":true,\"error\":false," +
"\"reportParams\":\"get=rpt.tmp&name=Student&format=csv\"}}"),
new HttpResponseMessage(HttpStatusCode.OK) { Content = new ByteArrayContent(csv) });
var settings = TestSupport.BuildWebUntisSettingsService();
await using var untis = new WebUntisIntegrationService(new HttpClient(handler), settings);
await untis.ConnectAsync(new WebUntisCredentials("bk-ostvest", "arche.webuntis.com", "lehrkraft", "geheim"));
var cache = new UntisReportCacheService(untis, new FakeUntisAbsenceCache(), new FakeUntisClassRegisterCache(),
new FakeUntisStudentRosterCache(), new FakeUntisCacheFetchStates());
var first = await cache.GetStudentRosterAsync("10c");
Assert.Single(first);
Assert.Equal("Max Mustermann", first[0].DisplayName);
Assert.Equal(4, handler.Requests.Count);
var second = await cache.GetStudentRosterAsync("10c");
Assert.Single(second);
Assert.Equal(4, handler.Requests.Count); // keine weiteren Anfragen - aus dem Cache bedient
}
private static HttpResponseMessage Json(string json) => new(HttpStatusCode.OK)
{
Content = new StringContent(json, Encoding.UTF8, "application/json"),