Notentendenzen (3+/2-) korrekt in Notenschnitt einrechnen, Abschnittsende-Vorschlag aus Einheit
CI / build-and-test (push) Canceled after 0s
CI / build-and-test (push) Canceled after 0s
Bisher liess int.TryParse jede Note mit Tendenz durchfallen und WeightedAverage rechnete den betroffenen Bereich mit 0 statt sie auszulassen - faelschte Notenuebersicht, Zielnoten- und Was-waere-wenn-Rechner. Ausserdem schlaegt der Mitarbeits-Assistent beim Abschnitt-Abschliessen jetzt das Ende der zuletzt abgeschlossenen Unterrichtseinheit statt immer "heute" vor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,13 +77,29 @@ public class GradingService
|
|||||||
public double WeightedAverage(List<(string Grade, double Weight)> grades)
|
public double WeightedAverage(List<(string Grade, double Weight)> grades)
|
||||||
{
|
{
|
||||||
var numeric = grades
|
var numeric = grades
|
||||||
.Select(g => (Value: int.TryParse(g.Grade, out var n) ? (int?)n : null, g.Weight))
|
.Select(g => (Value: ParseGradeValue(g.Grade), g.Weight))
|
||||||
.Where(g => g.Value.HasValue).ToList();
|
.Where(g => g.Value.HasValue).ToList();
|
||||||
if (numeric.Count == 0) return 0;
|
if (numeric.Count == 0) return 0;
|
||||||
var total = numeric.Sum(g => g.Weight);
|
var total = numeric.Sum(g => g.Weight);
|
||||||
return total == 0 ? 0 : numeric.Sum(g => g.Value!.Value * g.Weight) / total;
|
return total == 0 ? 0 : numeric.Sum(g => g.Value!.Value * g.Weight) / total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wandelt eine Notenangabe mit optionaler Tendenz ("3+", "2-") in einen rechnerischen
|
||||||
|
/// Notenwert um. Tendenzen verschieben um ein Drittel in Richtung der besseren Note
|
||||||
|
/// (z.B. "3+" → 2,67, "2-" → 2,33) — der in deutschen Notensystemen übliche Drittelabstand.
|
||||||
|
private static double? ParseGradeValue(string grade)
|
||||||
|
{
|
||||||
|
var trimmed = grade.Trim();
|
||||||
|
if (trimmed.Length == 0) return null;
|
||||||
|
|
||||||
|
var tendency = 0.0;
|
||||||
|
var core = trimmed;
|
||||||
|
if (core.EndsWith('+')) { tendency = -1.0 / 3.0; core = core[..^1].TrimEnd(); }
|
||||||
|
else if (core.EndsWith('-')) { tendency = 1.0 / 3.0; core = core[..^1].TrimEnd(); }
|
||||||
|
|
||||||
|
return int.TryParse(core, out var n) ? n + tendency : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Rundet einen rechnerischen Notenwert auf eine ganze Note/Punktzahl.
|
/// Rundet einen rechnerischen Notenwert auf eine ganze Note/Punktzahl.
|
||||||
/// "Kaufmännisch" rundet bei genau 0,5 immer vom Nullpunkt weg (Standard).
|
/// "Kaufmännisch" rundet bei genau 0,5 immer vom Nullpunkt weg (Standard).
|
||||||
/// "Pädagogisch" rundet bei genau 0,5 in Richtung der besseren Note
|
/// "Pädagogisch" rundet bei genau 0,5 in Richtung der besseren Note
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
private readonly IExamRepository _exams;
|
private readonly IExamRepository _exams;
|
||||||
private readonly IExamResultRepository _results;
|
private readonly IExamResultRepository _results;
|
||||||
private readonly IGradeRepository _grades;
|
private readonly IGradeRepository _grades;
|
||||||
|
private readonly IUnitRepository _units;
|
||||||
private readonly Dictionary<Guid, GroupMembership> _membershipsByStudent;
|
private readonly Dictionary<Guid, GroupMembership> _membershipsByStudent;
|
||||||
private readonly GradingService _grading;
|
private readonly GradingService _grading;
|
||||||
private readonly Guid _groupId;
|
private readonly Guid _groupId;
|
||||||
@@ -75,11 +76,11 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
||||||
IStudentRepository students, IGroupMembershipRepository memberships,
|
IStudentRepository students, IGroupMembershipRepository memberships,
|
||||||
IExamRepository exams, IExamResultRepository results,
|
IExamRepository exams, IExamResultRepository results,
|
||||||
IGradeRepository grades, GradingService grading,
|
IGradeRepository grades, IUnitRepository units, GradingService grading,
|
||||||
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
||||||
{
|
{
|
||||||
_sessions = sessions; _entries = entries; _sectionRepo = sectionRepo;
|
_sessions = sessions; _entries = entries; _sectionRepo = sectionRepo;
|
||||||
_exams = exams; _results = results; _grades = grades; _grading = grading;
|
_exams = exams; _results = results; _grades = grades; _units = units; _grading = grading;
|
||||||
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
_groupId = groupId; _schoolYear = schoolYear; _gradingSystem = gradingSystem;
|
||||||
GroupLabel = groupLabel;
|
GroupLabel = groupLabel;
|
||||||
_membershipsByStudent = memberships.GetByGroup(groupId).ToDictionary(m => m.StudentId);
|
_membershipsByStudent = memberships.GetByGroup(groupId).ToDictionary(m => m.StudentId);
|
||||||
@@ -98,6 +99,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
|
|
||||||
_rollupPeriod = RollupPeriodOptions[0];
|
_rollupPeriod = RollupPeriodOptions[0];
|
||||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||||
|
NewSectionEndDateText = SuggestSectionEndDate().ToString("dd.MM.yyyy");
|
||||||
|
|
||||||
if (_students.Count > 0) ShowStudent(0);
|
if (_students.Count > 0) ShowStudent(0);
|
||||||
}
|
}
|
||||||
@@ -277,6 +279,23 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
_sectionList.Count > 0 ? _sectionList.Max(s => s.EndDate).AddDays(1)
|
_sectionList.Count > 0 ? _sectionList.Max(s => s.EndDate).AddDays(1)
|
||||||
: (_allSessions.Count > 0 ? _allSessions.Min(s => s.Date) : DateOnly.FromDateTime(DateTime.Today));
|
: (_allSessions.Count > 0 ? _allSessions.Min(s => s.Date) : DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
|
||||||
|
/// Schlägt als Abschnittsende bewusst nicht immer "heute" vor: Wenn seit Abschnittsbeginn
|
||||||
|
/// eine Unterrichtseinheit (4.2) abgeschlossen wurde, ist deren Ende meist die fachlich
|
||||||
|
/// sinnvollere Zäsur für eine Mitarbeit-Teilnote als das Bearbeitungsdatum im Wizard.
|
||||||
|
private DateOnly SuggestSectionEndDate()
|
||||||
|
{
|
||||||
|
var openStart = ComputeOpenStart();
|
||||||
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
||||||
|
var completedUnitEnd = _units.GetByGroup(_groupId)
|
||||||
|
.Where(u => u.Status == UnitStatus.Completed && u.EndDate.HasValue)
|
||||||
|
.Select(u => u.EndDate!.Value)
|
||||||
|
.Where(d => d >= openStart && d <= today)
|
||||||
|
.OrderByDescending(d => d)
|
||||||
|
.Cast<DateOnly?>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
return completedUnitEnd ?? today;
|
||||||
|
}
|
||||||
|
|
||||||
private void BuildSections(Guid studentId)
|
private void BuildSections(Guid studentId)
|
||||||
{
|
{
|
||||||
Sections.Clear();
|
Sections.Clear();
|
||||||
@@ -378,6 +397,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||||
|
NewSectionEndDateText = SuggestSectionEndDate().ToString("dd.MM.yyyy");
|
||||||
BuildTimeline(CurrentStudentId);
|
BuildTimeline(CurrentStudentId);
|
||||||
BuildSections(CurrentStudentId);
|
BuildSections(CurrentStudentId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ public partial class ParticipationTabView : UserControl
|
|||||||
App.Services.GetRequiredService<IExamRepository>(),
|
App.Services.GetRequiredService<IExamRepository>(),
|
||||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||||
App.Services.GetRequiredService<IGradeRepository>(),
|
App.Services.GetRequiredService<IGradeRepository>(),
|
||||||
|
App.Services.GetRequiredService<IUnitRepository>(),
|
||||||
App.Services.GetRequiredService<GradingService>(),
|
App.Services.GetRequiredService<GradingService>(),
|
||||||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem, tabVm.GroupLabel);
|
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem, tabVm.GroupLabel);
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,25 @@ public class GradingServiceTests
|
|||||||
Assert.Equal(0, result);
|
Assert.Equal(0, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("3+", 2.6667)]
|
||||||
|
[InlineData("2-", 2.3333)]
|
||||||
|
[InlineData("1+", 0.6667)]
|
||||||
|
[InlineData("6-", 6.3333)]
|
||||||
|
public void WeightedAverage_NotenMitTendenz_VerschiebtUmEinDrittel(string grade, double expected)
|
||||||
|
{
|
||||||
|
var result = _sut.WeightedAverage([(grade, 1.0)]);
|
||||||
|
Assert.Equal(expected, result, precision: 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WeightedAverage_NotenMitTendenzGemischtMitGanzenNoten_GewichtetKorrekt()
|
||||||
|
{
|
||||||
|
// (3+ [2,667] + 2 [2,0]) / 2 = 2,333
|
||||||
|
var result = _sut.WeightedAverage([("3+", 1.0), ("2", 1.0)]);
|
||||||
|
Assert.Equal(2.3333, result, precision: 4);
|
||||||
|
}
|
||||||
|
|
||||||
// ── RoundToGrade ──────────────────────────────────────────────────────────
|
// ── RoundToGrade ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -252,6 +252,15 @@ eigenes Werkzeug, nur im Kopf überschlagen.
|
|||||||
**Bewusst zurückgestellt:** eine Übersicht über alle Fächer eines Schülers gleichzeitig
|
**Bewusst zurückgestellt:** eine Übersicht über alle Fächer eines Schülers gleichzeitig
|
||||||
(Gesamt-Zeugnisvorschau) — unterschiedliche Notenschemata/Gewichtungen pro Kurs wären ein
|
(Gesamt-Zeugnisvorschau) — unterschiedliche Notenschemata/Gewichtungen pro Kurs wären ein
|
||||||
eigener, größerer Umbau.
|
eigener, größerer Umbau.
|
||||||
|
- [x] **2.6.5** Bugfix (Nutzer-Feedback): frei eingetragene Noten mit Tendenz ("3+", "2-") in den
|
||||||
|
editierbaren Mitarbeit-/Sonstige-Feldern (2.6.4) wurden von `GradingService.WeightedAverage`
|
||||||
|
nicht erkannt (`int.TryParse` schlägt bei einem Suffix fehl) — bestanden nur Tendenznoten in
|
||||||
|
einem Bereich, wurde er komplett auf 0 statt ausgelassen berechnet, was den Zielnoten-Rechner
|
||||||
|
und die Zeugnisnote verfälschte. Neue `GradingService.ParseGradeValue()` erkennt ein
|
||||||
|
abschließendes `+`/`-` und verschiebt um ein Drittel Richtung der besseren Note
|
||||||
|
(z.B. "3+" → 2,67, "2-" → 2,33, üblicher Drittelabstand deutscher Notensysteme) — betrifft
|
||||||
|
damit auch die "Gesamt"-Spalte der Notenübersicht (2.1.2) und `CalculateReportGrade`, die
|
||||||
|
beide auf `WeightedAverage` aufbauen.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -442,6 +451,13 @@ zusammenziehen, daraus die Halbjahresnote bilden":
|
|||||||
- Dashboard-Kachel "Offene Entschuldigungen": listet alle `ExcusePending`-Einträge der letzten
|
- Dashboard-Kachel "Offene Entschuldigungen": listet alle `ExcusePending`-Einträge der letzten
|
||||||
21 Tage gruppenübergreifend mit Direktauflösung; ältere Einträge werden ausgeblendet statt
|
21 Tage gruppenübergreifend mit Direktauflösung; ältere Einträge werden ausgeblendet statt
|
||||||
automatisch entschieden (pädagogische Entscheidung bleibt bei der Lehrkraft).
|
automatisch entschieden (pädagogische Entscheidung bleibt bei der Lehrkraft).
|
||||||
|
- **Nachtrag (Nutzer-Feedback, nach erster produktiver Nutzung):** Vorgeschlagenes Abschnittsende
|
||||||
|
im Assistenten war immer "heute" (`NewSectionEndDateText`), obwohl die fachlich naheliegende
|
||||||
|
Zäsur oft schon feststeht, wenn die aktuelle Unterrichtseinheit (4.1) abgeschlossen ist. Neue
|
||||||
|
`SuggestSectionEndDate()` schlägt stattdessen das `Unit.EndDate` der zuletzt abgeschlossenen
|
||||||
|
Einheit (`UnitStatus.Completed`) vor, sofern deren Ende im laufenden, noch offenen Abschnitt
|
||||||
|
liegt (zwischen dem Ende des letzten Abschnitts und heute) — sonst bleibt es bei "heute". Der
|
||||||
|
Vorschlag ist nur eine Vorbelegung des Textfelds, weiterhin frei überschreibbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user