From d61a97de1e0565ef3d85bd29057983e1291b7e6a Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Thu, 22 Oct 2026 22:12:13 +0200 Subject: [PATCH] Punkteeingabe: nur angefahrene Zeilen nicht mehr als 0 Punkte/Note 6 speichern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ExamGradingDialogViewModelTests.cs | 92 +++++++++++++++++++ .../Groups/ExamGradingViewModels.cs | 29 +++++- TODO.md | 4 + 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/LehrerApp.Desktop.Tests/ExamGradingDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/ExamGradingDialogViewModelTests.cs index 0d1cac2..b131cf1 100644 --- a/LehrerApp.Desktop.Tests/ExamGradingDialogViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/ExamGradingDialogViewModelTests.cs @@ -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 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); + } } diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs index e6b921e..dd62abb 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs @@ -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? 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 tasks, ExamResult? existing, List 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) => diff --git a/TODO.md b/TODO.md index 945def2..ee74717 100644 --- a/TODO.md +++ b/TODO.md @@ -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.