This commit is contained in:
1 parent
30f0fee0b2
commit
45b7a2b7e6
2 files changed
+98
No files matched your search
@@ -61,6 +61,75 @@ public sealed class WebUntisRestSyncProcessorTests
|
||||
Assert.Empty(exams.GetAll());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_DoppeltAngelegteArbeitDurchGeraeteSync_WirdAufEinenDatensatzReduziert()
|
||||
{
|
||||
var group = new LearningGroup { Id = Guid.NewGuid(), Name = "Mat_E" };
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Tuesday, StartTime = new TimeOnly(9, 40), Summary = "Mat_E",
|
||||
ClassToken = "10a", Kind = SubstitutionKind.Lesson, GroupId = group.Id,
|
||||
PeriodNumber = 3, CoveredPeriods = [3, 4], Confirmed = true,
|
||||
});
|
||||
var exam = Entry([2002979, 2002982], "2026-09-29T09:40", "2026-09-29T11:10",
|
||||
"EXAM", "REGULAR", "Mat_E", "10a", "Arbeit 1. Mathearbeit");
|
||||
var externalId = WebUntisRestSyncProcessor.ExamPrefix + "2002979-2002982";
|
||||
// Zwei Geräte haben dieselbe WebUntis-Arbeit unabhängig voneinander (vor dem gegenseitigen
|
||||
// Sync) mit derselben ExternalId, aber unterschiedlicher Id angelegt.
|
||||
var olderDuplicate = new Exam
|
||||
{
|
||||
GroupId = group.Id, Date = new DateOnly(2026, 9, 29), Title = "Arbeit 1. Mathearbeit",
|
||||
ExternalId = externalId, CreatedAt = new DateTime(2026, 9, 22, 8, 0, 0, DateTimeKind.Utc),
|
||||
};
|
||||
var newerDuplicate = new Exam
|
||||
{
|
||||
GroupId = group.Id, Date = new DateOnly(2026, 9, 29), Title = "Arbeit 1. Mathearbeit",
|
||||
ExternalId = externalId, CreatedAt = new DateTime(2026, 9, 22, 9, 0, 0, DateTimeKind.Utc),
|
||||
};
|
||||
var exams = new RecordingExams([olderDuplicate, newerDuplicate]);
|
||||
var processor = BuildProcessor(mappings, new FakeSubstitutionEntries(), exams, [group]);
|
||||
|
||||
var result = processor.Process([exam], Monday, Monday.AddDays(13), Monday.AddDays(27));
|
||||
|
||||
var remaining = Assert.Single(exams.GetAll());
|
||||
Assert.Equal(olderDuplicate.Id, remaining.Id);
|
||||
Assert.Equal(1, result.ExamChanges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_DupliziertMitBereitsBenoteterKlausur_LoeschtNurDenNochPlanentenDatensatz()
|
||||
{
|
||||
var group = new LearningGroup { Id = Guid.NewGuid(), Name = "Mat_E" };
|
||||
var mappings = new FakeUntisSlotMappings();
|
||||
mappings.Add(new UntisSlotMapping
|
||||
{
|
||||
Weekday = DayOfWeek.Tuesday, StartTime = new TimeOnly(9, 40), Summary = "Mat_E",
|
||||
ClassToken = "10a", Kind = SubstitutionKind.Lesson, GroupId = group.Id,
|
||||
PeriodNumber = 3, CoveredPeriods = [3, 4], Confirmed = true,
|
||||
});
|
||||
var exam = Entry([2002979, 2002982], "2026-09-29T09:40", "2026-09-29T11:10",
|
||||
"EXAM", "REGULAR", "Mat_E", "10a", "Arbeit 1. Mathearbeit");
|
||||
var externalId = WebUntisRestSyncProcessor.ExamPrefix + "2002979-2002982";
|
||||
var gradedDuplicate = new Exam
|
||||
{
|
||||
GroupId = group.Id, Date = new DateOnly(2026, 9, 29), Title = "Arbeit 1. Mathearbeit",
|
||||
ExternalId = externalId, Status = ExamStatus.Graded,
|
||||
};
|
||||
var plannedDuplicate = new Exam
|
||||
{
|
||||
GroupId = group.Id, Date = new DateOnly(2026, 9, 29), Title = "Arbeit 1. Mathearbeit",
|
||||
ExternalId = externalId,
|
||||
};
|
||||
var exams = new RecordingExams([gradedDuplicate, plannedDuplicate]);
|
||||
var processor = BuildProcessor(mappings, new FakeSubstitutionEntries(), exams, [group]);
|
||||
|
||||
processor.Process([exam], Monday, Monday.AddDays(13), Monday.AddDays(27));
|
||||
|
||||
var remaining = Assert.Single(exams.GetAll());
|
||||
Assert.Equal(gradedDuplicate.Id, remaining.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Process_ManuelleArbeitAmSelbenTag_WirdNichtDupliziertOderGeloescht()
|
||||
{
|
||||
|
||||
@@ -138,6 +138,7 @@ public sealed class WebUntisRestSyncProcessor(
|
||||
var changes = 0;
|
||||
var desiredIds = new HashSet<string>();
|
||||
var allExams = exams.GetAll();
|
||||
changes += DeduplicateExams(allExams);
|
||||
foreach (var entry in examEntries)
|
||||
{
|
||||
var periods = ResolveCoveredPeriods(entry.Start, entry.End);
|
||||
@@ -185,6 +186,34 @@ public sealed class WebUntisRestSyncProcessor(
|
||||
return changes;
|
||||
}
|
||||
|
||||
/// <summary>Räumt Klausuren auf, die zwei Geräte unabhängig voneinander mit derselben
|
||||
/// <see cref="Exam.ExternalId"/> aber unterschiedlicher <see cref="Exam.Id"/> angelegt haben,
|
||||
/// weil jedes Gerät den anderen Datensatz zum Zeitpunkt seines eigenen Polls noch nicht
|
||||
/// gesynct hatte (Race Condition zwischen WebUntis-REST-Abgleich und Geräte-Sync). Pro Gruppe
|
||||
/// gleicher ExternalId gewinnt der am weitesten bearbeitete Datensatz (nicht mehr "Planned"),
|
||||
/// sonst der zuerst angelegte; bereits bearbeitete Duplikate werden nie automatisch gelöscht,
|
||||
/// um keine eingetragenen Ergebnisse zu verlieren - dieselbe Zurückhaltung wie beim
|
||||
/// Stale-Abbau unten (nur `Status == Planned` wird automatisch entfernt).</summary>
|
||||
private int DeduplicateExams(List<Exam> allExams)
|
||||
{
|
||||
var changes = 0;
|
||||
foreach (var group in allExams.Where(exam => exam.ExternalId is not null)
|
||||
.GroupBy(exam => exam.ExternalId)
|
||||
.Where(group => group.Count() > 1))
|
||||
{
|
||||
var ordered = group.OrderBy(exam => exam.Status == ExamStatus.Planned ? 1 : 0)
|
||||
.ThenBy(exam => exam.CreatedAt).ToList();
|
||||
foreach (var duplicate in ordered.Skip(1))
|
||||
{
|
||||
if (duplicate.Status != ExamStatus.Planned) continue;
|
||||
exams.Delete(duplicate.Id);
|
||||
allExams.Remove(duplicate);
|
||||
changes++;
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
private UntisSlotMapping? FindMapping(List<UntisSlotMapping> candidates, DayOfWeek weekday, TimeOnly start,
|
||||
List<int> periods, UntisTimetableGridEntryDto entry)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user