Förderarbeit als dritte Folgearbeits-Art ergänzen

Neben Wiederholung und Nachschreibarbeit gibt es im Arbeitsdialog die Art
Förderarbeit (Exam.IsSupportWork). Sie darf am Tag der ersten Arbeit liegen,
übernimmt dessen Datum als Vorschlag und ersetzt wie eine Nachschreibarbeit
nur fehlende Noten.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5.5 committed 2026-10-22 20:55:27 +02:00
1 parent 10921deb34
commit 651c9cddf8
14 files changed
+214 -32

No files matched your search

+11
View File
@@ -32,6 +32,10 @@ public class Exam
/// erste Arbeit (<see cref="RepeatOfExamId"/>) versäumt haben. Anders als eine Wiederholung
/// wird sie nur von diesen Schülern geschrieben und ersetzt deren fehlende Note.</summary>
public bool IsMakeUp { get; set; }
/// <summary>Förderarbeit: inhaltlich eigene Arbeit, die einzelne Schüler statt der verknüpften
/// ersten Arbeit schreiben — typischerweise am selben Tag, daher darf sie am Datum der ersten
/// Arbeit liegen. Zählt wie eine Nachschreibarbeit (ersetzt nur eine fehlende Note).</summary>
public bool IsSupportWork { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -78,6 +82,13 @@ public enum RepeatGradeRule { Better, Repeat, Original }
/// <summary>Reihenfolge ist persistiert (LiteDB speichert den Enum-Wert) — nur hinten anhängen.</summary>
public enum ExamKind { Klausur, Test, KontrollierteHausaufgabe, Sonstiges }
public static class ExamFollowUpExtensions
{
/// <summary>Nachschreib- und Förderarbeiten sind inhaltlich eigene Arbeiten nur für einzelne
/// Schüler: ihre Note ersetzt eine fehlende Note der ersten Arbeit, verdrängt sie aber nie.</summary>
public static bool ReplacesMissingGrade(this Exam exam) => exam.IsMakeUp || exam.IsSupportWork;
}
public static class ExamKindExtensions
{
public static bool CountsAsWritten(this ExamKind kind) => kind == ExamKind.Klausur;
@@ -37,12 +37,12 @@ public static class ExamCoverSheetService
/// <summary>Wer eine Arbeit mitschreibt — eine Regel für Punkteeingabe und Deckblätter.</summary>
public static class ExamParticipantService
{
/// <summary>Bei einer Nachschreibarbeit: Schüler, die die erste Arbeit bereits mit Note
/// geschrieben haben. Sonst leer.</summary>
/// <summary>Bei einer Nachschreib- oder Förderarbeit: Schüler, die die erste Arbeit bereits mit
/// Note geschrieben haben. Sonst leer.</summary>
public static HashSet<Guid> WroteOriginal(Exam exam, Func<Guid, List<ExamResult>> resultsByExam)
{
var wroteOriginal = new HashSet<Guid>();
if (exam.IsMakeUp && exam.RepeatOfExamId is { } originalId)
if (exam.ReplacesMissingGrade() && exam.RepeatOfExamId is { } originalId)
foreach (var r in resultsByExam(originalId).Where(r => !r.Absent && r.Grade is not null))
wroteOriginal.Add(r.StudentId);
return wroteOriginal;
@@ -54,7 +54,7 @@ public static class ExamParticipantService
// Niveau-Klausur: nur Schüler mit passendem Niveau. Klausuren ohne Niveau-Zuordnung gelten
// weiterhin für die ganze Gruppe.
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) return false;
// Nachschreibarbeit: wer die erste Arbeit schon mit Note geschrieben hat, taucht nicht auf —
// Nachschreib-/Förderarbeit: wer die erste Arbeit schon mit Note geschrieben hat, taucht nicht auf —
// außer es gibt hier bereits ein Ergebnis (nichts verstecken, was schon eingetragen wurde).
return hasResult || !wroteOriginal;
}
@@ -45,10 +45,10 @@ public static class ExamScoringService
AppliesTo(task, variant) && index < points.Count ? points[index] : 0).Sum();
/// <summary>
/// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreibarbeit)
/// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreib-/Förderarbeit)
/// genau eine Note. Andere Arbeiten bleiben erhalten. Fehlt die erste Arbeit (Schüler war
/// abwesend), zählt die Nachschreibarbeit allein; hat der Schüler beide geschrieben, zählt bei
/// einer Nachschreibarbeit immer die erste — sie ersetzt nur eine fehlende Note.
/// abwesend), zählt die Nachschreib-/Förderarbeit allein; hat der Schüler beide geschrieben,
/// zählt immer die erste — sie ersetzen nur eine fehlende Note.
/// </summary>
public static List<string> EffectiveGrades(IEnumerable<(Exam Exam, string Grade)> entries,
GradingSystem system)
@@ -72,7 +72,7 @@ public static class ExamScoringService
var current = ordered[0].Grade;
foreach (var next in ordered.Skip(1))
current = Choose(current, next.Grade,
next.Exam.IsMakeUp ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system);
next.Exam.ReplacesMissingGrade() ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system);
result.Add(current);
}
return result;
@@ -84,4 +84,81 @@ public sealed class ExamDialogViewModelTests
Assert.Equal(10, result.TotalPoints);
Assert.Equal("1", result.Grade);
}
// ── Förderarbeit ─────────────────────────────────────────────────────────
private static (ExamDialogViewModel Vm, Exam Original) BuildFollowUpViewModel(DateOnly originalDate)
{
var original = new Exam { GroupId = Guid.NewGuid(), Title = "Atombau", Date = originalDate };
var vm = new ExamDialogViewModel(new FakeExams([original]), new FakeCompetencyDomains(),
new FakeGradingKeyTemplates(), new GradingService(), original.GroupId, null, 9,
GradingSystem.Grades1To6, "Chemie", false, null, null);
vm.Title = "Atombau (Förder)";
vm.Tasks.Add(new ExamTaskEditItem("A1", 10, 1, [], []) { OnChanged = () => { } });
vm.Tasks[0].MaxPoints = 10;
vm.SelectedOriginalExam = original;
return (vm, original);
}
[Fact]
public void Foerderarbeit_UebernimmtDatumDerErstenArbeitUndDarfAmSelbenTagLiegen()
{
var (vm, original) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
vm.FollowUpKindName = "Förderarbeit";
Assert.Equal("14.10.2026", vm.DateText);
vm.SaveCommand.Execute(null);
Assert.Equal("", vm.RepeatError);
Assert.NotNull(vm.Result);
Assert.True(vm.Result!.IsSupportWork);
Assert.False(vm.Result.IsMakeUp);
Assert.Equal(original.Id, vm.Result.RepeatOfExamId);
Assert.Equal(original.Date, vm.Result.Date);
}
[Fact]
public void Foerderarbeit_VorDerErstenArbeit_WirdAbgelehnt()
{
var (vm, _) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
vm.FollowUpKindName = "Förderarbeit";
vm.DateText = "13.10.2026";
vm.SaveCommand.Execute(null);
Assert.Contains("Förderarbeit", vm.RepeatError);
Assert.Null(vm.Result);
}
[Fact]
public void Nachschreibarbeit_AmSelbenTag_BleibtAbgelehnt()
{
var (vm, _) = BuildFollowUpViewModel(new DateOnly(2026, 10, 14));
vm.FollowUpKindName = "Nachschreibarbeit";
vm.DateText = "14.10.2026";
vm.SaveCommand.Execute(null);
Assert.Contains("Nachschreibarbeit", vm.RepeatError);
Assert.Null(vm.Result);
}
[Fact]
public void Foerderarbeit_WirdBeimBearbeitenWiederhergestelltOhneDatumZuUeberschreiben()
{
var original = new Exam { GroupId = Guid.NewGuid(), Title = "Atombau", Date = new DateOnly(2026, 10, 14) };
var support = new Exam
{
GroupId = original.GroupId, Title = "Förder", Date = new DateOnly(2026, 10, 16),
RepeatOfExamId = original.Id, IsSupportWork = true,
};
var vm = new ExamDialogViewModel(new FakeExams([original, support]), new FakeCompetencyDomains(),
new FakeGradingKeyTemplates(), new GradingService(), original.GroupId, null, 9,
GradingSystem.Grades1To6, "Chemie", false, support, null);
Assert.True(vm.IsSupportWork);
Assert.Equal("16.10.2026", vm.DateText);
Assert.False(vm.ShowRepeatRule);
}
}
@@ -22,7 +22,8 @@ public static class GradeReportDrawingBuilder
if (exam.RepeatOfExamId is null) return new DrawingValue([], 0);
if (original is null || original.Id != exam.RepeatOfExamId || original.GroupId != exam.GroupId)
throw new InvalidDataException("Die verknüpfte erste Arbeit ist nicht verfügbar.");
var rule = exam.IsMakeUp ? "Die Nachschreibarbeit ersetzt die versäumte erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
var rule = exam.IsSupportWork ? "Die Förderarbeit ersetzt die erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
: exam.IsMakeUp ? "Die Nachschreibarbeit ersetzt die versäumte erste Arbeit. Liegen beide Noten vor, zählt die erste Arbeit."
: exam.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Für die Bewertung zählt die Note der Wiederholungsarbeit.",
@@ -35,7 +36,7 @@ public static class GradeReportDrawingBuilder
ExamScoringService.Maximum(original.Tasks, originalResult.Variant), original.GradingKey);
var firstGrade = originalResult is { Absent: true } ? "abwesend" : Value(grade);
return Text(template, ReportTextDrawing.RepeatInfo,
exam.IsMakeUp ? "Nachschreibarbeit" : "Wiederholungsarbeit",
exam.IsSupportWork ? "Förderarbeit" : exam.IsMakeUp ? "Nachschreibarbeit" : "Wiederholungsarbeit",
[$"Erste Arbeit: {original.Title} vom {original.Date:dd.MM.yyyy}",
$"Note der ersten Arbeit: {firstGrade}", rule]);
}
@@ -42,7 +42,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
public ObservableCollection<RepeatComparisonItem> RepeatComparison { get; } = [];
public bool HasRepeatComparison => RepeatComparison.Count > 0;
public string RepeatComparisonTitle { get; private set; } = "";
/// "Wiederholung" bzw. "Nachschreibarbeit" — Beschriftung der Folgearbeit im Vergleich.
/// "Wiederholung", "Nachschreibarbeit" bzw. "Förderarbeit" — Beschriftung der Folgearbeit im Vergleich.
public string FollowUpLabel { get; private set; } = "Wiederholung";
public string FollowUpComparisonHeading => $"Vergleich mit der {FollowUpLabel}";
public ObservableCollection<GradingKeyEntryEditItem> GradingKeyEntries { get; } = [];
@@ -72,8 +72,8 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
: _exams.GetByGroup(_exam.GroupId).Where(e => e.RepeatOfExamId == _exam.Id)
.OrderBy(e => e.Date).FirstOrDefault();
if (repeat is null) return;
FollowUpLabel = repeat.IsMakeUp ? "Nachschreibarbeit" : "Wiederholung";
var rule = repeat.IsMakeUp ? "ersetzt fehlende Note" : repeat.RepeatGradeRule switch
FollowUpLabel = repeat.IsSupportWork ? "Förderarbeit" : repeat.IsMakeUp ? "Nachschreibarbeit" : "Wiederholung";
var rule = repeat.ReplacesMissingGrade() ? "ersetzt fehlende Note" : repeat.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Wiederholung zählt",
RepeatGradeRule.Original => "Erste Arbeit zählt",
@@ -47,19 +47,33 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private string _repeatError = "";
[ObservableProperty] private string _taskPointsError = "";
[ObservableProperty] private string _kindName = ExamKind.Klausur.Label();
[ObservableProperty] private bool _isMakeUp;
[ObservableProperty] private string _followUpKindName = WiederholungName;
private const string WiederholungName = "Wiederholung";
private const string NachschreibarbeitName = "Nachschreibarbeit";
private const string FoerderarbeitName = "Förderarbeit";
// Beim Befüllen aus einer vorhandenen Arbeit darf die Datums-Übernahme das gespeicherte Datum
// nicht überschreiben.
private bool _initializing = true;
public bool IsDifferentiated { get; }
public string[] NiveauOptions => NiveauDisplay.Options;
public string[] KindOptions { get; } = Enum.GetValues<ExamKind>().Select(k => k.Label()).ToArray();
public string[] RepeatRuleOptions => ["Bessere zählt", "Wiederholung zählt", "Erste Arbeit zählt"];
public string[] FollowUpKindOptions { get; } = [WiederholungName, NachschreibarbeitName, FoerderarbeitName];
public bool IsMakeUp => FollowUpKindName == NachschreibarbeitName;
public bool IsSupportWork => FollowUpKindName == FoerderarbeitName;
public bool HasSelectedOriginalExam => SelectedOriginalExam is not null;
/// Art und Zählweise sind bei einer verknüpften Folgearbeit festgelegt: sie erbt die Art der
/// ersten Arbeit, eine Nachschreibarbeit ersetzt nur fehlende Noten (keine Regelwahl nötig).
/// ersten Arbeit, Nachschreib- und Förderarbeiten ersetzen nur fehlende Noten (keine Regelwahl nötig).
public bool CanChooseKind => !HasSelectedOriginalExam;
public bool ShowRepeatRule => HasSelectedOriginalExam && !IsMakeUp;
public string LinkedExamLabel => IsMakeUp
? "Nachschreibarbeit zu (optional)" : "Wiederholung oder Nachschreibarbeit zu (optional)";
public bool ShowRepeatRule => HasSelectedOriginalExam && FollowUpKindName == WiederholungName;
public string LinkedExamLabel => FollowUpKindName switch
{
NachschreibarbeitName => "Nachschreibarbeit zu (optional)",
FoerderarbeitName => "Förderarbeit zu (optional)",
_ => "Wiederholung, Nachschreib- oder Förderarbeit zu (optional)",
};
private ExamKind Kind => Enum.GetValues<ExamKind>().FirstOrDefault(k => k.Label() == KindName);
public ObservableCollection<Exam> AvailableOriginalExams { get; } = [];
@@ -128,7 +142,9 @@ public partial class ExamDialogViewModel : ObservableObject
if (!isDuplicate)
{
SelectedOriginalExam = AvailableOriginalExams.FirstOrDefault(e => e.Id == source.RepeatOfExamId);
IsMakeUp = source.IsMakeUp && SelectedOriginalExam is not null;
if (SelectedOriginalExam is not null)
FollowUpKindName = source.IsSupportWork ? FoerderarbeitName
: source.IsMakeUp ? NachschreibarbeitName : WiederholungName;
RepeatRuleName = source.RepeatGradeRule switch
{
RepeatGradeRule.Repeat => "Wiederholung zählt",
@@ -152,6 +168,7 @@ public partial class ExamDialogViewModel : ObservableObject
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
RecomputeTotals();
UpdateWeekLoadHint();
_initializing = false;
}
partial void OnUseWeightingChanged(bool value)
@@ -161,17 +178,29 @@ public partial class ExamDialogViewModel : ObservableObject
partial void OnSelectedOriginalExamChanged(Exam? value)
{
if (value is not null) KindName = value.Kind.Label();
else IsMakeUp = false;
else FollowUpKindName = WiederholungName;
OnPropertyChanged(nameof(HasSelectedOriginalExam));
OnPropertyChanged(nameof(CanChooseKind));
OnPropertyChanged(nameof(ShowRepeatRule));
TakeOverOriginalDateForSupportWork();
UpdateWeekLoadHint();
}
partial void OnIsMakeUpChanged(bool value)
partial void OnFollowUpKindNameChanged(string value)
{
OnPropertyChanged(nameof(IsMakeUp));
OnPropertyChanged(nameof(IsSupportWork));
OnPropertyChanged(nameof(ShowRepeatRule));
OnPropertyChanged(nameof(LinkedExamLabel));
TakeOverOriginalDateForSupportWork();
}
/// Eine Förderarbeit wird meist parallel zur ersten Arbeit geschrieben: das Datum der
/// ersten Arbeit ist der Vorschlag, bleibt aber änderbar.
private void TakeOverOriginalDateForSupportWork()
{
if (_initializing || !IsSupportWork || SelectedOriginalExam is null) return;
DateText = SelectedOriginalExam.Date.ToString("dd.MM.yyyy");
}
partial void OnKindNameChanged(string value) => UpdateWeekLoadHint();
@@ -382,12 +411,25 @@ public partial class ExamDialogViewModel : ObservableObject
var gradingKey = BuildGradingKeyEntries();
var gradingKeyError = _grading.ValidateGradingKey(gradingKey);
if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; valid = false; }
if (SelectedOriginalExam is not null && SelectedOriginalExam.Date >= date)
// Datumsprüfung nur bei gültigem Datum — sonst meldet das Datumsfeld den Fehler bereits.
// Eine Förderarbeit entsteht parallel zur ersten Arbeit und darf am selben Tag liegen.
if (SelectedOriginalExam is not null && DateTextError == "")
{
RepeatError = IsMakeUp
? "Die erste Arbeit muss vor der Nachschreibarbeit liegen."
: "Die erste Arbeit muss vor der Wiederholung liegen.";
valid = false;
if (IsSupportWork)
{
if (SelectedOriginalExam.Date > date)
{
RepeatError = "Die Förderarbeit darf nicht vor der ersten Arbeit liegen.";
valid = false;
}
}
else if (SelectedOriginalExam.Date >= date)
{
RepeatError = IsMakeUp
? "Die erste Arbeit muss vor der Nachschreibarbeit liegen."
: "Die erste Arbeit muss vor der Wiederholung liegen.";
valid = false;
}
}
if (Tasks.Any(t => t.VariantName != "Beide") &&
(TotalPoints <= 0 || Tasks.Where(t => !t.IsBonus && t.VariantName != "A").Sum(t => t.MaxPoints) <= 0))
@@ -410,6 +452,7 @@ public partial class ExamDialogViewModel : ObservableObject
Result.RepeatOfExamId = SelectedOriginalExam?.Id;
Result.Kind = SelectedOriginalExam?.Kind ?? Kind;
Result.IsMakeUp = IsMakeUp && SelectedOriginalExam is not null;
Result.IsSupportWork = IsSupportWork && SelectedOriginalExam is not null;
Result.RepeatGradeRule = RepeatRuleName switch
{
"Wiederholung zählt" => RepeatGradeRule.Repeat,
@@ -656,6 +656,7 @@ public class ExamSummary
NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle";
var hasVariants = e.Tasks.Any(t => t.Variant is not null);
var followUp = e.RepeatOfExamId is null ? null
: e.IsSupportWork ? (hasVariants ? "Förder · A/B" : "Förderarbeit")
: e.IsMakeUp ? (hasVariants ? "Nachschr. · A/B" : "Nachschreiben")
: hasVariants ? "Wdh. · A/B" : "Wiederholung";
// Folgearbeiten behalten ihre Art im Hintergrund (sie erben sie), die Spalte nennt nur
@@ -373,7 +373,7 @@ public partial class ReportGradeAssistantViewModel : ObservableObject
if (!ReportGradeCalculator.ExamApplies(exam, membership)) continue;
var results = _calculator.ResultsByExam[exam.Id];
results.TryGetValue(studentId, out var own);
// Nachschreib- und Wiederholungsarbeiten betreffen nur einzelne Schüler — ohne eigenes
// Nachschreib-, Förder- und Wiederholungsarbeiten betreffen nur einzelne Schüler — ohne eigenes
// Ergebnis gehören sie nicht in diese Übersicht.
if (own is null && exam.RepeatOfExamId.HasValue) continue;
@@ -396,7 +396,8 @@ public partial class ReportGradeAssistantViewModel : ObservableObject
var classAverage = classGrades.Count > 0 ? $"Ø Kurs {Format(_grading.WeightedAverage(classGrades))}" : "";
var kind = exam.Kind.Label();
if (exam.IsMakeUp) kind += " · Nachschreibarbeit";
if (exam.IsSupportWork) kind += " · Förderarbeit";
else if (exam.IsMakeUp) kind += " · Nachschreibarbeit";
else if (exam.RepeatOfExamId.HasValue) kind += " · Wiederholung";
Works.Add(new AssistantWorkCard(exam.Title, exam.Date, kind, gradeDisplay, percentDisplay,
@@ -76,9 +76,9 @@
<Button Grid.Column="1" Content="Auswahl löschen" Command="{Binding ClearOriginalExamCommand}"
IsVisible="{Binding HasSelectedOriginalExam}"/>
</Grid>
<CheckBox Content="Nachschreibarbeit für Schüler, die die erste Arbeit versäumt haben"
IsChecked="{Binding IsMakeUp}" IsVisible="{Binding HasSelectedOriginalExam}"
ToolTip.Tip="Inhaltlich eigene Arbeit nur für Fehlende. Die Note ersetzt die fehlende Note der ersten Arbeit; die Punkteeingabe zeigt nur die Schüler, die die erste Arbeit nicht mitgeschrieben haben. Ohne Haken gilt es als Wiederholung für alle."/>
<ComboBox ItemsSource="{Binding FollowUpKindOptions}" SelectedItem="{Binding FollowUpKindName}"
IsVisible="{Binding HasSelectedOriginalExam}" HorizontalAlignment="Stretch"
ToolTip.Tip="Wiederholung: Arbeit für alle, es zählt die gewählte Regel. Nachschreibarbeit: inhaltlich eigene Arbeit nur für Schüler, die die erste Arbeit versäumt haben. Förderarbeit: inhaltlich eigene Arbeit für einzelne Schüler, die sie statt der ersten Arbeit schreiben (darf am selben Tag liegen; das Datum der ersten Arbeit wird vorgeschlagen). Bei Nachschreib- und Förderarbeit ersetzt die Note eine fehlende Note der ersten Arbeit, und die Punkteeingabe blendet Schüler mit vorhandener Note der ersten Arbeit aus."/>
<ComboBox ItemsSource="{Binding RepeatRuleOptions}" SelectedItem="{Binding RepeatRuleName}"
IsVisible="{Binding ShowRepeatRule}"/>
<TextBlock Text="{Binding RepeatError}" Foreground="Red" FontSize="11"
@@ -60,6 +60,26 @@ public class ExamCoverSheetServiceTests
Assert.False(ExamParticipantService.Includes(exam, new GroupMembership { Niveau = Niveau.E }, false, false));
}
[Fact]
public void Includes_Foerderarbeit_BlendetSchuelerMitNoteDerErstenArbeitAus()
{
var original = new Exam();
var support = new Exam { IsSupportWork = true, RepeatOfExamId = original.Id, Date = original.Date };
var wrote = Guid.NewGuid();
var supported = Guid.NewGuid();
List<ExamResult> originalResults =
[
new() { ExamId = original.Id, StudentId = wrote, Grade = "3" },
new() { ExamId = original.Id, StudentId = supported, Absent = true },
];
var wroteOriginal = ExamParticipantService.WroteOriginal(support, _ => originalResults);
Assert.Equal([wrote], wroteOriginal);
Assert.False(ExamParticipantService.Includes(support, null, false, wroteOriginal.Contains(wrote)));
Assert.True(ExamParticipantService.Includes(support, null, false, wroteOriginal.Contains(supported)));
}
[Fact]
public void Includes_Nachschreibarbeit_OhneErgebnisNurWerDieErsteArbeitVersaeumtHat()
{
@@ -97,6 +97,19 @@ public class ExamScoringServiceTests
[(original, "4"), (makeUp, "1")], GradingSystem.Grades1To6));
}
[Fact]
public void Foerderarbeit_ErsetztNurFehlendeNote()
{
var original = new Exam();
var support = new Exam { RepeatOfExamId = original.Id, IsSupportWork = true };
// Förderschüler hat die erste Arbeit nicht geschrieben: die Förderarbeit zählt allein.
Assert.Equal(["3"], ExamScoringService.EffectiveGrades([(support, "3")], GradingSystem.Grades1To6));
// Liegen beide Noten vor, zählt die erste — auch wenn die Förderarbeit besser ist.
Assert.Equal(["4"], ExamScoringService.EffectiveGrades(
[(original, "4"), (support, "1")], GradingSystem.Grades1To6));
}
[Fact]
public void MehrereFolgearbeiten_ZaehlenGenauEinmal()
{
+12
View File
@@ -59,6 +59,18 @@ Diese Regeln gelten für jede Aufgabe unten und müssen nicht wiederholt werden.
nicht in Klausurwochen-Belastung und Vorjahresvergleich. Folgearbeiten erben die Art.
Bestehende Klausuren bleiben unverändert (Default `Klausur`).
- [x] **Nachtrag 2026-10: Förderarbeit als dritte Folgearbeits-Art.** Eine Förderarbeit
(`Exam.IsSupportWork`) ist eine inhaltlich eigene Arbeit, die einzelne Schüler statt der
parallel geschriebenen ersten Arbeit schreiben (Workflow: erste Arbeit per „Duplizieren“
kopieren, Aufgaben/Schlüssel anpassen, im Arbeitsdialog mit der ersten Arbeit verknüpfen).
Der Arbeitsdialog ersetzt die Nachschreib-Checkbox durch eine Auswahl
Wiederholung/Nachschreibarbeit/Förderarbeit; die Förderarbeit darf am selben Tag wie die erste
Arbeit liegen (nur *vor* der ersten Arbeit ist gesperrt) und übernimmt beim Auswählen dessen
Datum als änderbaren Vorschlag. Sie zählt wie eine Nachschreibarbeit
(`ExamFollowUpExtensions.ReplacesMissingGrade`): ihre Note ersetzt nur eine fehlende Note, die
Punkteeingabe blendet Schüler mit Note der ersten Arbeit aus — Schüler ohne Eintrag in der
ersten Arbeit (z. B. Kranke) lassen sich mit „Rest als abwesend markieren“ ausblenden.
Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits in
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt.
+4 -1
View File
@@ -142,7 +142,10 @@ Einige berechnete Werte bleiben absichtlich gespeichert:
- `Exam.RepeatOfExamId` verweist auf die ursprüngliche Arbeit derselben Gruppe.
`RepeatGradeRule` legt fest, ob die bessere, die wiederholte oder die erste Note
zählt. Beide Ergebnisse bleiben sichtbar, gehen aber als ein Wert in die
berechneten Durchschnitts- und Zeugnisnoten ein.
berechneten Durchschnitts- und Zeugnisnoten ein. `Exam.IsMakeUp` (Nachschreibarbeit) und
`Exam.IsSupportWork` (Förderarbeit) markieren Folgearbeiten nur für einzelne Schüler; ihre Note
ersetzt ausschließlich eine fehlende Note der ersten Arbeit. Anders als Nachschreibarbeiten darf
eine Förderarbeit am selben Tag wie die erste Arbeit liegen (sie wird parallel geschrieben).
- `CompetencyReference` (`Unit.Competencies`, `Lesson.Competencies`, `ExamTask.Competencies`,
`ParticipationSession.Competencies`) verweist über `ItemId` auf ein `CompetencyItem`, hält aber
zusätzlich `Code`/`Description` zum Zuordnungszeitpunkt fest (TODO 16.5, abgelöst von einer reinen