158 lines
9.2 KiB
C#
158 lines
9.2 KiB
C#
using LehrerApp.WebUntis;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
public sealed class WebUntisIntegrationException(string message) : Exception(message);
|
|
|
|
public sealed record UntisSchoolYearDto(int UntisId, string Name, int StartDate, int EndDate);
|
|
public sealed record UntisClassDto(int UntisId, string Name, string? LongName);
|
|
public sealed record UntisTeacherDto(int UntisId, string Name, string? ForeName, string? LongName, string? Title,
|
|
bool Active, IReadOnlyList<int> DepartmentUntisIds)
|
|
{
|
|
public string DisplayName => string.IsNullOrWhiteSpace(LongName)
|
|
? Name
|
|
: $"{ForeName} {LongName} ({Name})".Trim();
|
|
}
|
|
public sealed record UntisEntityDto(int Id, string Name, int? OriginalId, string? OriginalName, string? ExternalKey);
|
|
public sealed record UntisTimeUnitDto(string Name, int StartTime, int EndTime);
|
|
public sealed record UntisTimeGridDayDto(int Day, IReadOnlyList<UntisTimeUnitDto> TimeUnits);
|
|
public sealed record UntisTimetablePeriodDto(int Id, int Date, int StartTime, int EndTime, string? Code,
|
|
string? ActivityType, string? Info, string? LessonText, string? SubstitutionText, string? StudentGroup,
|
|
IReadOnlyList<UntisEntityDto> Classes, IReadOnlyList<UntisEntityDto> Teachers,
|
|
IReadOnlyList<UntisEntityDto> Subjects, IReadOnlyList<UntisEntityDto> Rooms);
|
|
public sealed record UntisStudentAddressDto(string? Email, string? Mobile, string? Phone, string? City,
|
|
string? PostCode, string? Street);
|
|
public sealed record UntisStudentDto(int UntisId, int ExternKey, string ClassName, string? Name, string? LongName,
|
|
string? ForeName, string DisplayName, string? Gender, int? BirthDate, string? BirthDateRaw, int? EntryDate,
|
|
string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty,
|
|
string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL);
|
|
public sealed record UntisStudentReportDto(int Count, string? ClassNameFilter, IReadOnlyList<UntisStudentDto> Students);
|
|
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>
|
|
public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettingsService settings) : IAsyncDisposable
|
|
{
|
|
private readonly SemaphoreSlim _clientGate = new(1, 1);
|
|
private WebUntisClient? _client;
|
|
|
|
public bool IsAvailable => settings.ApiIsConfigured;
|
|
|
|
public async Task ConnectAsync(WebUntisCredentials credentials, CancellationToken token = default)
|
|
{
|
|
var candidate = CreateClient(credentials);
|
|
try
|
|
{
|
|
await candidate.GetSchoolYearsAsync(token);
|
|
}
|
|
catch (Exception exception) when (IsUntisError(exception))
|
|
{
|
|
await candidate.DisposeAsync();
|
|
throw Translate(exception);
|
|
}
|
|
|
|
WebUntisClient? previous;
|
|
await _clientGate.WaitAsync(token);
|
|
try { previous = _client; _client = candidate; }
|
|
finally { _clientGate.Release(); }
|
|
if (previous is not null) await previous.DisposeAsync();
|
|
}
|
|
|
|
public async Task DisconnectAsync(CancellationToken token = default)
|
|
{
|
|
WebUntisClient? previous;
|
|
await _clientGate.WaitAsync(token);
|
|
try { previous = _client; _client = null; }
|
|
finally { _clientGate.Release(); }
|
|
if (previous is not null) await previous.DisposeAsync();
|
|
}
|
|
|
|
public Task<IReadOnlyList<UntisSchoolYearDto>> GetSchoolYearsAsync(CancellationToken token = default) => ExecuteAsync(
|
|
async client => (IReadOnlyList<UntisSchoolYearDto>)(await client.GetSchoolYearsAsync(token))
|
|
.Select(x => new UntisSchoolYearDto(x.UntisId, x.Name, x.StartDate, x.EndDate)).ToList(), token);
|
|
|
|
public Task<IReadOnlyList<UntisClassDto>> GetClassesAsync(int schoolYearId, CancellationToken token = default) => ExecuteAsync(
|
|
async client => (IReadOnlyList<UntisClassDto>)(await client.GetClassesAsync(schoolYearId, token))
|
|
.Select(x => new UntisClassDto(x.UntisId, x.Name, x.LongName)).ToList(), token);
|
|
|
|
public Task<IReadOnlyList<UntisTeacherDto>> GetTeachersAsync(CancellationToken token = default) => ExecuteAsync(
|
|
async client => (IReadOnlyList<UntisTeacherDto>)(await client.GetTeachersAsync(token))
|
|
.Select(x => new UntisTeacherDto(x.UntisId, x.Name, x.ForeName, x.LongName, x.Title, x.Active,
|
|
x.DepartmentUntisIds)).ToList(), token);
|
|
|
|
public Task<IReadOnlyList<UntisTimeGridDayDto>> GetTimeGridAsync(CancellationToken token = default) => ExecuteAsync(
|
|
async client => (IReadOnlyList<UntisTimeGridDayDto>)(await client.GetTimeGridAsync(token))
|
|
.Select(x => new UntisTimeGridDayDto(x.Day,
|
|
x.TimeUnits.Select(t => new UntisTimeUnitDto(t.Name, t.StartTime, t.EndTime)).ToList())).ToList(), token);
|
|
|
|
public Task<IReadOnlyList<UntisTimetablePeriodDto>> GetTimetableAsync(int teacherId, DateOnly start,
|
|
DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
|
(IReadOnlyList<UntisTimetablePeriodDto>)(await client.GetTimetableAsync(
|
|
UntisTimetableElementType.Teacher, teacherId, Date(start), Date(end), token))
|
|
.Select(x => new UntisTimetablePeriodDto(x.Id, x.Date, x.StartTime, x.EndTime, x.Code,
|
|
x.ActivityType, x.Info, x.LessonText, x.SubstitutionText, x.StudentGroup,
|
|
Entities(x.Classes), Entities(x.Teachers), Entities(x.Subjects), Entities(x.Rooms))).ToList(), token);
|
|
|
|
public Task<UntisStudentReportDto> GetStudentsAsync(string className, CancellationToken token = default) => ExecuteAsync(
|
|
async client =>
|
|
{
|
|
var report = await client.GetStudentReportAsync(className, token);
|
|
return new UntisStudentReportDto(report.Count, report.ClassNameFilter, report.Students.Select(x =>
|
|
new UntisStudentDto(x.UntisId, x.ExternKey, x.ClassName, x.Name, x.LongName, x.ForeName,
|
|
x.DisplayName, x.Gender, x.BirthDate, x.BirthDateRaw, x.EntryDate, x.EntryDateRaw,
|
|
x.ExitDate, x.ExitDateRaw, x.Text, x.MedicalReportDuty, x.Schulpflicht, x.Majority,
|
|
new UntisStudentAddressDto(x.Address.Email, x.Address.Mobile, x.Address.Phone, x.Address.City,
|
|
x.Address.PostCode, x.Address.Street), x.AttributeIL)).ToList());
|
|
}, token);
|
|
|
|
public Task<UntisStudentAbsenceReportDto> GetAbsencesAsync(int studentKey, 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());
|
|
}, token);
|
|
|
|
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
|
{
|
|
try { return await operation(await GetClientAsync(token)); }
|
|
catch (Exception exception) when (IsUntisError(exception)) { throw Translate(exception); }
|
|
}
|
|
|
|
private async Task<WebUntisClient> GetClientAsync(CancellationToken token)
|
|
{
|
|
if (_client is not null) return _client;
|
|
await _clientGate.WaitAsync(token);
|
|
try
|
|
{
|
|
if (_client is not null) return _client;
|
|
var credentials = settings.GetApiCredentials()
|
|
?? throw new WebUntisIntegrationException("Bitte zuerst die WebUntis-Anmeldedaten einrichten.");
|
|
return _client = CreateClient(credentials);
|
|
}
|
|
finally { _clientGate.Release(); }
|
|
}
|
|
|
|
private WebUntisClient CreateClient(WebUntisCredentials credentials) => new(http, new WebUntisOptions
|
|
{
|
|
School = credentials.School, Host = credentials.Host, Username = credentials.Username,
|
|
Password = credentials.Password, Client = "LehrerApp-Desktop", SessionIdleTimeoutMinutes = 10,
|
|
});
|
|
|
|
private static int Date(DateOnly date) => date.Year * 10000 + date.Month * 100 + date.Day;
|
|
private static IReadOnlyList<UntisEntityDto> Entities(IReadOnlyList<UntisEntity> values) => values
|
|
.Select(x => new UntisEntityDto(x.Id, x.Name, x.OriginalId, x.OriginalName, x.ExternalKey)).ToList();
|
|
private static bool IsUntisError(Exception exception) => exception is WebUntisException
|
|
or WebUntisConfigurationException or InvalidDataException;
|
|
private static WebUntisIntegrationException Translate(Exception exception) =>
|
|
new(exception.Message);
|
|
|
|
public async ValueTask DisposeAsync() => await DisconnectAsync();
|
|
}
|