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
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:
@@ -1,315 +1,4 @@
|
||||
"""
|
||||
PDF Export - Individual Gutachten PDF generation.
|
||||
|
||||
Generates a single student's Gutachten with criteria table,
|
||||
workflow info, and annotation summary.
|
||||
"""
|
||||
|
||||
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, KeepTogether
|
||||
)
|
||||
|
||||
from pdf_export_styles import (
|
||||
GRADE_POINTS_TO_NOTE,
|
||||
CRITERIA_DISPLAY_NAMES,
|
||||
CRITERIA_WEIGHTS,
|
||||
get_custom_styles,
|
||||
)
|
||||
|
||||
|
||||
def generate_gutachten_pdf(
|
||||
student_data: Dict[str, Any],
|
||||
klausur_data: Dict[str, Any],
|
||||
annotations: List[Dict[str, Any]] = None,
|
||||
workflow_data: Dict[str, Any] = None
|
||||
) -> bytes:
|
||||
"""
|
||||
Generate a PDF Gutachten for a single student.
|
||||
|
||||
Args:
|
||||
student_data: Student work data including criteria_scores, gutachten, grade_points
|
||||
klausur_data: Klausur metadata (title, subject, year, etc.)
|
||||
annotations: List of annotations for annotation summary
|
||||
workflow_data: Examiner workflow data (EK, ZK, DK info)
|
||||
|
||||
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("Gutachten zur Abiturklausur", 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 table
|
||||
meta_data = [
|
||||
["Pruefling:", student_data.get('student_name', 'Anonym')],
|
||||
["Schuljahr:", f"{klausur_data.get('year', 2025)}"],
|
||||
["Kurs:", klausur_data.get('semester', 'Abitur')],
|
||||
["Datum:", 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))
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0')))
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
|
||||
# Gutachten content
|
||||
_add_gutachten_content(story, styles, student_data)
|
||||
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0')))
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
|
||||
# Bewertungstabelle
|
||||
_add_criteria_table(story, styles, student_data)
|
||||
|
||||
# Final grade box
|
||||
_add_grade_box(story, styles, student_data)
|
||||
|
||||
# Examiner workflow information
|
||||
if workflow_data:
|
||||
_add_workflow_info(story, styles, workflow_data)
|
||||
|
||||
# Annotation summary
|
||||
if annotations:
|
||||
_add_annotation_summary(story, styles, annotations)
|
||||
|
||||
# Footer
|
||||
_add_footer(story, styles)
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
buffer.seek(0)
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
def _add_gutachten_content(story, styles, student_data):
|
||||
"""Add gutachten text sections to the story."""
|
||||
gutachten = student_data.get('gutachten', {})
|
||||
|
||||
if gutachten:
|
||||
if gutachten.get('einleitung'):
|
||||
story.append(Paragraph("Einleitung", styles['SectionHeader']))
|
||||
story.append(Paragraph(gutachten['einleitung'], styles['GutachtenBody']))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
|
||||
if gutachten.get('hauptteil'):
|
||||
story.append(Paragraph("Hauptteil", styles['SectionHeader']))
|
||||
story.append(Paragraph(gutachten['hauptteil'], styles['GutachtenBody']))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
|
||||
if gutachten.get('fazit'):
|
||||
story.append(Paragraph("Fazit", styles['SectionHeader']))
|
||||
story.append(Paragraph(gutachten['fazit'], styles['GutachtenBody']))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
|
||||
if gutachten.get('staerken') or gutachten.get('schwaechen'):
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
|
||||
if gutachten.get('staerken'):
|
||||
story.append(Paragraph("Staerken:", styles['SectionHeader']))
|
||||
for s in gutachten['staerken']:
|
||||
story.append(Paragraph(f"• {s}", styles['ListItem']))
|
||||
|
||||
if gutachten.get('schwaechen'):
|
||||
story.append(Paragraph("Verbesserungspotenzial:", styles['SectionHeader']))
|
||||
for s in gutachten['schwaechen']:
|
||||
story.append(Paragraph(f"• {s}", styles['ListItem']))
|
||||
else:
|
||||
story.append(Paragraph("<i>Kein Gutachten-Text vorhanden.</i>", styles['GutachtenBody']))
|
||||
|
||||
|
||||
def _add_criteria_table(story, styles, student_data):
|
||||
"""Add criteria scoring table to the story."""
|
||||
story.append(Paragraph("Bewertung nach Kriterien", styles['SectionHeader']))
|
||||
story.append(Spacer(1, 0.2*cm))
|
||||
|
||||
criteria_scores = student_data.get('criteria_scores', {})
|
||||
|
||||
table_data = [["Kriterium", "Gewichtung", "Erreicht", "Punkte"]]
|
||||
total_weighted = 0
|
||||
total_weight = 0
|
||||
|
||||
for key, display_name in CRITERIA_DISPLAY_NAMES.items():
|
||||
weight = CRITERIA_WEIGHTS.get(key, 0)
|
||||
score_data = criteria_scores.get(key, {})
|
||||
score = score_data.get('score', 0) if isinstance(score_data, dict) else score_data
|
||||
|
||||
weighted_score = (score / 100) * weight if score else 0
|
||||
total_weighted += weighted_score
|
||||
total_weight += weight
|
||||
|
||||
table_data.append([
|
||||
display_name,
|
||||
f"{weight}%",
|
||||
f"{score}%",
|
||||
f"{weighted_score:.1f}"
|
||||
])
|
||||
|
||||
table_data.append([
|
||||
"Gesamt",
|
||||
f"{total_weight}%",
|
||||
"",
|
||||
f"{total_weighted:.1f}"
|
||||
])
|
||||
|
||||
criteria_table = Table(table_data, colWidths=[8*cm, 2.5*cm, 2.5*cm, 2.5*cm])
|
||||
criteria_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), 10),
|
||||
('ALIGN', (1, 0), (-1, -1), 'CENTER'),
|
||||
('FONTSIZE', (0, 1), (-1, -1), 9),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 6),
|
||||
('GRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#e2e8f0')),
|
||||
('BACKGROUND', (0, -1), (-1, -1), colors.HexColor('#f7fafc')),
|
||||
('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
|
||||
('ROWBACKGROUNDS', (0, 1), (-1, -2), [colors.white, colors.HexColor('#f7fafc')]),
|
||||
]))
|
||||
story.append(criteria_table)
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
|
||||
|
||||
def _add_grade_box(story, styles, student_data):
|
||||
"""Add final grade box to the story."""
|
||||
grade_points = student_data.get('grade_points', 0)
|
||||
grade_note = GRADE_POINTS_TO_NOTE.get(grade_points, "?")
|
||||
raw_points = student_data.get('raw_points', 0)
|
||||
|
||||
grade_data = [
|
||||
["Rohpunkte:", f"{raw_points} / 100"],
|
||||
["Notenpunkte:", f"{grade_points} Punkte"],
|
||||
["Note:", grade_note]
|
||||
]
|
||||
|
||||
grade_table = Table(grade_data, colWidths=[4*cm, 4*cm])
|
||||
grade_table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, -1), colors.HexColor('#ebf8ff')),
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||
('FONTNAME', (1, -1), (1, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 11),
|
||||
('FONTSIZE', (1, -1), (1, -1), 14),
|
||||
('TEXTCOLOR', (1, -1), (1, -1), colors.HexColor('#2c5282')),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 8),
|
||||
('LEFTPADDING', (0, 0), (-1, -1), 12),
|
||||
('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#2c5282')),
|
||||
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
||||
]))
|
||||
|
||||
story.append(KeepTogether([
|
||||
Paragraph("Endergebnis", styles['SectionHeader']),
|
||||
Spacer(1, 0.2*cm),
|
||||
grade_table
|
||||
]))
|
||||
|
||||
|
||||
def _add_workflow_info(story, styles, workflow_data):
|
||||
"""Add examiner workflow information to the story."""
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0')))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
story.append(Paragraph("Korrekturverlauf", styles['SectionHeader']))
|
||||
|
||||
workflow_rows = []
|
||||
|
||||
if workflow_data.get('erst_korrektor'):
|
||||
ek = workflow_data['erst_korrektor']
|
||||
workflow_rows.append([
|
||||
"Erstkorrektor:",
|
||||
ek.get('name', 'Unbekannt'),
|
||||
f"{ek.get('grade_points', '-')} Punkte"
|
||||
])
|
||||
|
||||
if workflow_data.get('zweit_korrektor'):
|
||||
zk = workflow_data['zweit_korrektor']
|
||||
workflow_rows.append([
|
||||
"Zweitkorrektor:",
|
||||
zk.get('name', 'Unbekannt'),
|
||||
f"{zk.get('grade_points', '-')} Punkte"
|
||||
])
|
||||
|
||||
if workflow_data.get('dritt_korrektor'):
|
||||
dk = workflow_data['dritt_korrektor']
|
||||
workflow_rows.append([
|
||||
"Drittkorrektor:",
|
||||
dk.get('name', 'Unbekannt'),
|
||||
f"{dk.get('grade_points', '-')} Punkte"
|
||||
])
|
||||
|
||||
if workflow_data.get('final_grade_source'):
|
||||
workflow_rows.append([
|
||||
"Endnote durch:",
|
||||
workflow_data['final_grade_source'],
|
||||
""
|
||||
])
|
||||
|
||||
if workflow_rows:
|
||||
workflow_table = Table(workflow_rows, colWidths=[4*cm, 6*cm, 4*cm])
|
||||
workflow_table.setStyle(TableStyle([
|
||||
('FONTNAME', (0, 0), (0, -1), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, -1), 9),
|
||||
('BOTTOMPADDING', (0, 0), (-1, -1), 4),
|
||||
('TOPPADDING', (0, 0), (-1, -1), 4),
|
||||
]))
|
||||
story.append(workflow_table)
|
||||
|
||||
|
||||
def _add_annotation_summary(story, styles, annotations):
|
||||
"""Add annotation summary to the story."""
|
||||
story.append(Spacer(1, 0.5*cm))
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=colors.HexColor('#e2e8f0')))
|
||||
story.append(Spacer(1, 0.3*cm))
|
||||
story.append(Paragraph("Anmerkungen (Zusammenfassung)", styles['SectionHeader']))
|
||||
|
||||
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)} Anmerkungen)", styles['ListItem']))
|
||||
|
||||
|
||||
def _add_footer(story, styles):
|
||||
"""Add generation footer to the story."""
|
||||
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']
|
||||
))
|
||||
# Backward-compat shim -- module moved to korrektur/pdf_export_gutachten.py
|
||||
import importlib as _importlib
|
||||
import sys as _sys
|
||||
_sys.modules[__name__] = _importlib.import_module("korrektur.pdf_export_gutachten")
|
||||
|
||||
Reference in New Issue
Block a user