Klausur-Deckblatt: Titel im Dialog je Druck anpassbar
CI / build-and-test (push) Canceled after 0s

Der interne Klausurtitel passt nicht immer aufs Deckblatt. Das neue Feld
ist mit ihm vorbelegt, ändert nur Exam.Title für diesen Druck und fällt
leer gelassen auf den internen Titel zurück.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-08 08:55:10 +02:00
1 parent 2293ddb84d
commit 24a30881f3
5 files changed
+53 -7

No files matched your search

@@ -204,6 +204,35 @@ public sealed class ExamCoverSheetTests : IDisposable
Assert.Equal(new TextValue("G-Kurs"), Values(Niveau.E)["Exam.Course"]);
}
[Fact]
public void Platzhalter_TitelAusDemDialog_LeerNimmtDenKlausurtitel()
{
var store = Store();
Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover);
var template = store.Load(store.GetTemplates().Single());
Dictionary<string, PlaceholderValue> Values(string? title) => ExamCoverPlaceholderBuilder.Build(
_exam, _group, "Chemie", _anna, null, false, new GradingService(), template, new DateOnly(2026, 1, 1),
title: title);
Assert.Equal(new TextValue("Klassenarbeit Nr. 1"), Values(" Klassenarbeit Nr. 1 ")["Exam.Title"]);
Assert.Equal(new TextValue("Atombau und PSE"), Values(" ")["Exam.Title"]);
Assert.Equal(new TextValue("Atombau und PSE"), Values(null)["Exam.Title"]);
}
[Fact]
public void Dialog_TitelIstMitDemKlausurtitelVorbelegt()
{
var store = Store();
Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover);
var vm = Build([store]);
Assert.Equal("Atombau und PSE", vm.CoverTitle);
vm.CoverTitle = "Klassenarbeit Nr. 1";
Assert.Equal("Atombau und PSE", _exam.Title);
}
// Der Vorlagen-Designer bietet genau diesen Katalog an - liefert die Hauptapp einen Namen
// nicht oder mit anderem Typ, scheitert die Vorlage erst beim Drucken.
[Fact]
@@ -22,9 +22,12 @@ public static class ExamCoverPlaceholderBuilder
/// eingetragen, wenn <paramref name="fillResults"/> gesetzt und das Ergebnis korrigiert ist.</param>
/// <param name="studentNiveau">Niveau des Schülers in dieser Lerngruppe, falls die Arbeit
/// selbst keinem Niveau zugeordnet ist.</param>
/// <param name="title">Titel für das Deckblatt, falls er vom internen Klausurtitel abweichen
/// soll; null oder leer nimmt den Klausurtitel.</param>
public static Dictionary<string, PlaceholderValue> Build(Exam exam, LearningGroup group, string subjectName,
Student student, ExamResult? result, bool fillResults, GradingService grading, LoadedTemplate template,
DateOnly today, Niveau? studentNiveau = null, string teacherAbbreviation = "")
DateOnly today, Niveau? studentNiveau = null, string teacherAbbreviation = "",
string? title = null)
{
var tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
var variant = result?.Variant ?? ExamVariant.A;
@@ -34,7 +37,7 @@ public static class ExamCoverPlaceholderBuilder
var values = LetterPlaceholderBuilder.BuildStandardValues(student, contact: null, group, today, "", "");
values["Group.GradeLevel"] = new TextValue(group.GradeLevel > 0 ? group.GradeLevel.ToString() : "");
values["Subject.Name"] = new TextValue(subjectName);
values["Exam.Title"] = new TextValue(exam.Title);
values["Exam.Title"] = new TextValue(string.IsNullOrWhiteSpace(title) ? exam.Title : title.Trim());
values["Exam.Date"] = new DateValue(exam.Date);
values["Exam.Number"] = new TextValue(exam.ExamNumber?.ToString() ?? "");
values["Exam.Kind"] = new TextValue(exam.Kind.Label());
@@ -28,7 +28,11 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
/// <summary>Vorbelegt aus den Einstellungen, hier nur für diesen Druck änderbar (z. B. zwei
/// Kürzel bei einem gemeinsam unterrichteten Kurs) — die Einstellung bleibt unberührt.</summary>
[ObservableProperty] private string _teacherAbbreviation;
/// <summary>Vorbelegt mit dem internen Klausurtitel, hier nur für diesen Druck änderbar — der
/// Titel der Klausur bleibt unberührt. Leer gelassen gilt wieder der interne Titel.</summary>
[ObservableProperty] private string _coverTitle;
public string ExamTitle => _exam.Title;
public string ExamLabel => $"{_exam.Title} · {_group.Name} · {_exam.Date:dd.MM.yyyy}";
public ObservableCollection<LetterTemplateChoice> Templates { get; } = [];
public ObservableCollection<ExamCoverStudentChoice> Students { get; } = [];
@@ -41,6 +45,7 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
string teacherAbbreviation = "")
{
_teacherAbbreviation = teacherAbbreviation;
_coverTitle = exam.Title;
_exam = exam; _group = group; _subjectName = subjectName; _grading = grading; _renderer = renderer;
_loader = templateStores[0];
foreach (var template in templateStores.SelectMany(x => x.GetTemplates()).Where(x => x.IsExamCover)
@@ -76,7 +81,7 @@ public partial class ExamCoverSheetDialogViewModel : ObservableObject
var today = DateOnly.FromDateTime(DateTime.Now);
var data = included.Select(s => (ITemplateDataProvider)new LetterDataProvider(
ExamCoverPlaceholderBuilder.Build(_exam, _group, _subjectName, s.Model, s.Result, FillResults,
_grading, loaded, today, s.Niveau, TeacherAbbreviation))).ToList();
_grading, loaded, today, s.Niveau, TeacherAbbreviation, CoverTitle))).ToList();
var result = _renderer.RenderBatchToPdf(loaded, data);
File.WriteAllBytes(outputPath, result.Pdf);
@@ -4,7 +4,7 @@
x:Class="LehrerApp.Desktop.Views.Groups.ExamCoverSheetDialog"
x:DataType="vm:ExamCoverSheetDialogViewModel"
Title="Deckblätter drucken"
Width="460" Height="640"
Width="460" Height="680"
CanResize="False" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Margin="24">
@@ -21,9 +21,12 @@
<TextBlock Text="Noch keine Deckblatt-Vorlage importiert — in den Einstellungen unter „Briefvorlagen“ eine .lavorlage-Datei mit dem Metadatum report-type=exam-cover hinzufügen."
Classes="emptyhint" TextWrapping="Wrap"
IsVisible="{Binding HasNoTemplates}"/>
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
<TextBlock Grid.Column="0" Text="Kürzel" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding TeacherAbbreviation}" PlaceholderText="z. B. MON HED"
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="12" RowSpacing="10">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Titel" VerticalAlignment="Center"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding CoverTitle}" PlaceholderText="{Binding ExamTitle}"
ToolTip.Tip="Vorbelegt mit dem Titel der Klausur. Änderungen gelten nur für diesen Druck; leer gelassen steht der Klausurtitel auf dem Deckblatt."/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Kürzel" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TeacherAbbreviation}" PlaceholderText="z. B. MON HED"
ToolTip.Tip="Vorbelegt mit dem eigenen Kürzel aus den Einstellungen. Änderungen gelten nur für diesen Druck."/>
</Grid>
<CheckBox Content="Punkte und Note eintragen" IsChecked="{Binding FillResults}"
+6
View File
@@ -4854,6 +4854,12 @@ beides vor dem ersten produktiven Zwei-Geräte-Einsatz empfehlenswert nachzuhole
aus der Vorlage und ist im Dialog je Druck überschreibbar, ohne die Einstellung zu ändern —
für gemeinsam unterrichtete Kurse mit zwei Kürzeln.
Nachgezogen (Nutzer-Wunsch): Der Dialog hat zusätzlich ein Feld "Titel", vorbelegt mit dem
internen Klausurtitel und nur für diesen Druck änderbar (`CoverTitle` →
`ExamCoverPlaceholderBuilder.Build(title:)` → `Exam.Title`) — der interne Titel passt nicht
immer aufs Deckblatt. Die Klausur selbst und der vorgeschlagene Dateiname bleiben beim
internen Titel; ein leer gelassenes Feld fällt auf ihn zurück.
**Punktetabelle ohne Verkleinern:** `ExamPointsTableDrawingBuilder` zeichnet feste
Spaltenbreiten und bricht in weitere Blöcke um. Damit "Gesamtpunkte"/"Note" darunter
mitwandern, bekam `FLOWDRAWBOX` das Opt-in-Attribut `fit=content` (belegt nur die