"""
Classroom API - Export Routes
PDF/HTML export endpoints (Phase 5).
"""
from datetime import datetime
import logging
from fastapi import APIRouter, HTTPException
from fastapi.responses import HTMLResponse
from classroom_engine import LessonPhase
from ..services.persistence import (
sessions,
init_db_if_needed,
DB_ENABLED,
SessionLocal,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Export"])
@router.get("/export/session/{session_id}", response_class=HTMLResponse)
async def export_session_html(session_id: str) -> HTMLResponse:
"""
Exportiert eine Session-Zusammenfassung als druckbares HTML.
Kann im Browser ueber Strg+P als PDF gespeichert werden.
"""
# Session-Daten aus Memory oder DB holen
session = sessions.get(session_id)
if not session and DB_ENABLED:
init_db_if_needed()
with SessionLocal() as db:
from classroom_engine.repository import AnalyticsRepository
repo = AnalyticsRepository(db)
summary = repo.get_session_summary(session_id)
if not summary:
raise HTTPException(
status_code=404,
detail=f"Session {session_id} not found"
)
# HTML generieren aus Summary
return HTMLResponse(content=_generate_export_html_from_summary(summary))
if not session:
raise HTTPException(
status_code=404,
detail=f"Session {session_id} not found"
)
# HTML aus In-Memory Session generieren
return HTMLResponse(content=_generate_export_html_from_session(session))
def _generate_export_html_from_summary(summary) -> str:
"""Generiert druckbares HTML aus einer SessionSummary."""
phases_html = ""
for phase in summary.phase_statistics:
diff_class = "on-time"
if phase.difference_seconds < -60:
diff_class = "under-time"
elif phase.difference_seconds > 180:
diff_class = "way-over"
elif phase.difference_seconds > 60:
diff_class = "over-time"
phases_html += f"""
| {phase.display_name} |
{phase.planned_duration_seconds // 60}:{phase.planned_duration_seconds % 60:02d} |
{phase.actual_duration_seconds // 60}:{phase.actual_duration_seconds % 60:02d} |
{phase.difference_formatted} |
"""
return _get_export_html_template(
subject=summary.subject,
class_id=summary.class_id,
topic=summary.topic,
date_formatted=summary.date_formatted,
total_duration_formatted=summary.total_duration_formatted,
phases_completed=summary.phases_completed,
total_phases=summary.total_phases,
total_overtime_formatted=summary.total_overtime_formatted,
phases_html=phases_html,
phases_with_overtime=summary.phases_with_overtime,
total_overtime_seconds=summary.total_overtime_seconds,
reflection_notes=summary.reflection_notes,
)
def _generate_export_html_from_session(session) -> str:
"""Generiert druckbares HTML aus einer In-Memory Session."""
# Phasen-Tabelle generieren
phases_html = ""
total_overtime = 0
for entry in session.phase_history:
phase = entry.get("phase", "")
if phase in ["not_started", "ended"]:
continue
planned = session.phase_durations.get(phase, 0) * 60
actual = entry.get("duration_seconds", 0) or 0
diff = actual - planned
if diff > 0:
total_overtime += diff
diff_class = "on-time"
if diff < -60:
diff_class = "under-time"
elif diff > 180:
diff_class = "way-over"
elif diff > 60:
diff_class = "over-time"
phase_names = {
"einstieg": "Einstieg",
"erarbeitung": "Erarbeitung",
"sicherung": "Sicherung",
"transfer": "Transfer",
"reflexion": "Reflexion",
}
phases_html += f"""
| {phase_names.get(phase, phase)} |
{planned // 60}:{planned % 60:02d} |
{actual // 60}:{actual % 60:02d} |
{'+' if diff >= 0 else ''}{diff // 60}:{abs(diff) % 60:02d} |
"""
# Zeiten
total_duration = 0
date_str = "--"
if session.lesson_started_at:
date_str = session.lesson_started_at.strftime("%d.%m.%Y %H:%M")
if session.lesson_ended_at:
total_duration = int((session.lesson_ended_at - session.lesson_started_at).total_seconds())
total_mins = total_duration // 60
total_secs = total_duration % 60
overtime_mins = total_overtime // 60
overtime_secs = total_overtime % 60
completed_phases = len([e for e in session.phase_history if e.get("ended_at")])
return _get_export_html_template(
subject=session.subject,
class_id=session.class_id,
topic=session.topic,
date_formatted=date_str,
total_duration_formatted=f"{total_mins:02d}:{total_secs:02d}",
phases_completed=completed_phases,
total_phases=5,
total_overtime_formatted=f"{overtime_mins:02d}:{overtime_secs:02d}",
phases_html=phases_html,
phases_with_overtime=len([e for e in session.phase_history if e.get("duration_seconds", 0) > session.phase_durations.get(e.get("phase", ""), 0) * 60]),
total_overtime_seconds=total_overtime,
reflection_notes="",
)
def _get_export_html_template(
subject: str,
class_id: str,
topic: str,
date_formatted: str,
total_duration_formatted: str,
phases_completed: int,
total_phases: int,
total_overtime_formatted: str,
phases_html: str,
phases_with_overtime: int,
total_overtime_seconds: int,
reflection_notes: str,
) -> str:
"""Returns the full HTML template for export."""
return f"""
Stundenprotokoll - {subject}
Phasen-Analyse
| Phase |
Geplant |
Tatsaechlich |
Differenz |
{phases_html}
{f'''
Overtime-Zusammenfassung:
{phases_with_overtime} von {total_phases} Phasen hatten Overtime
(gesamt: {total_overtime_formatted})
''' if total_overtime_seconds > 0 else ''}
{f'''
Reflexion
{reflection_notes}
''' if reflection_notes else ''}
"""