Datenschutzkonforme Einzelansicht für Klausurergebnisse
CI / build-and-test (push) Canceled after 0s

Neuer Dialog ExamRevealDialog: zeigt zunächst nur den Namen eines Schülers,
erst nach „Aufdecken“ Note, Punktzahl und Punkte je Aufgabe. Blättern und
gezielte Auswahl oben; jeder Schülerwechsel deckt wieder zu. Erreichbar über
„Einzelansicht“ im Klausuren-Tab und auf der Klausuren-Hauptseite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-06 08:15:45 +02:00
1 parent c0f8b98202
commit 4f61c1fbd2
11 files changed
+392

No files matched your search

@@ -0,0 +1,94 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// Deckt 1.7.1 (datenschutzkonforme Einzelansicht) ab.
public sealed class ExamRevealDialogViewModelTests
{
private static (ExamRevealDialogViewModel Vm, Student Anna, Student Ben) Build()
{
var group = new LearningGroup { Name = "Q1 Chemie" };
var anna = new Student { FirstName = "Anna", LastName = "Bauer" };
var ben = new Student { FirstName = "Ben", LastName = "Cordes" };
var withoutResult = new Student { FirstName = "Cara", LastName = "Diehl" };
var exam = new Exam
{
GroupId = group.Id,
Title = "Klausur 1",
Tasks =
[
new ExamTask { Nr = 1, MaxPoints = 10 },
new ExamTask { Nr = 2, MaxPoints = 10, Variant = ExamVariant.A },
new ExamTask { Nr = 3, MaxPoints = 20, Variant = ExamVariant.B },
],
GradingKey = GradingService.DefaultKey1To6(),
};
var results = new FakeResults();
// Bewusst in "falscher" Reihenfolge eingefügt — die Liste sortiert nach Nachname.
results.Add(new ExamResult { ExamId = exam.Id, StudentId = ben.Id, Absent = true });
results.Add(new ExamResult
{
ExamId = exam.Id, StudentId = anna.Id, Points = [7.5, 9, 0], TotalPoints = 16.5,
Grade = "2", Variant = ExamVariant.A,
});
var vm = new ExamRevealDialogViewModel(results, new FakeStudents([ben, withoutResult, anna]),
new GradingService(), exam);
return (vm, anna, ben);
}
[Fact]
public void Start_ZeigtErstenSchülerZugedeckt_UndNurSchülerMitErgebnis()
{
var (vm, _, _) = Build();
Assert.Equal(["Bauer, Anna", "Cordes, Ben"], vm.Students.Select(s => s.ListName));
Assert.Equal("Anna Bauer", vm.SelectedStudent!.DisplayName);
Assert.False(vm.IsRevealed);
Assert.Equal("1 / 2", vm.PositionLabel);
}
[Fact]
public void Ergebnis_ZeigtNotePunkteUndNurAufgabenDerGeschriebenenFassung()
{
var (vm, _, _) = Build();
var anna = vm.SelectedStudent!;
Assert.Equal("2", anna.GradeDisplay);
Assert.Equal("16,5 von 20 Punkten", anna.PointsDisplay);
Assert.Equal(
[new ExamRevealTaskRow("Aufgabe 1", "7,5", "10"), new ExamRevealTaskRow("Aufgabe 2", "9", "10")],
anna.Tasks);
}
[Fact]
public void Schülerwechsel_DecktWiederZu()
{
var (vm, _, _) = Build();
vm.RevealCommand.Execute(null);
Assert.True(vm.IsRevealed);
vm.NextCommand.Execute(null);
Assert.False(vm.IsRevealed);
Assert.Equal("Ben Cordes", vm.SelectedStudent!.DisplayName);
Assert.True(vm.SelectedStudent.Absent);
Assert.Empty(vm.SelectedStudent.Tasks);
Assert.False(vm.NextCommand.CanExecute(null));
Assert.True(vm.PreviousCommand.CanExecute(null));
}
[Fact]
public void Zudecken_VerstecktErgebnisWieder()
{
var (vm, _, _) = Build();
vm.RevealCommand.Execute(null);
vm.HideCommand.Execute(null);
Assert.False(vm.IsRevealed);
}
}
@@ -42,6 +42,7 @@ public partial class ExamsOverviewViewModel : ObservableObject
public Func<Exam, Task>? OnGradeExam { get; set; }
public Func<Exam, Task>? OnEvaluateExam { get; set; }
public Func<Exam, Task>? OnRevealExam { get; set; }
public Action<Guid>? OnNavigateToGroup { get; set; }
public Action? OnNavigateToGroups { get; set; }
public bool HasNoExams => Rows.Count == 0;
@@ -169,6 +170,13 @@ public partial class ExamsOverviewViewModel : ObservableObject
Load();
}
[RelayCommand]
private async Task RevealSelected()
{
if (SelectedRow is null || OnRevealExam is null) return;
await OnRevealExam(SelectedRow.Exam);
}
[RelayCommand]
private void GoToGroup()
{
@@ -0,0 +1,125 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Groups;
// ── Datenschutzkonforme Einzelansicht (1.7) ──────────────────────────────────
/// Zeigt die Leistung genau eines Schülers — zunächst nur den Namen, das Ergebnis erst nach
/// "Aufdecken". So kann ein einzelner Schüler nach vorne geholt werden, ohne dass er (oder die
/// Klasse über den Beamer) Ergebnisse anderer sieht. Rein lesend.
public partial class ExamRevealDialogViewModel : ObservableObject
{
private readonly Exam _exam;
public string ExamTitle => _exam.Title;
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy");
public List<ExamRevealStudent> Students { get; } = [];
public bool HasStudents => Students.Count > 0;
[ObservableProperty] private ExamRevealStudent? _selectedStudent;
[ObservableProperty] private bool _isRevealed;
public string PositionLabel => SelectedStudent is null
? ""
: $"{Students.IndexOf(SelectedStudent) + 1} / {Students.Count}";
public ExamRevealDialogViewModel(IExamResultRepository results, IStudentRepository students,
GradingService grading, Exam exam)
{
_exam = exam;
var tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
var labels = ExamScoringService.TaskLabels(tasks);
var enrolled = students.GetByGroup(exam.GroupId).ToDictionary(s => s.Id);
// Grundlage sind die eingetragenen Ergebnisse: wer keins hat (anderes Niveau, nie
// angefasste Zeile), hat hier auch nichts zu sehen. Ausgetretene Schüler mit Ergebnis
// bleiben über GetById auffindbar.
Students = results.GetByExam(exam.Id)
.Select(r => (Result: r, Student: enrolled.GetValueOrDefault(r.StudentId) ?? students.GetById(r.StudentId)))
.Where(x => x.Student is not null)
.OrderBy(x => x.Student!.LastName).ThenBy(x => x.Student!.FirstName)
.Select(x => new ExamRevealStudent(x.Student!, x.Result, tasks, labels, exam.GradingKey, grading))
.ToList();
_selectedStudent = Students.FirstOrDefault();
}
/// Beim Wechsel wird immer wieder zugedeckt — sonst stünde beim Durchblättern sofort das
/// Ergebnis des nächsten Schülers offen auf dem Bildschirm.
partial void OnSelectedStudentChanged(ExamRevealStudent? value)
{
IsRevealed = false;
OnPropertyChanged(nameof(PositionLabel));
PreviousCommand.NotifyCanExecuteChanged();
NextCommand.NotifyCanExecuteChanged();
RevealCommand.NotifyCanExecuteChanged();
}
private int SelectedIndex => SelectedStudent is null ? -1 : Students.IndexOf(SelectedStudent);
private bool CanGoPrevious() => SelectedIndex > 0;
private bool CanGoNext() => SelectedIndex >= 0 && SelectedIndex < Students.Count - 1;
[RelayCommand(CanExecute = nameof(CanGoPrevious))]
private void Previous() => SelectedStudent = Students[SelectedIndex - 1];
[RelayCommand(CanExecute = nameof(CanGoNext))]
private void Next() => SelectedStudent = Students[SelectedIndex + 1];
[RelayCommand(CanExecute = nameof(CanReveal))]
private void Reveal() => IsRevealed = true;
private bool CanReveal() => SelectedStudent is not null;
[RelayCommand]
private void Hide() => IsRevealed = false;
}
/// Ergebnis eines Schülers, fertig aufbereitet für die Einzelansicht.
public class ExamRevealStudent
{
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
/// "Nachname, Vorname" — für die sortierte Auswahlliste.
public string ListName { get; }
/// "Vorname Nachname" — für die große Anzeige.
public string DisplayName { get; }
public bool Absent { get; }
public bool HasGrade => !Absent;
public string GradeDisplay { get; }
public string PointsDisplay { get; }
public List<ExamRevealTaskRow> Tasks { get; } = [];
public ExamRevealStudent(Student student, ExamResult result, IReadOnlyList<ExamTask> orderedTasks,
IReadOnlyList<string> taskLabels, List<GradingKeyEntry> gradingKey, GradingService grading)
{
ListName = student.FullName;
DisplayName = $"{student.FirstName} {student.LastName}".Trim();
Absent = result.Absent;
if (Absent)
{
GradeDisplay = "abwesend";
PointsDisplay = "";
return;
}
var maximum = ExamScoringService.Maximum(orderedTasks, result.Variant);
GradeDisplay = result.Grade ?? grading.CalculateGrade(result.TotalPoints, maximum, gradingKey);
PointsDisplay = $"{Format(result.TotalPoints)} von {Format(maximum)} Punkten";
// Nur die Aufgaben der geschriebenen Fassung — die der anderen gab es für den Schüler nicht.
for (var i = 0; i < orderedTasks.Count; i++)
{
if (!ExamScoringService.AppliesTo(orderedTasks[i], result.Variant)) continue;
var points = i < result.Points.Count ? result.Points[i] : 0;
Tasks.Add(new ExamRevealTaskRow(taskLabels[i], Format(points), Format(orderedTasks[i].MaxPoints)));
}
}
private static string Format(double value) => value.ToString("0.##", German);
}
public record ExamRevealTaskRow(string Label, string Points, string MaxPoints);
@@ -234,6 +234,7 @@ public partial class GroupDetailViewModel : ObservableObject
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
public Func<Exam, Task>? OnGradeExam { get; set; }
public Func<Exam, Task>? OnEvaluateExam { get; set; }
public Func<Exam, Task>? OnRevealExam { get; set; }
public Func<Task<bool>>? OnConfirmReactivate { get; set; }
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
@@ -411,6 +412,15 @@ public partial class GroupDetailViewModel : ObservableObject
await OnEvaluateExam(exam);
}
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task RevealExam()
{
if (SelectedExam is null || OnRevealExam is null) return;
var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return;
await OnRevealExam(exam);
}
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task DuplicateExam()
{
@@ -485,6 +495,7 @@ public partial class GroupDetailViewModel : ObservableObject
EditExamCommand.NotifyCanExecuteChanged();
GradeExamCommand.NotifyCanExecuteChanged();
EvaluateExamCommand.NotifyCanExecuteChanged();
RevealExamCommand.NotifyCanExecuteChanged();
DuplicateExamCommand.NotifyCanExecuteChanged();
DeleteExamCommand.NotifyCanExecuteChanged();
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
@@ -514,6 +525,7 @@ public partial class GroupDetailViewModel : ObservableObject
EditExamCommand.NotifyCanExecuteChanged();
GradeExamCommand.NotifyCanExecuteChanged();
EvaluateExamCommand.NotifyCanExecuteChanged();
RevealExamCommand.NotifyCanExecuteChanged();
DuplicateExamCommand.NotifyCanExecuteChanged();
DeleteExamCommand.NotifyCanExecuteChanged();
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
@@ -174,6 +174,9 @@
IsVisible="{Binding ShowCorrectionProgress}"/>
<Button Content="Auswertung" Command="{Binding EvaluateSelectedCommand}"
IsVisible="{Binding ShowGradeSummary}"/>
<Button Content="Einzelansicht" Command="{Binding RevealSelectedCommand}"
IsVisible="{Binding ShowGradeSummary}"
ToolTip.Tip="Zeigt das Ergebnis eines einzelnen Schülers – zunächst nur den Namen, erst nach „Aufdecken“ Note und Punkte."/>
<Button Content="Zum Kurs →" Command="{Binding GoToGroupCommand}" HorizontalAlignment="Right"/>
</StackPanel>
</StackPanel>
@@ -23,6 +23,7 @@ public partial class ExamsOverviewView : UserControl
{
vm.OnGradeExam = ShowGradeExamDialog;
vm.OnEvaluateExam = ShowEvaluateExamDialog;
vm.OnRevealExam = ShowRevealExamDialog;
vm.OnNavigateToGroup = groupId => App.Services.GetRequiredService<MainWindowViewModel>()
.NavigateToGroupDetail(groupId, GroupDetailKlausurenTabIndex);
}
@@ -62,4 +63,17 @@ public partial class ExamsOverviewView : UserControl
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
private async Task ShowRevealExamDialog(Exam exam)
{
var dialogVm = new ExamRevealDialogViewModel(
App.Services.GetRequiredService<IExamResultRepository>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<GradingService>(),
exam);
var dialog = new ExamRevealDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
}
@@ -0,0 +1,96 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.ExamRevealDialog"
x:DataType="vm:ExamRevealDialogViewModel"
Title="{Binding ExamTitle}"
Width="680" Height="780" MinWidth="480" MinHeight="420"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,*,Auto" Margin="24">
<!-- Durchblättern und gezielte Auswahl -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto,Auto" IsEnabled="{Binding HasStudents}">
<Button Grid.Column="0" Content="◀ Zurück" Command="{Binding PreviousCommand}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding Students}" SelectedItem="{Binding SelectedStudent}"
HorizontalAlignment="Stretch" Margin="8,0">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:ExamRevealStudent">
<TextBlock Text="{Binding ListName}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Grid.Column="2" Text="{Binding PositionLabel}" FontSize="12" Opacity="0.6"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<Button Grid.Column="3" Content="Weiter ▶" Command="{Binding NextCommand}"/>
</Grid>
<TextBlock Grid.Row="1" Text="Für diese Arbeit sind noch keine Ergebnisse eingetragen."
Classes="emptyhint" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding !HasStudents}"/>
<Grid Grid.Row="1" RowDefinitions="Auto,*" Margin="0,28,0,0" IsVisible="{Binding HasStudents}">
<TextBlock Grid.Row="0" Text="{Binding SelectedStudent.DisplayName}" FontSize="36" FontWeight="SemiBold"
HorizontalAlignment="Center" TextAlignment="Center" TextWrapping="Wrap"/>
<!-- Zugedeckt: nur der Name -->
<Button Grid.Row="1" Content="Aufdecken" Command="{Binding RevealCommand}"
FontSize="20" Padding="36,14" HorizontalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding !IsRevealed}"/>
<!-- Aufgedeckt: Note, Punkte, Aufgaben -->
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*,Auto" IsVisible="{Binding IsRevealed}">
<TextBlock Grid.Row="0" Text="{Binding SelectedStudent.GradeDisplay}" FontSize="110" FontWeight="Bold"
HorizontalAlignment="Center" Margin="0,8,0,0"
IsVisible="{Binding SelectedStudent.HasGrade}"/>
<TextBlock Grid.Row="0" Text="abwesend" FontSize="36" Opacity="0.6"
HorizontalAlignment="Center" Margin="0,48,0,0"
IsVisible="{Binding SelectedStudent.Absent}"/>
<TextBlock Grid.Row="1" Text="{Binding SelectedStudent.PointsDisplay}" FontSize="22"
HorizontalAlignment="Center" Margin="0,0,0,20"
IsVisible="{Binding SelectedStudent.HasGrade}"/>
<ScrollViewer Grid.Row="2" IsVisible="{Binding SelectedStudent.HasGrade}">
<StackPanel MaxWidth="480" Margin="0,0,12,0">
<Grid ColumnDefinitions="*,90,90" Margin="0,0,0,4">
<TextBlock Grid.Column="0" Text="AUFGABE" FontSize="10" FontWeight="Bold" Opacity="0.5"/>
<TextBlock Grid.Column="1" Text="ERREICHT" FontSize="10" FontWeight="Bold" Opacity="0.5"
TextAlignment="Right"/>
<TextBlock Grid.Column="2" Text="MAXIMAL" FontSize="10" FontWeight="Bold" Opacity="0.5"
TextAlignment="Right"/>
</Grid>
<ItemsControl ItemsSource="{Binding SelectedStudent.Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ExamRevealTaskRow">
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,1,0,0" Padding="0,6">
<Grid ColumnDefinitions="*,90,90">
<TextBlock Grid.Column="0" Text="{Binding Label}" FontSize="16"
TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Column="1" Text="{Binding Points}" FontSize="16" FontWeight="SemiBold"
TextAlignment="Right"/>
<TextBlock Grid.Column="2" Text="{Binding MaxPoints}" FontSize="16" Opacity="0.6"
TextAlignment="Right"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
<Button Grid.Row="3" Content="Zudecken" Command="{Binding HideCommand}"
FontSize="16" Padding="28,10" HorizontalAlignment="Center" Margin="0,16,0,0"/>
</Grid>
</Grid>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="0,16,0,0">
<TextBlock Grid.Column="0" FontSize="12" Opacity="0.6" VerticalAlignment="Center">
<Run Text="{Binding ExamTitle}"/>
<Run Text=" · "/>
<Run Text="{Binding ExamDateLabel}"/>
</TextBlock>
<Button Grid.Column="1" Content="Schließen" Click="OnClose"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace LehrerApp.Desktop.Views.Groups;
public partial class ExamRevealDialog : Window
{
public ExamRevealDialog() => InitializeComponent();
private void OnClose(object? sender, RoutedEventArgs e) => Close();
}
@@ -118,6 +118,8 @@
IsEnabled="{Binding IsEditable}">
<Button Content="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
<Button Content="Auswertung" Command="{Binding EvaluateExamCommand}"/>
<Button Content="Einzelansicht" Command="{Binding RevealExamCommand}"
ToolTip.Tip="Zeigt das Ergebnis eines einzelnen Schülers – zunächst nur den Namen, erst nach „Aufdecken“ Note und Punkte."/>
<Button Content="Bearbeiten" Command="{Binding EditExamCommand}"/>
<Button Content="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<SplitButton Content="Status ▸" Command="{Binding AdvanceExamStatusCommand}">
@@ -164,6 +166,7 @@
<ContextMenu>
<MenuItem Header="Punkte eingeben" Command="{Binding GradeExamCommand}"/>
<MenuItem Header="Auswertung" Command="{Binding EvaluateExamCommand}"/>
<MenuItem Header="Einzelansicht" Command="{Binding RevealExamCommand}"/>
<MenuItem Header="Bearbeiten" Command="{Binding EditExamCommand}"/>
<MenuItem Header="Duplizieren" Command="{Binding DuplicateExamCommand}"/>
<MenuItem Header="Status">
@@ -37,6 +37,7 @@ public partial class GroupDetailView : UserControl
vm.OnConfirmDeleteExam = ShowDeleteExamDialog;
vm.OnGradeExam = ShowGradeExamDialog;
vm.OnEvaluateExam = ShowEvaluateExamDialog;
vm.OnRevealExam = ShowRevealExamDialog;
vm.OnConfirmReactivate = ShowReactivateConfirmDialog;
vm.OverviewTab.OnNavigateToWorkload = () =>
App.Services.GetRequiredService<MainWindowViewModel>().NavigateToWorkload();
@@ -304,4 +305,17 @@ public partial class GroupDetailView : UserControl
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
private async Task ShowRevealExamDialog(Exam exam)
{
var dialogVm = new ExamRevealDialogViewModel(
App.Services.GetRequiredService<IExamResultRepository>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<GradingService>(),
exam);
var dialog = new ExamRevealDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is not null) await dialog.ShowDialog(owner);
}
}
+12
View File
@@ -196,6 +196,18 @@ unterrichten oft mehrere Kurse gleichzeitig und brauchen einen gruppenübergreif
Vorausschau) mit auffälliger Häufung unter den noch **geplanten** Klausuren auflistet —
bereits durchgeführte/korrigierte Klausuren deckt schon die Karte "Offene Korrekturen" ab.
### 1.7 Datenschutzkonforme Einzelansicht
- [x] **1.7.1** Nutzerwunsch: einzelne Schüler nach vorne holen und ihnen ihre Leistung zeigen,
ohne dass Ergebnisse anderer sichtbar sind. Neuer Dialog `ExamRevealDialog`
(`ExamRevealDialogViewModel`), erreichbar über "Einzelansicht" im Klausuren-Tab
(Button/Kontextmenü) und auf der Klausuren-Hauptseite. Zeigt zunächst nur den Namen; oben
Zurück/Weiter und eine Combo zur gezielten Auswahl. "Aufdecken" blendet groß die Note,
darunter "x von y Punkten" und eine Tabelle mit erreichten/maximalen Punkten je Aufgabe
ein, "Zudecken" versteckt alles wieder. Jeder Schülerwechsel deckt automatisch wieder zu.
Rein lesend; gelistet werden Schüler mit eingetragenem Ergebnis (abwesende zeigen
"abwesend"), bei A/B-Fassungen nur die Aufgaben der geschriebenen Fassung.
- [ ] **1.7.2** Einzelansicht auf weitere Leistungen ausdehnen (Mitarbeit, Zeugnisnote).
---
## 2. Noten & Zeugnisnoten