Untis-API Optimierung.
This commit is contained in:
@@ -100,11 +100,12 @@ public class App : Application
|
||||
|
||||
private static void DisposeServices()
|
||||
{
|
||||
if (_serviceProvider is null) return;
|
||||
var serviceProvider = Interlocked.Exchange(ref _serviceProvider, null);
|
||||
if (serviceProvider is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
|
||||
serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -112,18 +113,19 @@ public class App : Application
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Der direkte WebUntis-Client meldet seine Sitzung asynchron ab. Ein synchrones
|
||||
// ServiceProvider.Dispose() lehnt reine IAsyncDisposable-Dienste ab und ließ die App
|
||||
// beim Schließen mit InvalidOperationException abstürzen.
|
||||
// Der Exit-Handler läuft synchron auf dem Avalonia-UI-Thread. Die asynchrone
|
||||
// Entsorgung darf dort nicht mit GetResult() gestartet werden: Fortsetzungen aus
|
||||
// WebUntis/HttpClient könnten sonst auf den blockierten UI-Kontext zurückwarten.
|
||||
try
|
||||
{
|
||||
_serviceProvider.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
Task.Run(async () =>
|
||||
await serviceProvider.DisposeAsync().ConfigureAwait(false))
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppBootstrapper.Logger.Error("Dienste konnten beim Beenden nicht vollständig freigegeben werden.", ex);
|
||||
}
|
||||
_serviceProvider = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ public sealed record UntisStudentReportDto(int Count, string? ClassNameFilter, I
|
||||
public sealed record UntisStudentAbsenceDto(int StudentKey, int Date, int StartTime, int EndTime, int AbsentMinutes,
|
||||
bool Checked, string? AbsenceReason, string? ExcuseStatus, int? SubjectId, IReadOnlyList<int> TeacherIds,
|
||||
string? StudentGroup);
|
||||
public sealed record UntisStudentAbsenceReportDto(int StudentKey, int StartDate, int EndDate, int EntryCount,
|
||||
int AbsentMinutes, IReadOnlyList<UntisStudentAbsenceDto> Absences);
|
||||
|
||||
/// <summary>Direkter WebUntis-Client des Desktops. Personenbezogene Antworten und der
|
||||
/// unverschlüsselte CSV-Report passieren zu keinem Zeitpunkt den LehrerApp-Server.</summary>
|
||||
@@ -56,19 +54,19 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
}
|
||||
|
||||
WebUntisClient? previous;
|
||||
await _clientGate.WaitAsync(token);
|
||||
await _clientGate.WaitAsync(token).ConfigureAwait(false);
|
||||
try { previous = _client; _client = candidate; }
|
||||
finally { _clientGate.Release(); }
|
||||
if (previous is not null) await previous.DisposeAsync();
|
||||
if (previous is not null) await previous.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task DisconnectAsync(CancellationToken token = default)
|
||||
{
|
||||
WebUntisClient? previous;
|
||||
await _clientGate.WaitAsync(token);
|
||||
await _clientGate.WaitAsync(token).ConfigureAwait(false);
|
||||
try { previous = _client; _client = null; }
|
||||
finally { _clientGate.Release(); }
|
||||
if (previous is not null) await previous.DisposeAsync();
|
||||
if (previous is not null) await previous.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<UntisSchoolYearDto>> GetSchoolYearsAsync(CancellationToken token = default) => ExecuteAsync(
|
||||
@@ -109,14 +107,13 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
x.Address.PostCode, x.Address.Street), x.AttributeIL)).ToList());
|
||||
}, token);
|
||||
|
||||
public Task<UntisStudentAbsenceReportDto> GetAbsencesAsync(int studentKey, DateOnly start, DateOnly end,
|
||||
public Task<IReadOnlyList<UntisStudentAbsenceDto>> GetAbsencesAsync(DateOnly start, DateOnly end,
|
||||
CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var report = await client.GetStudentAbsencesAsync(studentKey, Date(start), Date(end), token);
|
||||
return new UntisStudentAbsenceReportDto(report.StudentKey, report.StartDate, report.EndDate,
|
||||
report.EntryCount, report.AbsentMinutes, report.Absences.Select(x => new UntisStudentAbsenceDto(
|
||||
x.StudentKey, x.Date, x.StartTime, x.EndTime, x.AbsentMinutes, x.Checked, x.AbsenceReason,
|
||||
x.ExcuseStatus, x.SubjectId, x.TeacherIds, x.StudentGroup)).ToList());
|
||||
var absences = await client.GetAbsencesAsync(Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisStudentAbsenceDto>)absences.Select(x => new UntisStudentAbsenceDto(
|
||||
x.StudentKey, x.Date, x.StartTime, x.EndTime, x.AbsentMinutes, x.Checked, x.AbsenceReason,
|
||||
x.ExcuseStatus, x.SubjectId, x.TeacherIds, x.StudentGroup)).ToList();
|
||||
}, token);
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
||||
@@ -153,5 +150,5 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
private static WebUntisIntegrationException Translate(Exception exception) =>
|
||||
new(exception.Message);
|
||||
|
||||
public async ValueTask DisposeAsync() => await DisconnectAsync();
|
||||
public async ValueTask DisposeAsync() => await DisconnectAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -60,16 +59,14 @@ public partial class WebUntisAbsenceComparisonViewModel : ObservableObject
|
||||
var localSessions = _sessions.GetByGroup(_group.Id)
|
||||
.Where(x => x.Date >= start && x.Date <= end).GroupBy(x => x.Date)
|
||||
.ToDictionary(x => x.Key, x => x.First());
|
||||
var loaded = new ConcurrentBag<(Student Student, UntisStudentAbsenceDto Absence)>();
|
||||
var linked = courseStudents.Select(student => (Student: student, Key: StudentKey(student)))
|
||||
.Where(x => x.Key is not null).ToList();
|
||||
await Parallel.ForEachAsync(linked, new ParallelOptions { MaxDegreeOfParallelism = 4 }, async (item, token) =>
|
||||
{
|
||||
var report = await _untis.GetAbsencesAsync(item.Key!.Value, start, end, token);
|
||||
foreach (var absence in report.Absences) loaded.Add((item.Student, absence));
|
||||
});
|
||||
.Where(x => x.Key is not null).ToDictionary(x => x.Key!.Value, x => x.Student);
|
||||
var absences = await _untis.GetAbsencesAsync(start, end);
|
||||
var loaded = absences.Where(absence => linked.ContainsKey(absence.StudentKey))
|
||||
.Select(absence => (Student: linked[absence.StudentKey], Absence: absence))
|
||||
.OrderBy(x => x.Absence.Date).ThenBy(x => x.Student.FullName);
|
||||
|
||||
foreach (var item in loaded.OrderBy(x => x.Absence.Date).ThenBy(x => x.Student.FullName))
|
||||
foreach (var item in loaded)
|
||||
{
|
||||
if (!TryDate(item.Absence.Date, out var date)) continue;
|
||||
localSessions.TryGetValue(date, out var session);
|
||||
|
||||
@@ -13,6 +13,7 @@ using LehrerApp.Desktop.Views.Shared;
|
||||
using LehrerApp.Desktop.Views.Students;
|
||||
using LehrerApp.Desktop.Views.Workload;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Groups;
|
||||
@@ -174,7 +175,7 @@ public partial class GroupDetailView : UserControl
|
||||
var values = new[]
|
||||
{
|
||||
student.LongName ?? student.Name, student.ForeName, student.Gender,
|
||||
student.BirthDate?.ToString() ?? student.BirthDateRaw, student.ClassName,
|
||||
FormatBirthDate(student), student.ClassName,
|
||||
student.ExternKey.ToString(), student.Address.Email, student.Address.Mobile,
|
||||
student.Address.Phone, student.Address.City, student.Address.PostCode, student.Address.Street,
|
||||
};
|
||||
@@ -183,6 +184,17 @@ public partial class GroupDetailView : UserControl
|
||||
return new ImportFile("webuntis-students.csv", Encoding.UTF8.GetBytes(builder.ToString()));
|
||||
}
|
||||
|
||||
private static string? FormatBirthDate(UntisStudentDto student)
|
||||
{
|
||||
if (student.BirthDate is not { } normalizedDate) return student.BirthDateRaw;
|
||||
|
||||
var value = normalizedDate.ToString("D8", CultureInfo.InvariantCulture);
|
||||
return DateOnly.TryParseExact(value, "yyyyMMdd", CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.None, out var date)
|
||||
? date.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)
|
||||
: student.BirthDateRaw;
|
||||
}
|
||||
|
||||
private static string SafeTsv(string? value) => (value ?? "").Replace('\t', ' ')
|
||||
.Replace('\r', ' ').Replace('\n', ' ');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user