A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
483 lines
17 KiB
Python
483 lines
17 KiB
Python
"""
|
|
Tests für den PDF Service.
|
|
|
|
Testet:
|
|
- Elternbrief-Generierung
|
|
- Zeugnis-Generierung
|
|
- Korrektur-Übersicht-Generierung
|
|
|
|
Note: These tests require WeasyPrint which needs system libraries (libgobject).
|
|
Tests are skipped if WeasyPrint cannot be loaded.
|
|
"""
|
|
|
|
import pytest
|
|
import sys
|
|
import os
|
|
from datetime import datetime
|
|
|
|
# Add parent directory to path
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
|
|
|
# Mark all tests in this module as requiring WeasyPrint
|
|
# These tests will be automatically skipped in CI via conftest.py
|
|
pytestmark = pytest.mark.requires_weasyprint
|
|
|
|
|
|
class TestPDFServiceImport:
|
|
"""Tests für PDF Service Import und Initialisierung."""
|
|
|
|
def test_import_pdf_service(self):
|
|
"""Test that PDFService can be imported."""
|
|
from services.pdf_service import PDFService
|
|
assert PDFService is not None
|
|
|
|
def test_import_data_classes(self):
|
|
"""Test that data classes can be imported."""
|
|
from services.pdf_service import (
|
|
LetterData,
|
|
CertificateData,
|
|
CorrectionData,
|
|
SchoolInfo
|
|
)
|
|
assert LetterData is not None
|
|
assert CertificateData is not None
|
|
assert CorrectionData is not None
|
|
assert SchoolInfo is not None
|
|
|
|
def test_import_convenience_functions(self):
|
|
"""Test that convenience functions can be imported."""
|
|
from services.pdf_service import (
|
|
generate_letter_pdf,
|
|
generate_certificate_pdf,
|
|
generate_correction_pdf,
|
|
get_pdf_service
|
|
)
|
|
assert callable(generate_letter_pdf)
|
|
assert callable(generate_certificate_pdf)
|
|
assert callable(generate_correction_pdf)
|
|
assert callable(get_pdf_service)
|
|
|
|
|
|
class TestPDFServiceInitialization:
|
|
"""Tests für PDF Service Initialisierung."""
|
|
|
|
def test_create_pdf_service_instance(self):
|
|
"""Test creating a PDFService instance."""
|
|
from services.pdf_service import PDFService
|
|
service = PDFService()
|
|
assert service is not None
|
|
assert service.templates_dir.exists()
|
|
|
|
def test_get_pdf_service_singleton(self):
|
|
"""Test that get_pdf_service returns a singleton."""
|
|
from services.pdf_service import get_pdf_service
|
|
service1 = get_pdf_service()
|
|
service2 = get_pdf_service()
|
|
assert service1 is service2
|
|
|
|
|
|
class TestLetterPDFGeneration:
|
|
"""Tests für Elternbrief-PDF-Generierung."""
|
|
|
|
def test_generate_simple_letter(self):
|
|
"""Test generating a simple letter PDF."""
|
|
from services.pdf_service import PDFService, LetterData
|
|
|
|
service = PDFService()
|
|
letter_data = LetterData(
|
|
recipient_name="Familie Müller",
|
|
recipient_address="Musterstraße 1\n12345 Musterstadt",
|
|
student_name="Max Müller",
|
|
student_class="5a",
|
|
subject="Einladung zum Elternsprechtag",
|
|
content="Sehr geehrte Familie Müller,\n\nhiermit laden wir Sie herzlich zum Elternsprechtag ein.",
|
|
date="15.01.2025",
|
|
teacher_name="Frau Schmidt",
|
|
teacher_title="Klassenlehrerin",
|
|
letter_type="elternabend",
|
|
tone="professional"
|
|
)
|
|
|
|
pdf_bytes = service.generate_letter_pdf(letter_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
# PDF magic number check
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_letter_with_school_info(self):
|
|
"""Test generating letter with school information."""
|
|
from services.pdf_service import PDFService, LetterData, SchoolInfo
|
|
|
|
service = PDFService()
|
|
school_info = SchoolInfo(
|
|
name="Musterschule",
|
|
address="Schulweg 10, 12345 Musterstadt",
|
|
phone="0123-456789",
|
|
email="info@musterschule.de",
|
|
website="www.musterschule.de",
|
|
principal="Dr. Hans Meier"
|
|
)
|
|
|
|
letter_data = LetterData(
|
|
recipient_name="Familie Schmidt",
|
|
recipient_address="Hauptstraße 5\n12345 Musterstadt",
|
|
student_name="Lisa Schmidt",
|
|
student_class="7b",
|
|
subject="Halbjahresbericht",
|
|
content="Sehr geehrte Eltern,\n\nanbei erhalten Sie den Halbjahresbericht.",
|
|
date="20.01.2025",
|
|
teacher_name="Herr Weber",
|
|
school_info=school_info,
|
|
letter_type="halbjahr",
|
|
tone="formal"
|
|
)
|
|
|
|
pdf_bytes = service.generate_letter_pdf(letter_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_letter_with_legal_references(self):
|
|
"""Test generating letter with legal references."""
|
|
from services.pdf_service import PDFService, LetterData
|
|
|
|
service = PDFService()
|
|
letter_data = LetterData(
|
|
recipient_name="Familie Braun",
|
|
recipient_address="Gartenstraße 20\n12345 Musterstadt",
|
|
student_name="Tim Braun",
|
|
student_class="8c",
|
|
subject="Fehlzeiten",
|
|
content="Sehr geehrte Eltern,\n\nwir möchten Sie über die Fehlzeiten informieren.",
|
|
date="25.01.2025",
|
|
teacher_name="Frau Lehmann",
|
|
letter_type="fehlzeiten",
|
|
tone="concerned",
|
|
legal_references=[
|
|
{"law": "SchulG NRW", "paragraph": "§ 42", "title": "Pflichten der Eltern"},
|
|
{"law": "SchulG NRW", "paragraph": "§ 43", "title": "Schulpflicht"}
|
|
],
|
|
gfk_principles_applied=["Beobachtung", "Bedürfnis", "Bitte"]
|
|
)
|
|
|
|
pdf_bytes = service.generate_letter_pdf(letter_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_letter_convenience_function(self):
|
|
"""Test the convenience function for letter generation."""
|
|
from services.pdf_service import generate_letter_pdf
|
|
|
|
letter_dict = {
|
|
"recipient_name": "Familie Test",
|
|
"recipient_address": "Testweg 1\n12345 Teststadt",
|
|
"student_name": "Test Kind",
|
|
"student_class": "3a",
|
|
"subject": "Test-Brief",
|
|
"content": "Dies ist ein Testbrief.",
|
|
"date": "01.02.2025",
|
|
"teacher_name": "Herr Test"
|
|
}
|
|
|
|
pdf_bytes = generate_letter_pdf(letter_dict)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
|
|
class TestCertificatePDFGeneration:
|
|
"""Tests für Zeugnis-PDF-Generierung."""
|
|
|
|
def test_generate_halbjahreszeugnis(self):
|
|
"""Test generating a half-year certificate."""
|
|
from services.pdf_service import PDFService, CertificateData
|
|
|
|
service = PDFService()
|
|
cert_data = CertificateData(
|
|
student_name="Anna Beispiel",
|
|
student_birthdate="15.05.2012",
|
|
student_class="6b",
|
|
school_year="2024/2025",
|
|
certificate_type="halbjahr",
|
|
subjects=[
|
|
{"name": "Deutsch", "grade": "2", "points": None},
|
|
{"name": "Mathematik", "grade": "2", "points": None},
|
|
{"name": "Englisch", "grade": "1", "points": None},
|
|
{"name": "Geschichte", "grade": "2", "points": None},
|
|
{"name": "Biologie", "grade": "3", "points": None},
|
|
{"name": "Sport", "grade": "1", "points": None},
|
|
],
|
|
attendance={"days_absent": 5, "days_excused": 4, "days_unexcused": 1},
|
|
class_teacher="Frau Mustermann",
|
|
principal="Dr. Hans Direktor",
|
|
issue_date="31.01.2025",
|
|
social_behavior="B",
|
|
work_behavior="A"
|
|
)
|
|
|
|
pdf_bytes = service.generate_certificate_pdf(cert_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_jahreszeugnis(self):
|
|
"""Test generating a full-year certificate."""
|
|
from services.pdf_service import PDFService, CertificateData
|
|
|
|
service = PDFService()
|
|
cert_data = CertificateData(
|
|
student_name="Peter Schüler",
|
|
student_birthdate="20.03.2011",
|
|
student_class="7a",
|
|
school_year="2024/2025",
|
|
certificate_type="jahres",
|
|
subjects=[
|
|
{"name": "Deutsch", "grade": "3", "points": None},
|
|
{"name": "Mathematik", "grade": "2", "points": None},
|
|
{"name": "Englisch", "grade": "2", "points": None},
|
|
],
|
|
attendance={"days_absent": 10, "days_excused": 10, "days_unexcused": 0},
|
|
remarks="Versetzung in die Klasse 8a.",
|
|
class_teacher="Herr Lehrer",
|
|
principal="Frau Direktorin",
|
|
issue_date="15.07.2025"
|
|
)
|
|
|
|
pdf_bytes = service.generate_certificate_pdf(cert_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_certificate_convenience_function(self):
|
|
"""Test the convenience function for certificate generation."""
|
|
from services.pdf_service import generate_certificate_pdf
|
|
|
|
cert_dict = {
|
|
"student_name": "Test Schüler",
|
|
"student_birthdate": "01.01.2012",
|
|
"student_class": "5a",
|
|
"school_year": "2024/2025",
|
|
"certificate_type": "halbjahr",
|
|
"subjects": [
|
|
{"name": "Deutsch", "grade": "2"},
|
|
{"name": "Mathe", "grade": "3"}
|
|
],
|
|
"attendance": {"days_absent": 3, "days_excused": 3, "days_unexcused": 0},
|
|
"class_teacher": "Herr Test",
|
|
"principal": "Frau Test"
|
|
}
|
|
|
|
pdf_bytes = generate_certificate_pdf(cert_dict)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
|
|
class TestCorrectionPDFGeneration:
|
|
"""Tests für Korrektur-PDF-Generierung."""
|
|
|
|
def test_generate_correction_overview(self):
|
|
"""Test generating a correction overview PDF."""
|
|
from services.pdf_service import PDFService, CorrectionData, StudentInfo
|
|
|
|
service = PDFService()
|
|
student = StudentInfo(
|
|
student_id="student-001",
|
|
name="Maria Musterschülerin",
|
|
class_name="9a"
|
|
)
|
|
correction_data = CorrectionData(
|
|
student=student,
|
|
exam_title="Klassenarbeit Nr. 3",
|
|
date="10.01.2025",
|
|
subject="Mathematik",
|
|
max_points=50,
|
|
achieved_points=42,
|
|
grade="2",
|
|
percentage=84.0,
|
|
grade_distribution={"1": 3, "2": 8, "3": 10, "4": 5, "5": 2, "6": 0},
|
|
class_average=2.8,
|
|
corrections=[
|
|
{
|
|
"question": "Lineare Gleichungen lösen",
|
|
"answer": "",
|
|
"points": 10,
|
|
"feedback": "Alle Aufgaben korrekt gelöst."
|
|
},
|
|
{
|
|
"question": "Textaufgabe: Geschwindigkeit",
|
|
"answer": "",
|
|
"points": 12,
|
|
"feedback": "Ansatz richtig, kleiner Rechenfehler am Ende."
|
|
},
|
|
{
|
|
"question": "Geometrie: Flächenberechnung",
|
|
"answer": "",
|
|
"points": 20,
|
|
"feedback": "Formeln korrekt angewendet, eine Teilaufgabe fehlt."
|
|
}
|
|
],
|
|
teacher_notes="Insgesamt eine gute Leistung. Weiter so!"
|
|
)
|
|
|
|
pdf_bytes = service.generate_correction_pdf(correction_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_correction_without_feedback(self):
|
|
"""Test generating correction PDF without individual feedback."""
|
|
from services.pdf_service import PDFService, CorrectionData, StudentInfo
|
|
|
|
service = PDFService()
|
|
student = StudentInfo(
|
|
student_id="student-002",
|
|
name="Tom Test",
|
|
class_name="10b"
|
|
)
|
|
correction_data = CorrectionData(
|
|
student=student,
|
|
exam_title="Vokabeltest",
|
|
date="20.01.2025",
|
|
subject="Englisch",
|
|
max_points=20,
|
|
achieved_points=18,
|
|
grade="1",
|
|
percentage=90.0,
|
|
grade_distribution={"1": 5, "2": 10, "3": 8, "4": 2, "5": 0, "6": 0},
|
|
class_average=2.3,
|
|
corrections=[
|
|
{"question": "Teil 1: Vokabeln DE-EN", "answer": "", "points": 9},
|
|
{"question": "Teil 2: Vokabeln EN-DE", "answer": "", "points": 9}
|
|
]
|
|
)
|
|
|
|
pdf_bytes = service.generate_correction_pdf(correction_data)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
def test_generate_correction_convenience_function(self):
|
|
"""Test the convenience function for correction generation."""
|
|
from services.pdf_service import generate_correction_pdf
|
|
|
|
correction_dict = {
|
|
"student_id": "student-003",
|
|
"student_name": "Test Student",
|
|
"student_class": "8a",
|
|
"exam_title": "Test Klausur",
|
|
"date": "15.01.2025",
|
|
"subject": "Physik",
|
|
"max_points": 30,
|
|
"achieved_points": 24,
|
|
"grade": "2",
|
|
"percentage": 80.0,
|
|
"grade_distribution": {"1": 2, "2": 5, "3": 8, "4": 3, "5": 1, "6": 0},
|
|
"class_average": 2.9,
|
|
"corrections": [
|
|
{"question": "Aufgabe 1", "answer": "", "points": 12, "feedback": "Gut gelöst"},
|
|
{"question": "Aufgabe 2", "answer": "", "points": 12, "feedback": "Korrekt"}
|
|
],
|
|
"teacher_notes": "Insgesamt gute Arbeit."
|
|
}
|
|
|
|
pdf_bytes = generate_correction_pdf(correction_dict)
|
|
|
|
assert pdf_bytes is not None
|
|
assert len(pdf_bytes) > 0
|
|
assert pdf_bytes[:4] == b'%PDF'
|
|
|
|
|
|
class TestPDFServiceHelpers:
|
|
"""Tests für Hilfsfunktionen des PDF Service."""
|
|
|
|
def test_date_format_filter(self):
|
|
"""Test the date format filter."""
|
|
from services.pdf_service import PDFService
|
|
|
|
service = PDFService()
|
|
|
|
# ISO date format
|
|
result = service._date_format("2025-01-15")
|
|
assert result == "15.01.2025"
|
|
|
|
# Empty value
|
|
result = service._date_format("")
|
|
assert result == ""
|
|
|
|
# Already formatted
|
|
result = service._date_format("15.01.2025")
|
|
assert result == "15.01.2025"
|
|
|
|
def test_grade_color_filter(self):
|
|
"""Test the grade color filter."""
|
|
from services.pdf_service import PDFService
|
|
|
|
service = PDFService()
|
|
|
|
# German grades
|
|
assert service._grade_color("1") == "#27ae60"
|
|
assert service._grade_color("2") == "#2ecc71"
|
|
assert service._grade_color("3") == "#f1c40f"
|
|
assert service._grade_color("4") == "#e67e22"
|
|
assert service._grade_color("5") == "#e74c3c"
|
|
assert service._grade_color("6") == "#c0392b"
|
|
|
|
# Behavior grades
|
|
assert service._grade_color("A") == "#27ae60"
|
|
assert service._grade_color("B") == "#2ecc71"
|
|
|
|
# Unknown grade
|
|
assert service._grade_color("X") == "#333333"
|
|
|
|
|
|
class TestPDFTemplates:
|
|
"""Tests für PDF Templates."""
|
|
|
|
def test_templates_directory_created(self):
|
|
"""Test that templates directory is created."""
|
|
from services.pdf_service import PDFService
|
|
from pathlib import Path
|
|
|
|
service = PDFService()
|
|
assert service.templates_dir.exists()
|
|
assert service.templates_dir.is_dir()
|
|
|
|
def test_inline_templates_work(self):
|
|
"""Test that inline templates work as fallback."""
|
|
from services.pdf_service import PDFService
|
|
|
|
service = PDFService()
|
|
|
|
# Test letter template
|
|
template_html = service._get_letter_template_html()
|
|
assert "{{ data.subject }}" in template_html
|
|
assert "{{ data.content" in template_html
|
|
|
|
# Test certificate template
|
|
template_html = service._get_certificate_template_html()
|
|
assert "{{ data.student_name }}" in template_html
|
|
# data.subjects is used in a for loop, not direct output
|
|
assert "data.subjects" in template_html
|
|
|
|
# Test correction template
|
|
template_html = service._get_correction_template_html()
|
|
assert "{{ data.exam_title }}" in template_html
|
|
# data.corrections is used in a for loop, not direct output
|
|
assert "data.corrections" in template_html
|
|
|
|
|
|
# Run tests if executed directly
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|