Restructure: Move 43 files into 8 domain packages (backend-lehrer)
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 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
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 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1
backend-lehrer/letters/__init__.py
Normal file
1
backend-lehrer/letters/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# letters — Elternbriefe and Zeugnisse (certificates).
|
||||
346
backend-lehrer/letters/api.py
Normal file
346
backend-lehrer/letters/api.py
Normal file
@@ -0,0 +1,346 @@
|
||||
"""
|
||||
Letters API - Elternbrief-Verwaltung fuer BreakPilot.
|
||||
|
||||
Bietet Endpoints fuer:
|
||||
- Speichern und Laden von Elternbriefen
|
||||
- PDF-Export von Briefen
|
||||
- Versenden per Email
|
||||
- GFK-Integration fuer Textverbesserung
|
||||
|
||||
Split into:
|
||||
- letters_models.py: Enums, Pydantic models, helper functions
|
||||
- letters_api.py (this file): API endpoints and in-memory store
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response, Query
|
||||
import httpx
|
||||
|
||||
# PDF service requires WeasyPrint with system libraries - make optional for CI
|
||||
try:
|
||||
from services.pdf_service import generate_letter_pdf, SchoolInfo
|
||||
_pdf_available = True
|
||||
except (ImportError, OSError):
|
||||
generate_letter_pdf = None # type: ignore
|
||||
SchoolInfo = None # type: ignore
|
||||
_pdf_available = False
|
||||
|
||||
from .models import (
|
||||
LetterType,
|
||||
LetterTone,
|
||||
LetterStatus,
|
||||
LetterCreateRequest,
|
||||
LetterUpdateRequest,
|
||||
LetterResponse,
|
||||
LetterListResponse,
|
||||
ExportPDFRequest,
|
||||
ImproveRequest,
|
||||
ImproveResponse,
|
||||
SendEmailRequest,
|
||||
SendEmailResponse,
|
||||
get_type_label as _get_type_label,
|
||||
get_tone_label as _get_tone_label,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/letters", tags=["letters"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# In-Memory Storage (Prototyp - spaeter durch DB ersetzen)
|
||||
# =============================================================================
|
||||
|
||||
_letters_store: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _get_letter(letter_id: str) -> Dict[str, Any]:
|
||||
"""Holt Brief aus dem Store."""
|
||||
if letter_id not in _letters_store:
|
||||
raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden")
|
||||
return _letters_store[letter_id]
|
||||
|
||||
|
||||
def _save_letter(letter_data: Dict[str, Any]) -> str:
|
||||
"""Speichert Brief und gibt ID zurueck."""
|
||||
letter_id = letter_data.get("id") or str(uuid.uuid4())
|
||||
letter_data["id"] = letter_id
|
||||
letter_data["updated_at"] = datetime.now()
|
||||
if "created_at" not in letter_data:
|
||||
letter_data["created_at"] = datetime.now()
|
||||
_letters_store[letter_id] = letter_data
|
||||
return letter_id
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/", response_model=LetterResponse)
|
||||
async def create_letter(request: LetterCreateRequest):
|
||||
"""Erstellt einen neuen Elternbrief."""
|
||||
logger.info(f"Creating new letter for student: {request.student_name}")
|
||||
|
||||
letter_data = {
|
||||
"recipient_name": request.recipient_name,
|
||||
"recipient_address": request.recipient_address,
|
||||
"student_name": request.student_name,
|
||||
"student_class": request.student_class,
|
||||
"subject": request.subject,
|
||||
"content": request.content,
|
||||
"letter_type": request.letter_type,
|
||||
"tone": request.tone,
|
||||
"teacher_name": request.teacher_name,
|
||||
"teacher_title": request.teacher_title,
|
||||
"school_info": request.school_info.model_dump() if request.school_info else None,
|
||||
"legal_references": [ref.model_dump() for ref in request.legal_references] if request.legal_references else None,
|
||||
"gfk_principles_applied": request.gfk_principles_applied,
|
||||
"gfk_score": None,
|
||||
"status": LetterStatus.DRAFT,
|
||||
"pdf_path": None,
|
||||
"dsms_cid": None,
|
||||
"sent_at": None,
|
||||
}
|
||||
|
||||
letter_id = _save_letter(letter_data)
|
||||
letter_data["id"] = letter_id
|
||||
logger.info(f"Letter created with ID: {letter_id}")
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
# NOTE: Static routes must come BEFORE dynamic routes like /{letter_id}
|
||||
@router.get("/types")
|
||||
async def get_letter_types():
|
||||
"""Gibt alle verfuegbaren Brieftypen zurueck."""
|
||||
return {"types": [{"value": t.value, "label": _get_type_label(t)} for t in LetterType]}
|
||||
|
||||
|
||||
@router.get("/tones")
|
||||
async def get_letter_tones():
|
||||
"""Gibt alle verfuegbaren Tonalitaeten zurueck."""
|
||||
return {"tones": [{"value": t.value, "label": _get_tone_label(t)} for t in LetterTone]}
|
||||
|
||||
|
||||
@router.get("/{letter_id}", response_model=LetterResponse)
|
||||
async def get_letter(letter_id: str):
|
||||
"""Laedt einen gespeicherten Brief."""
|
||||
logger.info(f"Getting letter: {letter_id}")
|
||||
letter_data = _get_letter(letter_id)
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
@router.get("/", response_model=LetterListResponse)
|
||||
async def list_letters(
|
||||
student_id: Optional[str] = Query(None),
|
||||
class_name: Optional[str] = Query(None),
|
||||
letter_type: Optional[LetterType] = Query(None),
|
||||
status: Optional[LetterStatus] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""Listet alle gespeicherten Briefe mit optionalen Filtern."""
|
||||
logger.info("Listing letters with filters")
|
||||
|
||||
filtered_letters = list(_letters_store.values())
|
||||
if class_name:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("student_class") == class_name]
|
||||
if letter_type:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("letter_type") == letter_type]
|
||||
if status:
|
||||
filtered_letters = [l for l in filtered_letters if l.get("status") == status]
|
||||
|
||||
filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True)
|
||||
total = len(filtered_letters)
|
||||
start = (page - 1) * page_size
|
||||
paginated_letters = filtered_letters[start:start + page_size]
|
||||
|
||||
return LetterListResponse(
|
||||
letters=[LetterResponse(**l) for l in paginated_letters],
|
||||
total=total, page=page, page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{letter_id}", response_model=LetterResponse)
|
||||
async def update_letter(letter_id: str, request: LetterUpdateRequest):
|
||||
"""Aktualisiert einen bestehenden Brief."""
|
||||
logger.info(f"Updating letter: {letter_id}")
|
||||
letter_data = _get_letter(letter_id)
|
||||
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if value is not None:
|
||||
if key == "school_info" and value:
|
||||
letter_data[key] = value if isinstance(value, dict) else value.model_dump()
|
||||
elif key == "legal_references" and value:
|
||||
letter_data[key] = [ref if isinstance(ref, dict) else ref.model_dump() for ref in value]
|
||||
else:
|
||||
letter_data[key] = value
|
||||
|
||||
_save_letter(letter_data)
|
||||
return LetterResponse(**letter_data)
|
||||
|
||||
|
||||
@router.delete("/{letter_id}")
|
||||
async def delete_letter(letter_id: str):
|
||||
"""Loescht einen Brief."""
|
||||
logger.info(f"Deleting letter: {letter_id}")
|
||||
if letter_id not in _letters_store:
|
||||
raise HTTPException(status_code=404, detail=f"Brief mit ID {letter_id} nicht gefunden")
|
||||
del _letters_store[letter_id]
|
||||
return {"message": f"Brief {letter_id} wurde geloescht"}
|
||||
|
||||
|
||||
@router.post("/export-pdf")
|
||||
async def export_letter_pdf(request: ExportPDFRequest):
|
||||
"""Exportiert einen Brief als PDF."""
|
||||
logger.info("Exporting letter as PDF")
|
||||
|
||||
if request.letter_id:
|
||||
letter_data = _get_letter(request.letter_id)
|
||||
elif request.letter_data:
|
||||
letter_data = request.letter_data.model_dump()
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Entweder letter_id oder letter_data muss angegeben werden")
|
||||
|
||||
if "date" not in letter_data:
|
||||
letter_data["date"] = datetime.now().strftime("%d.%m.%Y")
|
||||
|
||||
try:
|
||||
pdf_bytes = generate_letter_pdf(letter_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating PDF: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Fehler bei PDF-Generierung: {str(e)}")
|
||||
|
||||
student_name = letter_data.get("student_name", "Brief").replace(" ", "_")
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
filename = f"Elternbrief_{student_name}_{date_str}.pdf"
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes, media_type="application/pdf",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}", "Content-Length": str(len(pdf_bytes))}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{letter_id}/export-pdf")
|
||||
async def export_saved_letter_pdf(letter_id: str):
|
||||
"""Exportiert einen gespeicherten Brief als PDF (Kurzform)."""
|
||||
return await export_letter_pdf(ExportPDFRequest(letter_id=letter_id))
|
||||
|
||||
|
||||
@router.post("/improve", response_model=ImproveResponse)
|
||||
async def improve_letter_content(request: ImproveRequest):
|
||||
"""Verbessert den Briefinhalt nach GFK-Prinzipien."""
|
||||
logger.info("Improving letter content with GFK principles")
|
||||
|
||||
comm_service_url = os.getenv("COMMUNICATION_SERVICE_URL", "http://localhost:8000/v1/communication")
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
validate_response = await client.post(
|
||||
f"{comm_service_url}/validate",
|
||||
json={"text": request.content}, timeout=30.0
|
||||
)
|
||||
|
||||
if validate_response.status_code != 200:
|
||||
logger.warning(f"Validation service returned {validate_response.status_code}")
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Verbesserungsservice nicht verfuegbar"],
|
||||
gfk_score=0.5, gfk_principles_applied=[]
|
||||
)
|
||||
|
||||
validation_data = validate_response.json()
|
||||
|
||||
if validation_data.get("is_valid", False) and validation_data.get("gfk_score", 0) > 0.8:
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Text entspricht bereits GFK-Standards"],
|
||||
gfk_score=validation_data.get("gfk_score", 0.8),
|
||||
gfk_principles_applied=validation_data.get("positive_elements", [])
|
||||
)
|
||||
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=validation_data.get("suggestions", []),
|
||||
gfk_score=validation_data.get("gfk_score", 0.5),
|
||||
gfk_principles_applied=validation_data.get("positive_elements", [])
|
||||
)
|
||||
|
||||
except httpx.TimeoutException:
|
||||
logger.error("Timeout while calling communication service")
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=["Zeitueberschreitung beim Verbesserungsservice"],
|
||||
gfk_score=0.5, gfk_principles_applied=[]
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error improving content: {e}")
|
||||
return ImproveResponse(
|
||||
improved_content=request.content,
|
||||
changes=[f"Fehler: {str(e)}"],
|
||||
gfk_score=0.5, gfk_principles_applied=[]
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{letter_id}/send", response_model=SendEmailResponse)
|
||||
async def send_letter_email(letter_id: str, request: SendEmailRequest):
|
||||
"""Versendet einen Brief per Email."""
|
||||
logger.info(f"Sending letter {letter_id} to {request.recipient_email}")
|
||||
letter_data = _get_letter(letter_id)
|
||||
|
||||
try:
|
||||
pdf_attachment = None
|
||||
if request.include_pdf:
|
||||
letter_data["date"] = datetime.now().strftime("%d.%m.%Y")
|
||||
pdf_bytes = generate_letter_pdf(letter_data)
|
||||
pdf_attachment = {
|
||||
"filename": f"Elternbrief_{letter_data.get('student_name', 'Brief').replace(' ', '_')}.pdf",
|
||||
"content": pdf_bytes.hex(),
|
||||
"content_type": "application/pdf"
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
logger.info(f"Would send email: {letter_data.get('subject')} to {request.recipient_email}")
|
||||
letter_data["status"] = LetterStatus.SENT
|
||||
letter_data["sent_at"] = datetime.now()
|
||||
_save_letter(letter_data)
|
||||
|
||||
return SendEmailResponse(
|
||||
success=True,
|
||||
message=f"Brief wurde an {request.recipient_email} gesendet",
|
||||
sent_at=datetime.now()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending email: {e}")
|
||||
return SendEmailResponse(success=False, message=f"Fehler beim Versenden: {str(e)}", sent_at=None)
|
||||
|
||||
|
||||
@router.get("/student/{student_id}", response_model=LetterListResponse)
|
||||
async def get_letters_for_student(
|
||||
student_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""Laedt alle Briefe fuer einen bestimmten Schueler."""
|
||||
logger.info(f"Getting letters for student: {student_id}")
|
||||
|
||||
filtered_letters = [
|
||||
l for l in _letters_store.values()
|
||||
if student_id.lower() in l.get("student_name", "").lower()
|
||||
]
|
||||
|
||||
filtered_letters.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True)
|
||||
total = len(filtered_letters)
|
||||
start = (page - 1) * page_size
|
||||
paginated_letters = filtered_letters[start:start + page_size]
|
||||
|
||||
return LetterListResponse(
|
||||
letters=[LetterResponse(**l) for l in paginated_letters],
|
||||
total=total, page=page, page_size=page_size
|
||||
)
|
||||
340
backend-lehrer/letters/certificates_api.py
Normal file
340
backend-lehrer/letters/certificates_api.py
Normal file
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Certificates API - Zeugnisverwaltung fuer BreakPilot.
|
||||
|
||||
Split into:
|
||||
- certificates_models.py: Enums, Pydantic models, helper functions
|
||||
- certificates_api.py (this file): API endpoints and in-memory store
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, List, Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Response, Query
|
||||
|
||||
# PDF service requires WeasyPrint with system libraries - make optional for CI
|
||||
try:
|
||||
from services.pdf_service import generate_certificate_pdf, SchoolInfo
|
||||
_pdf_available = True
|
||||
except (ImportError, OSError):
|
||||
generate_certificate_pdf = None # type: ignore
|
||||
SchoolInfo = None # type: ignore
|
||||
_pdf_available = False
|
||||
|
||||
from .certificates_models import (
|
||||
CertificateType,
|
||||
CertificateStatus,
|
||||
BehaviorGrade,
|
||||
CertificateCreateRequest,
|
||||
CertificateUpdateRequest,
|
||||
CertificateResponse,
|
||||
CertificateListResponse,
|
||||
GradeStatistics,
|
||||
get_type_label as _get_type_label,
|
||||
calculate_average as _calculate_average,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/certificates", tags=["certificates"])
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# In-Memory Storage (Prototyp - spaeter durch DB ersetzen)
|
||||
# =============================================================================
|
||||
|
||||
_certificates_store: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
|
||||
def _get_certificate(cert_id: str) -> Dict[str, Any]:
|
||||
"""Holt Zeugnis aus dem Store."""
|
||||
if cert_id not in _certificates_store:
|
||||
raise HTTPException(status_code=404, detail=f"Zeugnis mit ID {cert_id} nicht gefunden")
|
||||
return _certificates_store[cert_id]
|
||||
|
||||
|
||||
def _save_certificate(cert_data: Dict[str, Any]) -> str:
|
||||
"""Speichert Zeugnis und gibt ID zurueck."""
|
||||
cert_id = cert_data.get("id") or str(uuid.uuid4())
|
||||
cert_data["id"] = cert_id
|
||||
cert_data["updated_at"] = datetime.now()
|
||||
if "created_at" not in cert_data:
|
||||
cert_data["created_at"] = datetime.now()
|
||||
_certificates_store[cert_id] = cert_data
|
||||
return cert_id
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# API Endpoints
|
||||
# =============================================================================
|
||||
|
||||
@router.post("/", response_model=CertificateResponse)
|
||||
async def create_certificate(request: CertificateCreateRequest):
|
||||
"""Erstellt ein neues Zeugnis."""
|
||||
logger.info(f"Creating new certificate for student: {request.student_name}")
|
||||
|
||||
subjects_list = [s.model_dump() for s in request.subjects]
|
||||
|
||||
cert_data = {
|
||||
"student_id": request.student_id,
|
||||
"student_name": request.student_name,
|
||||
"student_birthdate": request.student_birthdate,
|
||||
"student_class": request.student_class,
|
||||
"school_year": request.school_year,
|
||||
"certificate_type": request.certificate_type,
|
||||
"subjects": subjects_list,
|
||||
"attendance": request.attendance.model_dump(),
|
||||
"remarks": request.remarks,
|
||||
"class_teacher": request.class_teacher,
|
||||
"principal": request.principal,
|
||||
"school_info": request.school_info.model_dump() if request.school_info else None,
|
||||
"issue_date": request.issue_date or datetime.now().strftime("%d.%m.%Y"),
|
||||
"social_behavior": request.social_behavior,
|
||||
"work_behavior": request.work_behavior,
|
||||
"status": CertificateStatus.DRAFT,
|
||||
"average_grade": _calculate_average(subjects_list),
|
||||
"pdf_path": None,
|
||||
"dsms_cid": None,
|
||||
}
|
||||
|
||||
cert_id = _save_certificate(cert_data)
|
||||
cert_data["id"] = cert_id
|
||||
logger.info(f"Certificate created with ID: {cert_id}")
|
||||
return CertificateResponse(**cert_data)
|
||||
|
||||
|
||||
# IMPORTANT: Static routes must be defined BEFORE dynamic /{cert_id} route
|
||||
@router.get("/types")
|
||||
async def get_certificate_types():
|
||||
"""Gibt alle verfuegbaren Zeugnistypen zurueck."""
|
||||
return {"types": [{"value": t.value, "label": _get_type_label(t)} for t in CertificateType]}
|
||||
|
||||
|
||||
@router.get("/behavior-grades")
|
||||
async def get_behavior_grades():
|
||||
"""Gibt alle verfuegbaren Verhaltensnoten zurueck."""
|
||||
labels = {
|
||||
BehaviorGrade.A: "A - Sehr gut", BehaviorGrade.B: "B - Gut",
|
||||
BehaviorGrade.C: "C - Befriedigend", BehaviorGrade.D: "D - Verbesserungswuerdig"
|
||||
}
|
||||
return {"grades": [{"value": g.value, "label": labels[g]} for g in BehaviorGrade]}
|
||||
|
||||
|
||||
@router.get("/{cert_id}", response_model=CertificateResponse)
|
||||
async def get_certificate(cert_id: str):
|
||||
"""Laedt ein gespeichertes Zeugnis."""
|
||||
logger.info(f"Getting certificate: {cert_id}")
|
||||
return CertificateResponse(**_get_certificate(cert_id))
|
||||
|
||||
|
||||
@router.get("/", response_model=CertificateListResponse)
|
||||
async def list_certificates(
|
||||
student_id: Optional[str] = Query(None),
|
||||
class_name: Optional[str] = Query(None),
|
||||
school_year: Optional[str] = Query(None),
|
||||
certificate_type: Optional[CertificateType] = Query(None),
|
||||
status: Optional[CertificateStatus] = Query(None),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""Listet alle gespeicherten Zeugnisse mit optionalen Filtern."""
|
||||
logger.info("Listing certificates with filters")
|
||||
|
||||
filtered_certs = list(_certificates_store.values())
|
||||
if student_id:
|
||||
filtered_certs = [c for c in filtered_certs if c.get("student_id") == student_id]
|
||||
if class_name:
|
||||
filtered_certs = [c for c in filtered_certs if c.get("student_class") == class_name]
|
||||
if school_year:
|
||||
filtered_certs = [c for c in filtered_certs if c.get("school_year") == school_year]
|
||||
if certificate_type:
|
||||
filtered_certs = [c for c in filtered_certs if c.get("certificate_type") == certificate_type]
|
||||
if status:
|
||||
filtered_certs = [c for c in filtered_certs if c.get("status") == status]
|
||||
|
||||
filtered_certs.sort(key=lambda x: x.get("created_at", datetime.min), reverse=True)
|
||||
total = len(filtered_certs)
|
||||
start = (page - 1) * page_size
|
||||
paginated_certs = filtered_certs[start:start + page_size]
|
||||
|
||||
return CertificateListResponse(
|
||||
certificates=[CertificateResponse(**c) for c in paginated_certs],
|
||||
total=total, page=page, page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{cert_id}", response_model=CertificateResponse)
|
||||
async def update_certificate(cert_id: str, request: CertificateUpdateRequest):
|
||||
"""Aktualisiert ein bestehendes Zeugnis."""
|
||||
logger.info(f"Updating certificate: {cert_id}")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
|
||||
if cert_data.get("status") in [CertificateStatus.ISSUED, CertificateStatus.ARCHIVED]:
|
||||
raise HTTPException(status_code=400, detail="Zeugnis wurde bereits ausgestellt und kann nicht mehr bearbeitet werden")
|
||||
|
||||
update_data = request.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
if value is not None:
|
||||
if key == "subjects":
|
||||
cert_data[key] = [s if isinstance(s, dict) else s.model_dump() for s in value]
|
||||
cert_data["average_grade"] = _calculate_average(cert_data["subjects"])
|
||||
elif key == "attendance":
|
||||
cert_data[key] = value if isinstance(value, dict) else value.model_dump()
|
||||
else:
|
||||
cert_data[key] = value
|
||||
|
||||
_save_certificate(cert_data)
|
||||
return CertificateResponse(**cert_data)
|
||||
|
||||
|
||||
@router.delete("/{cert_id}")
|
||||
async def delete_certificate(cert_id: str):
|
||||
"""Loescht ein Zeugnis. Nur Entwuerfe koennen geloescht werden."""
|
||||
logger.info(f"Deleting certificate: {cert_id}")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
if cert_data.get("status") != CertificateStatus.DRAFT:
|
||||
raise HTTPException(status_code=400, detail="Nur Zeugnis-Entwuerfe koennen geloescht werden")
|
||||
del _certificates_store[cert_id]
|
||||
return {"message": f"Zeugnis {cert_id} wurde geloescht"}
|
||||
|
||||
|
||||
@router.post("/{cert_id}/export-pdf")
|
||||
async def export_certificate_pdf(cert_id: str):
|
||||
"""Exportiert ein Zeugnis als PDF."""
|
||||
logger.info(f"Exporting certificate {cert_id} as PDF")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
|
||||
try:
|
||||
pdf_bytes = generate_certificate_pdf(cert_data)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating PDF: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Fehler bei PDF-Generierung: {str(e)}")
|
||||
|
||||
student_name = cert_data.get("student_name", "Zeugnis").replace(" ", "_")
|
||||
school_year = cert_data.get("school_year", "").replace("/", "-")
|
||||
cert_type = cert_data.get("certificate_type", "zeugnis")
|
||||
filename = f"Zeugnis_{student_name}_{cert_type}_{school_year}.pdf"
|
||||
|
||||
from urllib.parse import quote
|
||||
filename_ascii = filename.encode('ascii', 'replace').decode('ascii')
|
||||
filename_encoded = quote(filename, safe='')
|
||||
|
||||
return Response(
|
||||
content=pdf_bytes, media_type="application/pdf",
|
||||
headers={
|
||||
"Content-Disposition": f"attachment; filename=\"{filename_ascii}\"; filename*=UTF-8''{filename_encoded}",
|
||||
"Content-Length": str(len(pdf_bytes))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{cert_id}/submit-review")
|
||||
async def submit_for_review(cert_id: str):
|
||||
"""Reicht Zeugnis zur Pruefung ein."""
|
||||
logger.info(f"Submitting certificate {cert_id} for review")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
if cert_data.get("status") != CertificateStatus.DRAFT:
|
||||
raise HTTPException(status_code=400, detail="Nur Entwuerfe koennen zur Pruefung eingereicht werden")
|
||||
if not cert_data.get("subjects"):
|
||||
raise HTTPException(status_code=400, detail="Keine Fachnoten eingetragen")
|
||||
cert_data["status"] = CertificateStatus.REVIEW
|
||||
_save_certificate(cert_data)
|
||||
return {"message": "Zeugnis wurde zur Pruefung eingereicht", "status": CertificateStatus.REVIEW}
|
||||
|
||||
|
||||
@router.post("/{cert_id}/approve")
|
||||
async def approve_certificate(cert_id: str):
|
||||
"""Genehmigt ein Zeugnis."""
|
||||
logger.info(f"Approving certificate {cert_id}")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
if cert_data.get("status") != CertificateStatus.REVIEW:
|
||||
raise HTTPException(status_code=400, detail="Nur Zeugnisse in Pruefung koennen genehmigt werden")
|
||||
cert_data["status"] = CertificateStatus.APPROVED
|
||||
_save_certificate(cert_data)
|
||||
return {"message": "Zeugnis wurde genehmigt", "status": CertificateStatus.APPROVED}
|
||||
|
||||
|
||||
@router.post("/{cert_id}/issue")
|
||||
async def issue_certificate(cert_id: str):
|
||||
"""Stellt ein Zeugnis offiziell aus."""
|
||||
logger.info(f"Issuing certificate {cert_id}")
|
||||
cert_data = _get_certificate(cert_id)
|
||||
if cert_data.get("status") != CertificateStatus.APPROVED:
|
||||
raise HTTPException(status_code=400, detail="Nur genehmigte Zeugnisse koennen ausgestellt werden")
|
||||
cert_data["status"] = CertificateStatus.ISSUED
|
||||
cert_data["issue_date"] = datetime.now().strftime("%d.%m.%Y")
|
||||
_save_certificate(cert_data)
|
||||
return {"message": "Zeugnis wurde ausgestellt", "status": CertificateStatus.ISSUED, "issue_date": cert_data["issue_date"]}
|
||||
|
||||
|
||||
@router.get("/student/{student_id}", response_model=CertificateListResponse)
|
||||
async def get_certificates_for_student(
|
||||
student_id: str, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100)
|
||||
):
|
||||
"""Laedt alle Zeugnisse fuer einen bestimmten Schueler."""
|
||||
logger.info(f"Getting certificates for student: {student_id}")
|
||||
filtered_certs = [c for c in _certificates_store.values() if c.get("student_id") == student_id]
|
||||
filtered_certs.sort(key=lambda x: (x.get("school_year", ""), x.get("certificate_type", "")), reverse=True)
|
||||
total = len(filtered_certs)
|
||||
start = (page - 1) * page_size
|
||||
paginated_certs = filtered_certs[start:start + page_size]
|
||||
return CertificateListResponse(
|
||||
certificates=[CertificateResponse(**c) for c in paginated_certs],
|
||||
total=total, page=page, page_size=page_size
|
||||
)
|
||||
|
||||
|
||||
@router.get("/class/{class_name}/statistics", response_model=GradeStatistics)
|
||||
async def get_class_statistics(
|
||||
class_name: str,
|
||||
school_year: str = Query(..., description="Schuljahr"),
|
||||
certificate_type: CertificateType = Query(CertificateType.HALBJAHR)
|
||||
):
|
||||
"""Berechnet Notenstatistiken fuer eine Klasse."""
|
||||
logger.info(f"Calculating statistics for class {class_name}")
|
||||
|
||||
class_certs = [
|
||||
c for c in _certificates_store.values()
|
||||
if c.get("student_class") == class_name
|
||||
and c.get("school_year") == school_year
|
||||
and c.get("certificate_type") == certificate_type
|
||||
]
|
||||
|
||||
if not class_certs:
|
||||
raise HTTPException(status_code=404, detail=f"Keine Zeugnisse fuer Klasse {class_name} im Schuljahr {school_year} gefunden")
|
||||
|
||||
all_grades: List[float] = []
|
||||
subject_grades: Dict[str, List[float]] = {}
|
||||
grade_counts = {"1": 0, "2": 0, "3": 0, "4": 0, "5": 0, "6": 0}
|
||||
|
||||
for cert in class_certs:
|
||||
avg = cert.get("average_grade")
|
||||
if avg:
|
||||
all_grades.append(avg)
|
||||
rounded = str(round(avg))
|
||||
if rounded in grade_counts:
|
||||
grade_counts[rounded] += 1
|
||||
|
||||
for subject in cert.get("subjects", []):
|
||||
name = subject.get("name")
|
||||
grade_str = subject.get("grade")
|
||||
try:
|
||||
grade = float(grade_str)
|
||||
if name not in subject_grades:
|
||||
subject_grades[name] = []
|
||||
subject_grades[name].append(grade)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
subject_averages = {
|
||||
name: round(sum(grades) / len(grades), 2)
|
||||
for name, grades in subject_grades.items() if grades
|
||||
}
|
||||
|
||||
return GradeStatistics(
|
||||
class_name=class_name, school_year=school_year,
|
||||
certificate_type=certificate_type, student_count=len(class_certs),
|
||||
average_grade=round(sum(all_grades) / len(all_grades), 2) if all_grades else 0.0,
|
||||
grade_distribution=grade_counts, subject_averages=subject_averages
|
||||
)
|
||||
184
backend-lehrer/letters/certificates_models.py
Normal file
184
backend-lehrer/letters/certificates_models.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
Certificates Models - Pydantic models and enums for Zeugnisverwaltung.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enums
|
||||
# =============================================================================
|
||||
|
||||
class CertificateType(str, Enum):
|
||||
"""Typen von Zeugnissen."""
|
||||
HALBJAHR = "halbjahr"
|
||||
JAHRES = "jahres"
|
||||
ABSCHLUSS = "abschluss"
|
||||
ABGANG = "abgang"
|
||||
UEBERGANG = "uebergang"
|
||||
|
||||
|
||||
class CertificateStatus(str, Enum):
|
||||
"""Status eines Zeugnisses."""
|
||||
DRAFT = "draft"
|
||||
REVIEW = "review"
|
||||
APPROVED = "approved"
|
||||
ISSUED = "issued"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class GradeType(str, Enum):
|
||||
"""Notentyp."""
|
||||
NUMERIC = "numeric"
|
||||
POINTS = "points"
|
||||
TEXT = "text"
|
||||
|
||||
|
||||
class BehaviorGrade(str, Enum):
|
||||
"""Verhaltens-/Arbeitsnoten."""
|
||||
A = "A"
|
||||
B = "B"
|
||||
C = "C"
|
||||
D = "D"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Models
|
||||
# =============================================================================
|
||||
|
||||
class SchoolInfoModel(BaseModel):
|
||||
"""Schulinformationen fuer Zeugnis."""
|
||||
name: str
|
||||
address: str
|
||||
phone: str
|
||||
email: str
|
||||
website: Optional[str] = None
|
||||
principal: Optional[str] = None
|
||||
logo_path: Optional[str] = None
|
||||
|
||||
|
||||
class SubjectGrade(BaseModel):
|
||||
"""Note fuer ein Fach."""
|
||||
name: str = Field(..., description="Fachname")
|
||||
grade: str = Field(..., description="Note (1-6 oder A-D)")
|
||||
points: Optional[int] = Field(None, description="Punkte (Oberstufe, 0-15)")
|
||||
note: Optional[str] = Field(None, description="Bemerkung zum Fach")
|
||||
|
||||
|
||||
class AttendanceInfo(BaseModel):
|
||||
"""Anwesenheitsinformationen."""
|
||||
days_absent: int = Field(0, description="Fehlende Tage gesamt")
|
||||
days_excused: int = Field(0, description="Entschuldigte Tage")
|
||||
days_unexcused: int = Field(0, description="Unentschuldigte Tage")
|
||||
hours_absent: Optional[int] = Field(None, description="Fehlstunden gesamt")
|
||||
|
||||
|
||||
class CertificateCreateRequest(BaseModel):
|
||||
"""Request zum Erstellen eines neuen Zeugnisses."""
|
||||
student_id: str = Field(..., description="ID des Schuelers")
|
||||
student_name: str = Field(..., description="Name des Schuelers")
|
||||
student_birthdate: str = Field(..., description="Geburtsdatum")
|
||||
student_class: str = Field(..., description="Klasse")
|
||||
school_year: str = Field(..., description="Schuljahr (z.B. '2024/2025')")
|
||||
certificate_type: CertificateType = Field(..., description="Art des Zeugnisses")
|
||||
subjects: List[SubjectGrade] = Field(..., description="Fachnoten")
|
||||
attendance: AttendanceInfo = Field(default_factory=AttendanceInfo)
|
||||
remarks: Optional[str] = Field(None, description="Bemerkungen")
|
||||
class_teacher: str = Field(..., description="Klassenlehrer/in")
|
||||
principal: str = Field(..., description="Schulleiter/in")
|
||||
school_info: Optional[SchoolInfoModel] = Field(None)
|
||||
issue_date: Optional[str] = Field(None, description="Ausstellungsdatum")
|
||||
social_behavior: Optional[BehaviorGrade] = Field(None)
|
||||
work_behavior: Optional[BehaviorGrade] = Field(None)
|
||||
|
||||
|
||||
class CertificateUpdateRequest(BaseModel):
|
||||
"""Request zum Aktualisieren eines Zeugnisses."""
|
||||
subjects: Optional[List[SubjectGrade]] = None
|
||||
attendance: Optional[AttendanceInfo] = None
|
||||
remarks: Optional[str] = None
|
||||
class_teacher: Optional[str] = None
|
||||
principal: Optional[str] = None
|
||||
social_behavior: Optional[BehaviorGrade] = None
|
||||
work_behavior: Optional[BehaviorGrade] = None
|
||||
status: Optional[CertificateStatus] = None
|
||||
|
||||
|
||||
class CertificateResponse(BaseModel):
|
||||
"""Response mit Zeugnisdaten."""
|
||||
id: str
|
||||
student_id: str
|
||||
student_name: str
|
||||
student_birthdate: str
|
||||
student_class: str
|
||||
school_year: str
|
||||
certificate_type: CertificateType
|
||||
subjects: List[SubjectGrade]
|
||||
attendance: AttendanceInfo
|
||||
remarks: Optional[str]
|
||||
class_teacher: str
|
||||
principal: str
|
||||
school_info: Optional[SchoolInfoModel]
|
||||
issue_date: Optional[str]
|
||||
social_behavior: Optional[BehaviorGrade]
|
||||
work_behavior: Optional[BehaviorGrade]
|
||||
status: CertificateStatus
|
||||
average_grade: Optional[float]
|
||||
pdf_path: Optional[str]
|
||||
dsms_cid: Optional[str]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class CertificateListResponse(BaseModel):
|
||||
"""Response mit Liste von Zeugnissen."""
|
||||
certificates: List[CertificateResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class GradeStatistics(BaseModel):
|
||||
"""Notenstatistiken fuer eine Klasse."""
|
||||
class_name: str
|
||||
school_year: str
|
||||
certificate_type: CertificateType
|
||||
student_count: int
|
||||
average_grade: float
|
||||
grade_distribution: Dict[str, int]
|
||||
subject_averages: Dict[str, float]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def get_type_label(cert_type: CertificateType) -> str:
|
||||
"""Gibt menschenlesbare Labels fuer Zeugnistypen zurueck."""
|
||||
labels = {
|
||||
CertificateType.HALBJAHR: "Halbjahreszeugnis",
|
||||
CertificateType.JAHRES: "Jahreszeugnis",
|
||||
CertificateType.ABSCHLUSS: "Abschlusszeugnis",
|
||||
CertificateType.ABGANG: "Abgangszeugnis",
|
||||
CertificateType.UEBERGANG: "Uebergangszeugnis",
|
||||
}
|
||||
return labels.get(cert_type, cert_type.value)
|
||||
|
||||
|
||||
def calculate_average(subjects: List[Dict]) -> Optional[float]:
|
||||
"""Berechnet Notendurchschnitt."""
|
||||
numeric_grades = []
|
||||
for subject in subjects:
|
||||
grade = subject.get("grade", "")
|
||||
try:
|
||||
numeric = float(grade)
|
||||
if 1 <= numeric <= 6:
|
||||
numeric_grades.append(numeric)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if numeric_grades:
|
||||
return round(sum(numeric_grades) / len(numeric_grades), 2)
|
||||
return None
|
||||
195
backend-lehrer/letters/models.py
Normal file
195
backend-lehrer/letters/models.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Letters Models - Pydantic models and enums for Elternbrief-Verwaltung.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Enums
|
||||
# =============================================================================
|
||||
|
||||
class LetterType(str, Enum):
|
||||
"""Typen von Elternbriefen."""
|
||||
GENERAL = "general"
|
||||
HALBJAHR = "halbjahr"
|
||||
FEHLZEITEN = "fehlzeiten"
|
||||
ELTERNABEND = "elternabend"
|
||||
LOB = "lob"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class LetterTone(str, Enum):
|
||||
"""Tonalitaet der Briefe."""
|
||||
FORMAL = "formal"
|
||||
PROFESSIONAL = "professional"
|
||||
WARM = "warm"
|
||||
CONCERNED = "concerned"
|
||||
APPRECIATIVE = "appreciative"
|
||||
|
||||
|
||||
class LetterStatus(str, Enum):
|
||||
"""Status eines Briefes."""
|
||||
DRAFT = "draft"
|
||||
SENT = "sent"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pydantic Models
|
||||
# =============================================================================
|
||||
|
||||
class SchoolInfoModel(BaseModel):
|
||||
"""Schulinformationen fuer Briefkopf."""
|
||||
name: str
|
||||
address: str
|
||||
phone: str
|
||||
email: str
|
||||
website: Optional[str] = None
|
||||
principal: Optional[str] = None
|
||||
logo_path: Optional[str] = None
|
||||
|
||||
|
||||
class LegalReferenceModel(BaseModel):
|
||||
"""Rechtliche Referenz."""
|
||||
law: str
|
||||
paragraph: str
|
||||
title: str
|
||||
summary: Optional[str] = None
|
||||
relevance: Optional[str] = None
|
||||
|
||||
|
||||
class LetterCreateRequest(BaseModel):
|
||||
"""Request zum Erstellen eines neuen Briefes."""
|
||||
recipient_name: str = Field(..., description="Name des Empfaengers")
|
||||
recipient_address: str = Field(..., description="Adresse des Empfaengers")
|
||||
student_name: str = Field(..., description="Name des Schuelers")
|
||||
student_class: str = Field(..., description="Klasse des Schuelers")
|
||||
subject: str = Field(..., description="Betreff des Briefes")
|
||||
content: str = Field(..., description="Inhalt des Briefes")
|
||||
letter_type: LetterType = Field(LetterType.GENERAL, description="Art des Briefes")
|
||||
tone: LetterTone = Field(LetterTone.PROFESSIONAL, description="Tonalitaet des Briefes")
|
||||
teacher_name: str = Field(..., description="Name des Lehrers")
|
||||
teacher_title: Optional[str] = Field(None, description="Titel des Lehrers")
|
||||
school_info: Optional[SchoolInfoModel] = Field(None, description="Schulinformationen")
|
||||
legal_references: Optional[List[LegalReferenceModel]] = Field(None, description="Rechtliche Referenzen")
|
||||
gfk_principles_applied: Optional[List[str]] = Field(None, description="Angewandte GFK-Prinzipien")
|
||||
|
||||
|
||||
class LetterUpdateRequest(BaseModel):
|
||||
"""Request zum Aktualisieren eines Briefes."""
|
||||
recipient_name: Optional[str] = None
|
||||
recipient_address: Optional[str] = None
|
||||
student_name: Optional[str] = None
|
||||
student_class: Optional[str] = None
|
||||
subject: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
letter_type: Optional[LetterType] = None
|
||||
tone: Optional[LetterTone] = None
|
||||
teacher_name: Optional[str] = None
|
||||
teacher_title: Optional[str] = None
|
||||
school_info: Optional[SchoolInfoModel] = None
|
||||
legal_references: Optional[List[LegalReferenceModel]] = None
|
||||
gfk_principles_applied: Optional[List[str]] = None
|
||||
status: Optional[LetterStatus] = None
|
||||
|
||||
|
||||
class LetterResponse(BaseModel):
|
||||
"""Response mit Briefdaten."""
|
||||
id: str
|
||||
recipient_name: str
|
||||
recipient_address: str
|
||||
student_name: str
|
||||
student_class: str
|
||||
subject: str
|
||||
content: str
|
||||
letter_type: LetterType
|
||||
tone: LetterTone
|
||||
teacher_name: str
|
||||
teacher_title: Optional[str]
|
||||
school_info: Optional[SchoolInfoModel]
|
||||
legal_references: Optional[List[LegalReferenceModel]]
|
||||
gfk_principles_applied: Optional[List[str]]
|
||||
gfk_score: Optional[float]
|
||||
status: LetterStatus
|
||||
pdf_path: Optional[str]
|
||||
dsms_cid: Optional[str]
|
||||
sent_at: Optional[datetime]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LetterListResponse(BaseModel):
|
||||
"""Response mit Liste von Briefen."""
|
||||
letters: List[LetterResponse]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
|
||||
class ExportPDFRequest(BaseModel):
|
||||
"""Request zum PDF-Export."""
|
||||
letter_id: Optional[str] = Field(None, description="ID eines gespeicherten Briefes")
|
||||
letter_data: Optional[LetterCreateRequest] = Field(None, description="Oder direkte Briefdaten")
|
||||
|
||||
|
||||
class ImproveRequest(BaseModel):
|
||||
"""Request zur GFK-Verbesserung."""
|
||||
content: str = Field(..., description="Text zur Verbesserung")
|
||||
communication_type: Optional[str] = Field("general_info", description="Art der Kommunikation")
|
||||
tone: Optional[str] = Field("professional", description="Gewuenschte Tonalitaet")
|
||||
|
||||
|
||||
class ImproveResponse(BaseModel):
|
||||
"""Response mit verbessertem Text."""
|
||||
improved_content: str
|
||||
changes: List[str]
|
||||
gfk_score: float
|
||||
gfk_principles_applied: List[str]
|
||||
|
||||
|
||||
class SendEmailRequest(BaseModel):
|
||||
"""Request zum Email-Versand."""
|
||||
letter_id: str
|
||||
recipient_email: str
|
||||
cc_emails: Optional[List[str]] = None
|
||||
include_pdf: bool = True
|
||||
|
||||
|
||||
class SendEmailResponse(BaseModel):
|
||||
"""Response nach Email-Versand."""
|
||||
success: bool
|
||||
message: str
|
||||
sent_at: Optional[datetime]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
def get_type_label(letter_type: LetterType) -> str:
|
||||
"""Gibt menschenlesbare Labels fuer Brieftypen zurueck."""
|
||||
labels = {
|
||||
LetterType.GENERAL: "Allgemeine Information",
|
||||
LetterType.HALBJAHR: "Halbjahresinformation",
|
||||
LetterType.FEHLZEITEN: "Fehlzeiten-Mitteilung",
|
||||
LetterType.ELTERNABEND: "Einladung Elternabend",
|
||||
LetterType.LOB: "Positives Feedback",
|
||||
LetterType.CUSTOM: "Benutzerdefiniert",
|
||||
}
|
||||
return labels.get(letter_type, letter_type.value)
|
||||
|
||||
|
||||
def get_tone_label(tone: LetterTone) -> str:
|
||||
"""Gibt menschenlesbare Labels fuer Tonalitaeten zurueck."""
|
||||
labels = {
|
||||
LetterTone.FORMAL: "Sehr foermlich",
|
||||
LetterTone.PROFESSIONAL: "Professionell-freundlich",
|
||||
LetterTone.WARM: "Warmherzig",
|
||||
LetterTone.CONCERNED: "Besorgt",
|
||||
LetterTone.APPRECIATIVE: "Wertschaetzend",
|
||||
}
|
||||
return labels.get(tone, tone.value)
|
||||
Reference in New Issue
Block a user