CI / build-and-test (push) Canceled after 0s
Neuer Assistent (Einstieg im Zeugnisnoten-Dialog) geht die Lerngruppe Schüler für Schüler durch: berechnete Note mit ungerundetem Wert und Teilbereichen, Mitarbeitsgraph abschnittsweise oder als Verlauf, Arbeiten und Einzelnoten. Das 1. Halbjahr erscheint im 2. Halbjahr nur als Kontext. Die Endjahresnote wird jetzt in Dialog und Assistent aus den Halbjahresnoten verrechnet (gemeinsamer ReportGradeCalculator). In Klasse 10 kommen Projektprüfung, schriftliche/mündliche Abschlussprüfung und zusätzliche mündliche Prüfung hinzu, gewichtet nach Oberschulverordnung und PrüfV Sek. I; gerundet wird dabei bewusst nicht automatisch. Die Zeitleisten-Erzeugung des Mitarbeits-Assistenten liegt dafür im gemeinsamen ParticipationTimelineBuilder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
249 lines
12 KiB
C#
249 lines
12 KiB
C#
using LehrerApp.Core.Models;
|
||
using LehrerApp.Core.Services;
|
||
using LehrerApp.Desktop.ViewModels.Groups;
|
||
using Xunit;
|
||
|
||
namespace LehrerApp.Desktop.Tests;
|
||
|
||
/// Tests für die Zeugnisnotenberechnung (2.4): ReportGradeDialogViewModel kombiniert
|
||
/// Klausuren, Mitarbeit und sonstige Noten gemäß Gewichtungsschema zu einer Zeugnisnote.
|
||
public class ReportGradeCalculationTests
|
||
{
|
||
private static Guid GroupId { get; } = Guid.NewGuid();
|
||
private static Student Anna { get; } = new() { Id = Guid.NewGuid(), FirstName = "Anna", LastName = "Beispiel" };
|
||
private static Student Ben { get; } = new() { Id = Guid.NewGuid(), FirstName = "Ben", LastName = "Muster" };
|
||
|
||
private static ReportGradeDialogViewModel BuildViewModel(
|
||
FakeGrades grades, FakeSchemes schemes, FakeReportGrades reportGrades,
|
||
FakeExams exams, FakeResults results, FakeSessions? sessions = null, FakeEntries? entries = null)
|
||
{
|
||
var students = new FakeStudents([Anna, Ben]);
|
||
var memberships = new FakeMemberships([]);
|
||
var grading = new GradingService();
|
||
|
||
return new ReportGradeDialogViewModel(grades, exams, results, students, memberships,
|
||
schemes, reportGrades, sessions ?? new FakeSessions([]), entries ?? new FakeEntries(),
|
||
new AttendanceBalanceService(), grading, GroupId, GroupType.Class, GradingSystem.Grades1To6,
|
||
"Testgruppe", "2025/26");
|
||
}
|
||
|
||
private static (Exam Exam1, Exam Exam2, FakeExams Exams, FakeResults Results) BuildExams()
|
||
{
|
||
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
|
||
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
|
||
var exams = new FakeExams([exam1, exam2]);
|
||
var results = new FakeResults();
|
||
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
|
||
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "4" });
|
||
return (exam1, exam2, exams, results);
|
||
}
|
||
|
||
[Fact]
|
||
public void CalculatedGrade_KombiniertKlausurenMitarbeitUndSonstigeGemaessSchema()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var grades = new FakeGrades();
|
||
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Participation,
|
||
Date = new DateOnly(2025, 9, 5), Value = "3", Weight = 1.0 });
|
||
grades.Add(new Grade { StudentId = Anna.Id, GroupId = GroupId, Category = GradeCategory.Other,
|
||
Date = new DateOnly(2025, 9, 5), Value = "1", Weight = 1.0 });
|
||
var schemes = new FakeSchemes();
|
||
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 });
|
||
|
||
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
|
||
|
||
// Klausuren Ø3 (Note 2 und 4), Mitarbeit 3, Sonstige 1 -> (3*50+3*40+1*10)/100 = 2.8 -> kaufmännisch 3
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
Assert.Equal("3", anna.CalculatedValue);
|
||
}
|
||
|
||
[Fact]
|
||
public void CalculatedGrade_SchuelerOhneJedeNote_ZeigtKeinenWert()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var schemes = new FakeSchemes();
|
||
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
|
||
|
||
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
|
||
Assert.Null(ben.CalculatedValue);
|
||
Assert.Equal("–", ben.CalculatedDisplay);
|
||
}
|
||
|
||
[Fact]
|
||
public void AbsenceRatePercent_BerechnetFehlquoteAusSitzungenDieserGruppeImZeitraum()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var session1 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 5) };
|
||
var session2 = new ParticipationSession { GroupId = GroupId, Date = new DateOnly(2025, 9, 12) };
|
||
var sessions = new FakeSessions([session1, session2]);
|
||
var entries = new FakeEntries();
|
||
entries.Add(new ParticipationEntry { SessionId = session1.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Present });
|
||
entries.Add(new ParticipationEntry { SessionId = session2.Id, StudentId = Anna.Id, Attendance = AttendanceStatus.Unexcused });
|
||
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results,
|
||
sessions, entries);
|
||
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
Assert.Equal(50.0, anna.AbsenceRatePercent);
|
||
Assert.True(anna.HasHighAbsenceRate);
|
||
var ben = vm.Rows.Single(r => r.StudentId == Ben.Id);
|
||
Assert.Equal(0.0, ben.AbsenceRatePercent);
|
||
Assert.False(ben.HasHighAbsenceRate);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveScheme_BevorzugtGruppenspezifischesSchemaVorVoreinstellung()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var grades = new FakeGrades();
|
||
var schemes = new FakeSchemes();
|
||
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
|
||
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 0, ParticipationPercent = 0, OtherPercent = 100 });
|
||
|
||
var vm = BuildViewModel(grades, schemes, new FakeReportGrades(), exams, results);
|
||
|
||
// Gruppenspezifisches Schema gewichtet nur Klausuren (Ø3 aus Note 2 und 4) -> exakt "3"
|
||
Assert.Contains("Klausuren 100", vm.SchemeSummary);
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
Assert.Equal("3", anna.CalculatedValue);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveScheme_OhneGruppenschema_VerwendetVoreinstellungDesGruppentyps()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var schemes = new FakeSchemes();
|
||
schemes.SetDefault(GroupType.Class, new GradingScheme { ExamsPercent = 70, ParticipationPercent = 20, OtherPercent = 10 });
|
||
|
||
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
|
||
|
||
Assert.Contains("Klausuren 70", vm.SchemeSummary);
|
||
}
|
||
|
||
[Fact]
|
||
public void ResolveScheme_OhneJedeVoreinstellung_VerwendetHartcodiertenFallback()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
|
||
|
||
Assert.Contains("Klausuren 50", vm.SchemeSummary);
|
||
Assert.Contains("Mitarbeit 40", vm.SchemeSummary);
|
||
Assert.Contains("Sonstige 10", vm.SchemeSummary);
|
||
}
|
||
|
||
[Fact]
|
||
public void Save_UebersteuernOhneBegruendung_LiefertValidierungsfehlerUndSpeichertNicht()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var reportGrades = new FakeReportGrades();
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
|
||
anna.OverrideValue = "1";
|
||
anna.SaveCommand.Execute(null);
|
||
|
||
Assert.False(string.IsNullOrEmpty(anna.ValidationMessage));
|
||
Assert.Equal(0, reportGrades.SavedCount);
|
||
}
|
||
|
||
[Fact]
|
||
public void Save_UebersteuernMitBegruendung_AktualisiertFinalDisplay()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var reportGrades = new FakeReportGrades();
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
|
||
anna.OverrideValue = "1";
|
||
anna.OverrideReason = "Mündliche Steigerung im letzten Quartal";
|
||
anna.SaveCommand.Execute(null);
|
||
|
||
Assert.True(string.IsNullOrEmpty(anna.ValidationMessage));
|
||
Assert.Equal("1", anna.FinalDisplay);
|
||
Assert.Equal(1, reportGrades.SavedCount);
|
||
}
|
||
|
||
[Fact]
|
||
public void ToggleLock_FriertDenStandEinUndIgnoriertSpaetereNeuberechnung()
|
||
{
|
||
var (exam1, _, exams, results) = BuildExams();
|
||
var reportGrades = new FakeReportGrades();
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, exams, results);
|
||
var anna = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
anna.OverrideValue = "1";
|
||
anna.OverrideReason = "Testgrund";
|
||
anna.SaveCommand.Execute(null);
|
||
|
||
anna.ToggleLockCommand.Execute(null);
|
||
|
||
// Neue, bessere Klausurnote nachtragen -> würde die Berechnung ändern, wenn ungesperrt.
|
||
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "1" });
|
||
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.H1);
|
||
vm.SelectedPeriod = vm.PeriodOptions.First(p => p.Period == ParticipationPeriod.FullYear);
|
||
|
||
var annaNachSperre = vm.Rows.Single(r => r.StudentId == Anna.Id);
|
||
Assert.True(annaNachSperre.IsLocked);
|
||
Assert.Equal("1", annaNachSperre.FinalDisplay);
|
||
}
|
||
|
||
[Fact]
|
||
public void Schuljahr_WirdAusHalbjahresnotenUndPruefungsnotenVerrechnet()
|
||
{
|
||
// H1: eine 1 und eine 2 -> 1,5 -> 2. H2: eine 4 -> 4.
|
||
var h1a = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
|
||
var h1b = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 11, 10), Title = "K2" };
|
||
var h2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2026, 3, 10), Title = "K3" };
|
||
var results = new FakeResults();
|
||
results.Add(new ExamResult { ExamId = h1a.Id, StudentId = Anna.Id, Grade = "1" });
|
||
results.Add(new ExamResult { ExamId = h1b.Id, StudentId = Anna.Id, Grade = "2" });
|
||
results.Add(new ExamResult { ExamId = h2.Id, StudentId = Anna.Id, Grade = "4" });
|
||
var reportGrades = new FakeReportGrades();
|
||
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), reportGrades, new FakeExams([h1a, h1b, h2]), results);
|
||
|
||
// Aus den Halbjahresnoten: (2 + 4) / 2 = 3 — nicht aus allen Arbeiten ((1+2+4)/3 = 2,33 -> 2).
|
||
Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
|
||
|
||
// Klasse 10: eine im Fach abgelegte Projektprüfung zählt wie ein drittes Halbjahr.
|
||
reportGrades.Save(new ReportGrade { StudentId = Anna.Id, GroupId = GroupId, Period = "Gesamtes Schuljahr",
|
||
ProjectExamGrade = "6" });
|
||
vm.Refresh();
|
||
|
||
// (2 + 4 + 6) / 3 = 4
|
||
Assert.Equal("4", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
|
||
}
|
||
|
||
[Fact]
|
||
public void ExportCsv_EnthaeltAlleSchuelerDerGruppe()
|
||
{
|
||
var (_, _, exams, results) = BuildExams();
|
||
var vm = BuildViewModel(new FakeGrades(), new FakeSchemes(), new FakeReportGrades(), exams, results);
|
||
|
||
var csv = vm.ExportCsv();
|
||
|
||
Assert.Contains("Beispiel, Anna", csv);
|
||
Assert.Contains("Muster, Ben", csv);
|
||
}
|
||
|
||
[Fact]
|
||
public void RoundingRule_Paedagogisch_RundetGrenzfallZurBesserenNote()
|
||
{
|
||
var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 10), Title = "K1" };
|
||
var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Date = new DateOnly(2025, 9, 24), Title = "K2" };
|
||
var exams = new FakeExams([exam1, exam2]);
|
||
var results = new FakeResults();
|
||
// Ø exakt 2.5 -> kaufmännisch 3, pädagogisch 2 (bessere Note)
|
||
results.Add(new ExamResult { ExamId = exam1.Id, StudentId = Anna.Id, Grade = "2" });
|
||
results.Add(new ExamResult { ExamId = exam2.Id, StudentId = Anna.Id, Grade = "3" });
|
||
var schemes = new FakeSchemes();
|
||
schemes.SetForGroup(GroupId, new GradingScheme { ExamsPercent = 100, ParticipationPercent = 0, OtherPercent = 0 });
|
||
var vm = BuildViewModel(new FakeGrades(), schemes, new FakeReportGrades(), exams, results);
|
||
|
||
Assert.Equal("3", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
|
||
|
||
vm.RoundingRuleName = "Pädagogisch";
|
||
|
||
Assert.Equal("2", vm.Rows.Single(r => r.StudentId == Anna.Id).CalculatedValue);
|
||
}
|
||
}
|