@@ -205,6 +205,13 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<UntisOpenPeriodsMeta> GetOpenPeriodsMetaAsync(int schoolYearId, CancellationToken token = default) =>
|
||||||
|
ExecuteAsync(client => client.GetOpenPeriodsMetaAsync(schoolYearId, token), token);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<UntisOpenPeriod>> GetOpenPeriodsAsync(int schoolYearId, int? teacherId,
|
||||||
|
int? classId, DateOnly start, DateOnly end, CancellationToken token = default) =>
|
||||||
|
ExecuteAsync(client => client.GetOpenPeriodsAsync(schoolYearId, teacherId, classId, start, end, token), token);
|
||||||
|
|
||||||
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
||||||
{
|
{
|
||||||
try { return await operation(await GetClientAsync(token)); }
|
try { return await operation(await GetClientAsync(token)); }
|
||||||
|
|||||||
@@ -250,6 +250,8 @@
|
|||||||
<TextBlock Classes="navlabel" Text="Klassenlehrer"/>
|
<TextBlock Classes="navlabel" Text="Klassenlehrer"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button Classes="navitem" HorizontalAlignment="Stretch"
|
||||||
|
Content="Offene Untis-Stunden" Click="OnOpenUntisPeriods"/>
|
||||||
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
<Button Classes="navitem" Classes.active="{Binding IsSettingsActive}" HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Left"
|
HorizontalContentAlignment="Left"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ public partial class MainWindow : Window
|
|||||||
KeyDown += OnWindowKeyDown;
|
KeyDown += OnWindowKeyDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnOpenUntisPeriods(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await new OpenUntisPeriodsDialog().ShowDialog(this);
|
||||||
|
}
|
||||||
|
|
||||||
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
if (DataContext is not MainWindowViewModel vm) return;
|
if (DataContext is not MainWindowViewModel vm) return;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using LehrerApp.Desktop.Services;
|
||||||
|
using LehrerApp.WebUntis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace LehrerApp.Desktop.Views;
|
||||||
|
|
||||||
|
public sealed class OpenUntisPeriodsDialog : Window
|
||||||
|
{
|
||||||
|
private readonly WebUntisIntegrationService _service = App.Services.GetRequiredService<WebUntisIntegrationService>();
|
||||||
|
private readonly WebUntisSettingsService _settings = App.Services.GetRequiredService<WebUntisSettingsService>();
|
||||||
|
private readonly CancellationTokenSource _closed = new();
|
||||||
|
private readonly ComboBox _mode = new() { ItemsSource = new[] { "Mein Unterricht", "Meine Klasse" }, SelectedIndex = 0 };
|
||||||
|
private readonly ComboBox _element = new() { MinWidth = 150 };
|
||||||
|
private readonly DatePicker _start = new();
|
||||||
|
private readonly DatePicker _end = new() { SelectedDate = DateTimeOffset.Now };
|
||||||
|
private readonly Button _load = new() { Content = "Abrufen" };
|
||||||
|
private readonly Button _schoolYear = new() { Content = "Seit Schuljahresbeginn" };
|
||||||
|
private readonly TextBlock _status = new() { TextWrapping = Avalonia.Media.TextWrapping.Wrap };
|
||||||
|
private readonly StackPanel _rows = new() { Spacing = 12 };
|
||||||
|
private readonly StackPanel _filters = new() { Spacing = 10, IsEnabled = false };
|
||||||
|
private UntisOpenPeriodsMeta? _meta;
|
||||||
|
private int _yearId;
|
||||||
|
|
||||||
|
public OpenUntisPeriodsDialog()
|
||||||
|
{
|
||||||
|
Title = "Offene WebUntis-Stunden";
|
||||||
|
Width = 850; Height = 650; MinWidth = 600; MinHeight = 450;
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterOwner;
|
||||||
|
var root = new DockPanel { Margin = new Thickness(20) };
|
||||||
|
var header = new StackPanel { Spacing = 12 };
|
||||||
|
header.Children.Add(new TextBlock { Text = Title, FontSize = 22 });
|
||||||
|
var selection = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 12 };
|
||||||
|
selection.Children.Add(_mode); selection.Children.Add(_element);
|
||||||
|
_filters.Children.Add(selection);
|
||||||
|
var dates = new WrapPanel { Orientation = Avalonia.Layout.Orientation.Horizontal };
|
||||||
|
dates.Children.Add(new TextBlock { Text = "Von ", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center });
|
||||||
|
dates.Children.Add(_start);
|
||||||
|
dates.Children.Add(new TextBlock { Text = " bis ", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center });
|
||||||
|
dates.Children.Add(_end);
|
||||||
|
_filters.Children.Add(dates);
|
||||||
|
var actions = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal, Spacing = 12 };
|
||||||
|
actions.Children.Add(_schoolYear); actions.Children.Add(_load);
|
||||||
|
_filters.Children.Add(actions);
|
||||||
|
header.Children.Add(_filters); header.Children.Add(_status);
|
||||||
|
DockPanel.SetDock(header, Dock.Top); root.Children.Add(header);
|
||||||
|
root.Children.Add(new ScrollViewer { Content = _rows, Margin = new Thickness(0, 20, 0, 0) });
|
||||||
|
Content = root;
|
||||||
|
_mode.SelectionChanged += (_, _) => SelectMode();
|
||||||
|
_element.SelectionChanged += (_, _) => ClearResults();
|
||||||
|
_start.SelectedDateChanged += (_, _) => ClearResults();
|
||||||
|
_end.SelectedDateChanged += (_, _) => ClearResults();
|
||||||
|
_schoolYear.Click += (_, _) => { if (_meta is not null) _start.SelectedDate = new DateTimeOffset(_meta.SchoolYearStart.ToDateTime(TimeOnly.MinValue)); };
|
||||||
|
_load.Click += async (_, _) => await LoadAsync();
|
||||||
|
Opened += async (_, _) => await InitializeAsync();
|
||||||
|
Closed += (_, _) => _closed.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearResults() { _rows.Children.Clear(); _status.Text = "Auswahl festlegen und Abrufen wählen."; }
|
||||||
|
|
||||||
|
private void SelectMode()
|
||||||
|
{
|
||||||
|
if (_meta is null) return;
|
||||||
|
var own = _mode.SelectedIndex == 0;
|
||||||
|
var elements = own ? _meta.Teachers : _meta.Classes;
|
||||||
|
_element.ItemsSource = elements;
|
||||||
|
var preferred = own ? _settings.TeacherUntisId ?? _meta.OwnTeacherId
|
||||||
|
: _settings.HomeroomClassUntisId ?? _meta.MyClassIds.Cast<int?>().FirstOrDefault();
|
||||||
|
_element.SelectedItem = elements.FirstOrDefault(x => x.Id == preferred)
|
||||||
|
?? (!own ? elements.FirstOrDefault(x => _meta.MyClassIds.Contains(x.Id)) : null);
|
||||||
|
ClearResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_status.Text = "WebUntis-Auswahl wird geladen …";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var date = today.Year * 10000 + today.Month * 100 + today.Day;
|
||||||
|
var years = await _service.GetSchoolYearsAsync(_closed.Token);
|
||||||
|
var year = years.FirstOrDefault(x => x.StartDate <= date && date <= x.EndDate)
|
||||||
|
?? throw new InvalidDataException("Kein aktuelles WebUntis-Schuljahr gefunden.");
|
||||||
|
_yearId = year.UntisId;
|
||||||
|
_meta = await _service.GetOpenPeriodsMetaAsync(_yearId, _closed.Token);
|
||||||
|
_start.SelectedDate = new DateTimeOffset(_meta.SchoolYearStart.ToDateTime(TimeOnly.MinValue));
|
||||||
|
SelectMode();
|
||||||
|
_filters.IsEnabled = true;
|
||||||
|
if (_element.SelectedItem is not null) await LoadAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_closed.IsCancellationRequested) { }
|
||||||
|
catch (Exception ex) { _status.Text = $"Abruf fehlgeschlagen: {ex.Message}"; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
_rows.Children.Clear();
|
||||||
|
if (_element.SelectedItem is not UntisOpenElement element || _start.SelectedDate is null || _end.SelectedDate is null)
|
||||||
|
{ _status.Text = "Bitte Lehrkraft oder Klasse und Zeitraum auswählen."; return; }
|
||||||
|
var start = DateOnly.FromDateTime(_start.SelectedDate.Value.DateTime);
|
||||||
|
var end = DateOnly.FromDateTime(_end.SelectedDate.Value.DateTime);
|
||||||
|
if (start > end || start < _meta!.SchoolYearStart || end > _meta.SchoolYearEnd)
|
||||||
|
{ _status.Text = "Bitte einen gültigen Zeitraum innerhalb des Schuljahres auswählen."; return; }
|
||||||
|
_filters.IsEnabled = false;
|
||||||
|
_status.Text = "Offene Stunden werden geladen …";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var own = _mode.SelectedIndex == 0;
|
||||||
|
var periods = await _service.GetOpenPeriodsAsync(_yearId, own ? element.Id : null,
|
||||||
|
own ? null : element.Id, start, end, _closed.Token);
|
||||||
|
foreach (var period in periods)
|
||||||
|
_rows.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"{period.When}\n{period.Classes} · {period.Subject} · {period.Teachers}\n{period.Missing}",
|
||||||
|
TextWrapping = Avalonia.Media.TextWrapping.Wrap
|
||||||
|
});
|
||||||
|
_status.Text = periods.Count == 0 ? "Keine offenen Stunden im gewählten Zeitraum."
|
||||||
|
: $"{periods.Count} offene Stunde(n) · Stand {DateTime.Now:HH:mm}";
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (_closed.IsCancellationRequested) { }
|
||||||
|
catch (Exception ex) { _status.Text = $"Abruf fehlgeschlagen: {ex.Message}"; }
|
||||||
|
finally { _filters.IsEnabled = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
using LehrerApp.WebUntis;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.WebUntis.Tests;
|
||||||
|
|
||||||
|
public sealed class WebUntisOpenPeriodsTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true, false, true, "", true, false)]
|
||||||
|
[InlineData(false, true, true, "Bruchrechnung", false, true)]
|
||||||
|
[InlineData(false, true, true, " ", true, true)]
|
||||||
|
[InlineData(false, false, false, "", false, false)]
|
||||||
|
public void MissingFlagsRespectRequirementsAndIgnoreTopicId(bool checkedAbs, bool neededAbs,
|
||||||
|
bool neededTopic, string topic, bool missingTopic, bool missingAbs)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.SerializeToElement(new
|
||||||
|
{
|
||||||
|
periods = new[] { new {
|
||||||
|
period = new { id = 123, hr = 2,
|
||||||
|
dtRange = new { start = "2026-09-08T08:35:00", end = "2026-09-08T09:20:00" },
|
||||||
|
classes = new[] { new { el = new { id = 1, nameShort = "7a" } } },
|
||||||
|
teachers = new[] { new { el = new { id = 2, nameShort = "ABC" } } },
|
||||||
|
subject = new { el = new { name = "Mat" } } },
|
||||||
|
absChecked = checkedAbs, absCheckNeeded = neededAbs, topicNeeded = neededTopic,
|
||||||
|
topicShort = topic, topicId = 999 } }, total = 80
|
||||||
|
});
|
||||||
|
var row = Assert.Single(WebUntisClient.ParseOpenPeriods(json));
|
||||||
|
Assert.Equal(missingTopic, row.TopicMissing);
|
||||||
|
Assert.Equal(missingAbs, row.AttendanceMissing);
|
||||||
|
Assert.Equal(DateTimeKind.Unspecified, row.Start.Kind);
|
||||||
|
Assert.Equal("7a", row.Classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(true)]
|
||||||
|
[InlineData(false)]
|
||||||
|
public async Task RequestUsesExclusiveFilterAndSessionToken(bool teacher)
|
||||||
|
{
|
||||||
|
var handler = new Handler();
|
||||||
|
await using var client = new WebUntisClient(new HttpClient(handler), new WebUntisOptions
|
||||||
|
{ School = "test", Host = "test.webuntis.com", Username = "test", Password = "test" });
|
||||||
|
var meta = await client.GetOpenPeriodsMetaAsync(22, CancellationToken.None);
|
||||||
|
Assert.Equal(2, meta.OwnTeacherId);
|
||||||
|
Assert.Equal(1, Assert.Single(meta.MyClassIds));
|
||||||
|
var rows = await client.GetOpenPeriodsAsync(22, teacher ? 2 : null, teacher ? null : 1,
|
||||||
|
new DateOnly(2026, 8, 13), new DateOnly(2026, 9, 8), CancellationToken.None);
|
||||||
|
Assert.Empty(rows);
|
||||||
|
using var body = JsonDocument.Parse(handler.Body!);
|
||||||
|
Assert.Equal(teacher ? 2 : 1, body.RootElement.GetProperty(teacher ? "teacherId" : "classId").GetInt32());
|
||||||
|
Assert.False(body.RootElement.TryGetProperty(teacher ? "classId" : "teacherId", out _));
|
||||||
|
Assert.Equal("TOPIC_OR_ABSENCE_OPEN", body.RootElement.GetProperty("filter").GetString());
|
||||||
|
Assert.Equal("2026-08-13", body.RootElement.GetProperty("dateRange").GetProperty("start").GetString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class Handler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
public string? Body { get; private set; }
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken token)
|
||||||
|
{
|
||||||
|
var path = request.RequestUri!.AbsolutePath;
|
||||||
|
string response;
|
||||||
|
if (path.EndsWith("jsonrpc.do"))
|
||||||
|
response = "{\"result\":{\"sessionId\":\"test-session\",\"personId\":2}}";
|
||||||
|
else if (path.EndsWith("token/new"))
|
||||||
|
{
|
||||||
|
Assert.Contains("test-session", request.Headers.GetValues("Cookie").Single());
|
||||||
|
response = "test.token.value";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal("Bearer", request.Headers.Authorization!.Scheme);
|
||||||
|
Assert.Equal("test.token.value", request.Headers.Authorization.Parameter);
|
||||||
|
Assert.Equal("22", request.Headers.GetValues("x-webuntis-api-school-year-id").Single());
|
||||||
|
if (path.EndsWith("/meta"))
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpMethod.Get, request.Method);
|
||||||
|
response = """
|
||||||
|
{"teachers":[{"el":{"id":2,"nameShort":"ABC"}}],
|
||||||
|
"classes":[{"el":{"id":1,"nameShort":"7a"}}],"myClassIds":[1],
|
||||||
|
"schoolYear":{"start":"2026-08-13","end":"2027-07-07"}}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.Equal(HttpMethod.Post, request.Method);
|
||||||
|
Body = await request.Content!.ReadAsStringAsync(token);
|
||||||
|
response = "{\"periods\":[],\"total\":80}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace LehrerApp.WebUntis;
|
namespace LehrerApp.WebUntis;
|
||||||
|
|
||||||
public sealed class WebUntisClient : IAsyncDisposable
|
public sealed partial class WebUntisClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
private static readonly UTF8Encoding StrictUtf8 = new(false, true);
|
private static readonly UTF8Encoding StrictUtf8 = new(false, true);
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LehrerApp.WebUntis;
|
||||||
|
|
||||||
|
public sealed record UntisOpenElement(int Id, string Name)
|
||||||
|
{
|
||||||
|
public override string ToString() => Name;
|
||||||
|
}
|
||||||
|
public sealed record UntisOpenPeriodsMeta(
|
||||||
|
IReadOnlyList<UntisOpenElement> Teachers, IReadOnlyList<UntisOpenElement> Classes,
|
||||||
|
IReadOnlyList<int> MyClassIds, DateOnly SchoolYearStart, DateOnly SchoolYearEnd,
|
||||||
|
int? OwnTeacherId);
|
||||||
|
public sealed record UntisOpenPeriod(int Id, DateTime Start, DateTime End, int Hour,
|
||||||
|
string Classes, string Teachers, string Subject, bool TopicMissing, bool AttendanceMissing)
|
||||||
|
{
|
||||||
|
public string When => $"{Start:dd.MM.yyyy} · {Hour}. Stunde · {Start:HH:mm}–{End:HH:mm}";
|
||||||
|
public string Missing => (TopicMissing, AttendanceMissing) switch
|
||||||
|
{
|
||||||
|
(true, true) => "Thema und Anwesenheitskontrolle fehlen",
|
||||||
|
(true, false) => "Thema fehlt",
|
||||||
|
(false, true) => "Anwesenheitskontrolle fehlt",
|
||||||
|
_ => "Von WebUntis als offen gemeldet"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class WebUntisClient
|
||||||
|
{
|
||||||
|
private const string OpenPeriodsPath = "/WebUntis/api/rest/view/v1/classreg/open-periods";
|
||||||
|
|
||||||
|
public Task<UntisOpenPeriodsMeta> GetOpenPeriodsMetaAsync(int schoolYearId, CancellationToken token) =>
|
||||||
|
WithSessionAsync(async session =>
|
||||||
|
{
|
||||||
|
var json = await OpenPeriodsRequestAsync(session, schoolYearId, null, token);
|
||||||
|
var year = json.GetProperty("schoolYear");
|
||||||
|
return new UntisOpenPeriodsMeta(OpenElements(json.GetProperty("teachers")),
|
||||||
|
OpenElements(json.GetProperty("classes")),
|
||||||
|
json.GetProperty("myClassIds").EnumerateArray().Select(x => x.GetInt32()).ToArray(),
|
||||||
|
year.GetProperty("start").GetDateOnly(), year.GetProperty("end").GetDateOnly(),
|
||||||
|
_myTeacherUntisId);
|
||||||
|
}, token);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<UntisOpenPeriod>> GetOpenPeriodsAsync(int schoolYearId, int? teacherId,
|
||||||
|
int? classId, DateOnly start, DateOnly end, CancellationToken token)
|
||||||
|
{
|
||||||
|
if ((teacherId is null) == (classId is null) || teacherId is <= 0 || classId is <= 0)
|
||||||
|
throw new ArgumentException("Genau eine gültige Lehrer- oder Klassen-ID ist erforderlich.");
|
||||||
|
if (start > end) throw new ArgumentException("Der Beginn muss vor dem Ende liegen.");
|
||||||
|
var body = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
[teacherId.HasValue ? "teacherId" : "classId"] = teacherId ?? classId!.Value,
|
||||||
|
["filter"] = "TOPIC_OR_ABSENCE_OPEN",
|
||||||
|
["dateRange"] = new { start = start.ToString("yyyy-MM-dd"), end = end.ToString("yyyy-MM-dd") }
|
||||||
|
};
|
||||||
|
return WithSessionAsync(async session => ParseOpenPeriods(
|
||||||
|
await OpenPeriodsRequestAsync(session, schoolYearId, body, token)), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<UntisOpenPeriod> ParseOpenPeriods(JsonElement json) =>
|
||||||
|
json.GetProperty("periods").EnumerateArray().Select(row =>
|
||||||
|
{
|
||||||
|
var p = row.GetProperty("period");
|
||||||
|
var range = p.GetProperty("dtRange");
|
||||||
|
return new UntisOpenPeriod(p.GetProperty("id").GetInt32(),
|
||||||
|
range.GetProperty("start").GetDateTime(), range.GetProperty("end").GetDateTime(),
|
||||||
|
p.GetProperty("hr").GetInt32(),
|
||||||
|
string.Join(", ", OpenElements(p.GetProperty("classes")).Select(x => x.Name)),
|
||||||
|
string.Join(", ", OpenElements(p.GetProperty("teachers")).Select(x => x.Name)),
|
||||||
|
p.GetProperty("subject").GetProperty("el").GetProperty("name").GetString() ?? "",
|
||||||
|
row.GetProperty("topicNeeded").GetBoolean() &&
|
||||||
|
string.IsNullOrWhiteSpace(row.GetProperty("topicShort").GetString()),
|
||||||
|
row.GetProperty("absCheckNeeded").GetBoolean() && !row.GetProperty("absChecked").GetBoolean());
|
||||||
|
}).OrderBy(x => x.Start).ToArray();
|
||||||
|
|
||||||
|
private static IReadOnlyList<UntisOpenElement> OpenElements(JsonElement array) => array.EnumerateArray()
|
||||||
|
.Select(x => x.GetProperty("el"))
|
||||||
|
.Select(x => new UntisOpenElement(x.GetProperty("id").GetInt32(),
|
||||||
|
x.GetProperty("nameShort").GetString() ?? x.GetProperty("name").GetString() ?? ""))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
private async Task<JsonElement> OpenPeriodsRequestAsync(string session, int schoolYearId,
|
||||||
|
object? body, CancellationToken token)
|
||||||
|
{
|
||||||
|
// The REST UI uses a bearer token issued for the existing WebUntis session.
|
||||||
|
using var tokenRequest = ReportRequest($"https://{GetConfiguration().Host}/WebUntis/api/token/new", session, true);
|
||||||
|
using var tokenResponse = await SendAsync(tokenRequest, TimeSpan.FromSeconds(20), token);
|
||||||
|
if (!tokenResponse.IsSuccessStatusCode)
|
||||||
|
throw new WebUntisException($"WebUntis-Token konnte nicht abgerufen werden: HTTP {(int)tokenResponse.StatusCode}.");
|
||||||
|
var bearer = (await tokenResponse.Content.ReadAsStringAsync(token)).Trim();
|
||||||
|
if (bearer.StartsWith('"')) bearer = JsonSerializer.Deserialize<string>(bearer) ?? "";
|
||||||
|
if (string.IsNullOrWhiteSpace(bearer) ||
|
||||||
|
bearer.Any(c => !char.IsAsciiLetterOrDigit(c) && c is not '-' and not '_' and not '.'))
|
||||||
|
throw new WebUntisException("WebUntis hat keinen gültigen Zugriffstoken geliefert.");
|
||||||
|
using var request = ReportRequest($"https://{GetConfiguration().Host}{OpenPeriodsPath}{(body is null ? "/meta" : "")}", session, true);
|
||||||
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||||||
|
request.Headers.Add("x-webuntis-api-school-year-id", schoolYearId.ToString());
|
||||||
|
if (body is not null)
|
||||||
|
{
|
||||||
|
request.Method = HttpMethod.Post;
|
||||||
|
request.Content = JsonContent.Create(body, options: JsonOptions);
|
||||||
|
}
|
||||||
|
using var response = await SendAsync(request, TimeSpan.FromSeconds(30), token);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
throw new WebUntisException($"Offene Stunden konnten nicht abgerufen werden: HTTP {(int)response.StatusCode}.");
|
||||||
|
return await ReadJsonAsync(response, token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class OpenPeriodJsonDates
|
||||||
|
{
|
||||||
|
public static DateOnly GetDateOnly(this JsonElement value) => DateOnly.ParseExact(value.GetString()!, "yyyy-MM-dd");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user