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)
|
||||
{
|
||||
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();
|
||||
if (numeric.Count == 0) return 0;
|
||||
var total = numeric.Sum(g => g.Weight);
|
||||
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.
|
||||
/// "Kaufmännisch" rundet bei genau 0,5 immer vom Nullpunkt weg (Standard).
|
||||
/// "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 IExamResultRepository _results;
|
||||
private readonly IGradeRepository _grades;
|
||||
private readonly IUnitRepository _units;
|
||||
private readonly Dictionary<Guid, GroupMembership> _membershipsByStudent;
|
||||
private readonly GradingService _grading;
|
||||
private readonly Guid _groupId;
|
||||
@@ -75,11 +76,11 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
IParticipationAspectRepository aspects, IParticipationSectionRepository sectionRepo,
|
||||
IStudentRepository students, IGroupMembershipRepository memberships,
|
||||
IExamRepository exams, IExamResultRepository results,
|
||||
IGradeRepository grades, GradingService grading,
|
||||
IGradeRepository grades, IUnitRepository units, GradingService grading,
|
||||
Guid groupId, string schoolYear, GradingSystem gradingSystem, string groupLabel)
|
||||
{
|
||||
_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;
|
||||
GroupLabel = groupLabel;
|
||||
_membershipsByStudent = memberships.GetByGroup(groupId).ToDictionary(m => m.StudentId);
|
||||
@@ -98,6 +99,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
|
||||
_rollupPeriod = RollupPeriodOptions[0];
|
||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||
NewSectionEndDateText = SuggestSectionEndDate().ToString("dd.MM.yyyy");
|
||||
|
||||
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)
|
||||
: (_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)
|
||||
{
|
||||
Sections.Clear();
|
||||
@@ -378,6 +397,7 @@ public partial class ParticipationWizardDialogViewModel : ObservableObject
|
||||
}
|
||||
|
||||
NewSectionLabel = $"Abschnitt {_sectionList.Count + 1}";
|
||||
NewSectionEndDateText = SuggestSectionEndDate().ToString("dd.MM.yyyy");
|
||||
BuildTimeline(CurrentStudentId);
|
||||
BuildSections(CurrentStudentId);
|
||||
}
|
||||
|
||||
@@ -350,6 +350,7 @@ public partial class ParticipationTabView : UserControl
|
||||
App.Services.GetRequiredService<IExamRepository>(),
|
||||
App.Services.GetRequiredService<IExamResultRepository>(),
|
||||
App.Services.GetRequiredService<IGradeRepository>(),
|
||||
App.Services.GetRequiredService<IUnitRepository>(),
|
||||
App.Services.GetRequiredService<GradingService>(),
|
||||
tabVm.GroupId, tabVm.SchoolYear, tabVm.GradingSystem, tabVm.GroupLabel);
|
||||
|
||||
|
||||
@@ -181,6 +181,25 @@ public class GradingServiceTests
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -252,6 +252,15 @@ eigenes Werkzeug, nur im Kopf überschlagen.
|
||||
**Bewusst zurückgestellt:** eine Übersicht über alle Fächer eines Schülers gleichzeitig
|
||||
(Gesamt-Zeugnisvorschau) — unterschiedliche Notenschemata/Gewichtungen pro Kurs wären ein
|
||||
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
|
||||
21 Tage gruppenübergreifend mit Direktauflösung; ältere Einträge werden ausgeblendet statt
|
||||
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