Punkteeingabe: nur angefahrene Zeilen nicht mehr als 0 Punkte/Note 6 speichern
CI / build-and-test (push) Canceled after 0s

Das Verlassen eines unveränderten Punktefelds löste Autosave aus; leere Zellen
wurden als 0 gespeichert und ergaben Note 6. Zellen melden nur noch echte
Änderungen, Zeilen ohne Werte werden nicht angelegt und haben keine Note.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Sonnet 5.5 committed 2026-10-22 22:12:13 +02:00
1 parent 41649b0a05
commit d61a97de1e
3 files changed
+120 -5

No files matched your search

@@ -1,3 +1,4 @@
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
@@ -133,4 +134,95 @@ public sealed class ExamGradingDialogViewModelTests
Assert.Equal(["Aufgabe 1", "★ Bonus", "2. Redox"], vm.TaskHeaders);
}
private sealed class RecordingResults : FakeResults, IExamResultRepository
{
public List<ExamResult> Saved { get; } = [];
public new void Save(ExamResult result) => Saved.Add(result);
}
private static (ExamGradingDialogViewModel Vm, RecordingResults Results) CreateRecordingGrid(
ExamResult? existing = null)
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var student = new Student { FirstName = "Ben", LastName = "Cordes" };
var exam = new Exam
{
GroupId = group.Id, Title = "Klausur 1", Tasks = [.. ThreeTasks()],
GradingKey = GradingService.DefaultKey1To6(),
};
var results = new RecordingResults();
if (existing is not null)
{
existing.ExamId = exam.Id; existing.StudentId = student.Id;
results.Add(existing);
}
var vm = new ExamGradingDialogViewModel(results, new FakeStudents([student]),
new FakeMemberships([new GroupMembership { GroupId = group.Id, StudentId = student.Id }]),
new GradingService(), exam, group.Id);
return (vm, results);
}
[Fact]
public void NurAngefahreneZelle_LegtKeinErgebnisMitNullPunktenAn()
{
var (vm, results) = CreateRecordingGrid();
// Fokus in die Zelle und wieder heraus ohne Eingabe: die View meldet das als TrySetValue(null).
vm.Rows[0].Cells[0].TrySetValue(null);
vm.Rows[0].Cells[1].TrySetValue(null);
Assert.Empty(results.Saved);
Assert.Equal("", vm.Rows[0].GradeDisplay);
}
[Fact]
public void ErsterEingetragenerWert_LegtDasErgebnisAn()
{
var (vm, results) = CreateRecordingGrid();
vm.Rows[0].Cells[0].TrySetValue(4);
var saved = Assert.Single(results.Saved);
Assert.Equal([4, 0, 0], saved.Points);
Assert.Equal(4, saved.TotalPoints);
}
[Fact]
public void UnveraenderterWert_SpeichertNichtErneut()
{
var (vm, results) = CreateRecordingGrid();
vm.Rows[0].Cells[0].TrySetValue(4);
vm.Rows[0].Cells[0].TrySetValue(4);
Assert.Single(results.Saved);
}
[Fact]
public void LeereZeileMitBestehendemErgebnis_WirdAlsUnkorrigiertGespeichertNichtAlsNull()
{
var (vm, results) = CreateRecordingGrid(
new ExamResult { Points = [4], TotalPoints = 4, Grade = "4" });
vm.Rows[0].Cells[0].TrySetValue(null);
var saved = Assert.Single(results.Saved);
Assert.Empty(saved.Points);
Assert.Null(saved.Grade);
Assert.False(ExamCoverSheetService.IsCorrected(saved));
}
[Fact]
public void AbwesendheitOhnePunkte_WirdOhnePunkteUndNoteGespeichert()
{
var (vm, results) = CreateRecordingGrid();
vm.Rows[0].Absent = true;
var saved = Assert.Single(results.Saved);
Assert.True(saved.Absent);
Assert.Empty(saved.Points);
Assert.Null(saved.Grade);
}
}
@@ -65,11 +65,21 @@ public partial class ExamGradingDialogViewModel : ObservableObject
var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey,
ExamScoringService.Maximum(Tasks, ExamVariant.A), _grading);
row.OnChanged = SaveRow;
row.HasStoredResult = result is not null;
Rows.Add(row);
}
}
private void SaveRow(ExamResultRow row) => _results.Save(row.ToModel(_exam.Id));
/// Eine Zeile, die nur angefahren wurde (Fokus rein, Fokus raus), ist kein Ergebnis: ohne
/// Punkte, Abwesenheit oder Kommentar und ohne bereits gespeichertes Ergebnis wird nichts
/// angelegt — sonst stünde der Schüler später mit 0 Punkten und Note 6 in der Liste.
private void SaveRow(ExamResultRow row)
{
if (!row.HasStoredResult && !row.HasPoints && !row.Absent && string.IsNullOrWhiteSpace(row.Comment))
return;
_results.Save(row.ToModel(_exam.Id));
row.HasStoredResult = true;
}
partial void OnEntryOrderNameChanged(string value) =>
_settings?.Save(new ExamGradingSettings { EntryOrder = EntryOrder });
@@ -160,6 +170,12 @@ public partial class ExamResultRow : ObservableObject
public Action<ExamResultRow>? OnChanged { get; set; }
/// Mindestens eine Aufgabe hat einen eingetragenen Wert (auch 0). Ohne Werte gibt es weder
/// Summe noch Note.
public bool HasPoints => Cells.Any(c => c.Value is not null);
/// Für diesen Schüler liegt in der Datenbank bereits ein Ergebnis dieser Arbeit.
public bool HasStoredResult { get; set; }
public ExamResultRow(Guid studentId, string name, List<ExamTask> tasks, ExamResult? existing,
List<GradingKeyEntry> gradingKey, double examMaxPoints, GradingService grading)
{
@@ -202,7 +218,7 @@ public partial class ExamResultRow : ObservableObject
{
TotalPoints = Cells.Sum(c => c.IsApplicable && !c.IsInvalid ? (c.Value ?? 0) : 0);
OnPropertyChanged(nameof(TotalPointsDisplay));
GradeDisplay = Absent ? "abwesend" : _grading.CalculateGrade(TotalPoints,
GradeDisplay = Absent ? "abwesend" : !HasPoints ? "" : _grading.CalculateGrade(TotalPoints,
ExamScoringService.Maximum(_tasks, Variant), _gradingKey);
}
@@ -211,9 +227,9 @@ public partial class ExamResultRow : ObservableObject
Id = _resultId,
ExamId = examId,
StudentId = StudentId,
Points = Cells.Select(c => c.Value ?? 0).ToList(),
Points = HasPoints ? Cells.Select(c => c.Value ?? 0).ToList() : [],
TotalPoints = TotalPoints,
Grade = Absent ? null : GradeDisplay,
Grade = Absent || !HasPoints ? null : GradeDisplay,
Absent = Absent,
Variant = Variant,
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
@@ -240,11 +256,14 @@ public partial class PointsCell : ObservableObject
/// Setzt den Wert; bei Punkten außerhalb 0..Maximalpunkte wird nur rot markiert,
/// aber nicht gespeichert (1.4.5) — OnChanged (und damit Autosave) wird dann nicht ausgelöst.
/// Auch unverändert gelassene Zellen melden sich beim Verlassen des Eingabefelds hier —
/// gespeichert wird nur bei einer tatsächlichen Änderung.
public void TrySetValue(double? value)
{
var changed = Value != value;
Value = value;
IsInvalid = IsOutOfRange(value);
if (!IsInvalid) OnChanged?.Invoke();
if (!IsInvalid && changed) OnChanged?.Invoke();
}
private bool IsOutOfRange(double? value) =>
+4
View File
@@ -74,6 +74,10 @@ Diese Regeln gelten für jede Aufgabe unten und müssen nicht wiederholt werden.
Niveaudifferenzierung (Checkbox-Spalte „Förder“ in der Schülerliste; in differenzierten
Gruppen zählt das Förderniveau). Die Punkteeingabe/Deckblätter einer Förderarbeit zeigen nur
Förderschüler (plus Schüler mit vorhandenem Ergebnis), das Kranke-Ausblenden entfällt damit.
Fix Punkteeingabe: eine nur angefahrene Zeile (Fokus rein/raus ohne Eingabe) legt kein
Ergebnis mit 0 Punkten und Note 6 mehr an (`ExamGradingDialogViewModel.SaveRow` speichert nur
bei Wert, Abwesenheit, Kommentar oder vorhandenem Ergebnis; `PointsCell.TrySetValue` meldet
nur echte Änderungen; Zeilen ohne Werte haben keine Note und speichern keine Punkteliste).
Modelle `Exam`, `ExamTask`, `GradingKeyEntry`, `ExamResult` existieren bereits in
[Exam.cs](LehrerApp.Core/Models/Exam.cs), Repositories ebenfalls. Die komplette UI fehlt.