Datenschutzkonforme Einzelansicht für Klausurergebnisse
CI / build-and-test (push) Canceled after 0s
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:
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user