Restructure: Move 52 files into 7 domain packages
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 28s
CI / test-python-klausur (push) Failing after 2m22s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 23s

korrektur/ zeugnis/ admin/ compliance/ worksheet/ training/ metrics/
52 shims, relative imports, RAG untouched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 22:10:48 +02:00
parent 0504d22b8e
commit 165c493d1e
111 changed files with 11859 additions and 11609 deletions

View File

@@ -1,297 +1,4 @@
"""
PDF Export - Klausur overview and annotations PDF generation.
Generates:
- Klausur overview with grade distribution for all students
- Annotations PDF for a single student
"""
import io
from datetime import datetime
from typing import Dict, List, Optional, Any
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable
)
from pdf_export_styles import (
GRADE_POINTS_TO_NOTE,
CRITERIA_DISPLAY_NAMES,
get_custom_styles,
)
def generate_klausur_overview_pdf(
klausur_data: Dict[str, Any],
students: List[Dict[str, Any]],
fairness_data: Optional[Dict[str, Any]] = None
) -> bytes:
"""
Generate an overview PDF for an entire Klausur with all student grades.
Args:
klausur_data: Klausur metadata
students: List of all student work data
fairness_data: Optional fairness analysis data
Returns:
PDF as bytes
"""
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=1.5*cm,
leftMargin=1.5*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
styles = get_custom_styles()
story = []
# Header
story.append(Paragraph("Notenuebersicht", styles['GutachtenTitle']))
story.append(Paragraph(f"{klausur_data.get('subject', 'Deutsch')} - {klausur_data.get('title', '')}", styles['GutachtenSubtitle']))
story.append(Spacer(1, 0.5*cm))
# Meta information
meta_data = [
["Schuljahr:", f"{klausur_data.get('year', 2025)}"],
["Kurs:", klausur_data.get('semester', 'Abitur')],
["Anzahl Arbeiten:", str(len(students))],
["Stand:", datetime.now().strftime("%d.%m.%Y")]
]
meta_table = Table(meta_data, colWidths=[4*cm, 10*cm])
meta_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
]))
story.append(meta_table)
story.append(Spacer(1, 0.5*cm))
# Statistics (if fairness data available)
if fairness_data and fairness_data.get('statistics'):
_add_statistics(story, styles, fairness_data['statistics'])
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0')))
story.append(Spacer(1, 0.5*cm))
# Student grades table
sorted_students = sorted(students, key=lambda s: s.get('grade_points', 0), reverse=True)
_add_student_table(story, styles, sorted_students)
# Grade distribution
_add_grade_distribution(story, styles, sorted_students)
# Footer
story.append(Spacer(1, 1*cm))
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cbd5e0')))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
f"Erstellt am {datetime.now().strftime('%d.%m.%Y um %H:%M Uhr')} | BreakPilot Abiturkorrektur-System",
styles['MetaText']
))
# Build PDF
doc.build(story)
buffer.seek(0)
return buffer.getvalue()
def _add_statistics(story, styles, stats):
"""Add statistics section."""
story.append(Paragraph("Statistik", styles['SectionHeader']))
stats_data = [
["Durchschnitt:", f"{stats.get('average_grade', 0):.1f} Punkte"],
["Minimum:", f"{stats.get('min_grade', 0)} Punkte"],
["Maximum:", f"{stats.get('max_grade', 0)} Punkte"],
["Standardabweichung:", f"{stats.get('standard_deviation', 0):.2f}"],
]
stats_table = Table(stats_data, colWidths=[4*cm, 4*cm])
stats_table.setStyle(TableStyle([
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 9),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#f7fafc')),
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
]))
story.append(stats_table)
story.append(Spacer(1, 0.5*cm))
def _add_student_table(story, styles, sorted_students):
"""Add student grades table."""
story.append(Paragraph("Einzelergebnisse", styles['SectionHeader']))
story.append(Spacer(1, 0.2*cm))
table_data = [["#", "Name", "Rohpunkte", "Notenpunkte", "Note", "Status"]]
for idx, student in enumerate(sorted_students, 1):
grade_points = student.get('grade_points', 0)
grade_note = GRADE_POINTS_TO_NOTE.get(grade_points, "-")
raw_points = student.get('raw_points', 0)
status = student.get('status', 'unknown')
status_display = {
'completed': 'Abgeschlossen',
'first_examiner': 'In Korrektur',
'second_examiner': 'Zweitkorrektur',
'uploaded': 'Hochgeladen',
'ocr_complete': 'OCR fertig',
'analyzing': 'Wird analysiert'
}.get(status, status)
table_data.append([
str(idx),
student.get('student_name', 'Anonym'),
f"{raw_points}/100",
str(grade_points),
grade_note,
status_display
])
student_table = Table(table_data, colWidths=[1*cm, 5*cm, 2.5*cm, 3*cm, 2*cm, 3*cm])
student_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c5282')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 9),
('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('FONTSIZE', (0, 1), (-1, -1), 9),
('ALIGN', (0, 1), (0, -1), 'CENTER'),
('ALIGN', (2, 1), (4, -1), 'CENTER'),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('TOPPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f7fafc')]),
]))
story.append(student_table)
def _add_grade_distribution(story, styles, sorted_students):
"""Add grade distribution table."""
story.append(Spacer(1, 0.5*cm))
story.append(Paragraph("Notenverteilung", styles['SectionHeader']))
story.append(Spacer(1, 0.2*cm))
grade_counts = {}
for student in sorted_students:
gp = student.get('grade_points', 0)
grade_counts[gp] = grade_counts.get(gp, 0) + 1
dist_data = [["Punkte", "Note", "Anzahl"]]
for points in range(15, -1, -1):
if points in grade_counts:
note = GRADE_POINTS_TO_NOTE.get(points, "-")
count = grade_counts[points]
dist_data.append([str(points), note, str(count)])
if len(dist_data) > 1:
dist_table = Table(dist_data, colWidths=[2.5*cm, 2.5*cm, 2.5*cm])
dist_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c5282')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, -1), 9),
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
('TOPPADDING', (0, 0), (-1, -1), 4),
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
]))
story.append(dist_table)
def generate_annotations_pdf(
student_data: Dict[str, Any],
klausur_data: Dict[str, Any],
annotations: List[Dict[str, Any]]
) -> bytes:
"""
Generate a PDF with all annotations for a student work.
Args:
student_data: Student work data
klausur_data: Klausur metadata
annotations: List of all annotations
Returns:
PDF as bytes
"""
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=A4,
rightMargin=2*cm,
leftMargin=2*cm,
topMargin=2*cm,
bottomMargin=2*cm
)
styles = get_custom_styles()
story = []
# Header
story.append(Paragraph("Anmerkungen zur Klausur", styles['GutachtenTitle']))
story.append(Paragraph(f"{student_data.get('student_name', 'Anonym')}", styles['GutachtenSubtitle']))
story.append(Spacer(1, 0.5*cm))
if not annotations:
story.append(Paragraph("<i>Keine Anmerkungen vorhanden.</i>", styles['GutachtenBody']))
else:
# Group by type
by_type = {}
for ann in annotations:
ann_type = ann.get('type', 'comment')
if ann_type not in by_type:
by_type[ann_type] = []
by_type[ann_type].append(ann)
for ann_type, anns in by_type.items():
type_name = CRITERIA_DISPLAY_NAMES.get(ann_type, ann_type.replace('_', ' ').title())
story.append(Paragraph(f"{type_name} ({len(anns)})", styles['SectionHeader']))
story.append(Spacer(1, 0.2*cm))
sorted_anns = sorted(anns, key=lambda a: (a.get('page', 0), a.get('position', {}).get('y', 0)))
for idx, ann in enumerate(sorted_anns, 1):
page = ann.get('page', 1)
text = ann.get('text', '')
suggestion = ann.get('suggestion', '')
severity = ann.get('severity', 'minor')
ann_text = f"<b>[S.{page}]</b> {text}"
if suggestion:
ann_text += f" -> <i>{suggestion}</i>"
if severity == 'critical':
ann_text = f"<font color='red'>{ann_text}</font>"
elif severity == 'major':
ann_text = f"<font color='orange'>{ann_text}</font>"
story.append(Paragraph(f"{idx}. {ann_text}", styles['ListItem']))
story.append(Spacer(1, 0.3*cm))
# Footer
story.append(Spacer(1, 1*cm))
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor('#cbd5e0')))
story.append(Spacer(1, 0.2*cm))
story.append(Paragraph(
f"Erstellt am {datetime.now().strftime('%d.%m.%Y um %H:%M Uhr')} | BreakPilot Abiturkorrektur-System",
styles['MetaText']
))
# Build PDF
doc.build(story)
buffer.seek(0)
return buffer.getvalue()
# Backward-compat shim -- module moved to korrektur/pdf_export_overview.py
import importlib as _importlib
import sys as _sys
_sys.modules[__name__] = _importlib.import_module("korrektur.pdf_export_overview")