""" API Routes für Alert-Templates (Playbooks). Endpoints für Guided Mode: - GET /templates - Liste aller verfügbaren Templates - GET /templates/{template_id} - Template-Details - POST /templates/{template_id}/preview - Vorschau generieren - GET /templates/by-role/{role} - Templates für eine Rolle """ from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from sqlalchemy.orm import Session as DBSession from ..db.database import get_db from ..db.models import AlertTemplateDB, UserRoleEnum router = APIRouter(prefix="/templates", tags=["templates"]) # ============================================================================ # Request/Response Models # ============================================================================ class TemplateListItem(BaseModel): """Kurzinfo für Template-Liste.""" id: str slug: str name: str description: str icon: str category: str target_roles: List[str] is_premium: bool max_cards_per_day: int sort_order: int class Config: from_attributes = True class TemplateDetail(BaseModel): """Vollständige Template-Details.""" id: str slug: str name: str description: str icon: str category: str target_roles: List[str] topics_config: List[dict] rules_config: List[dict] profile_config: dict importance_config: dict max_cards_per_day: int digest_enabled: bool digest_day: str is_premium: bool is_active: bool class Config: from_attributes = True class TemplateListResponse(BaseModel): """Response für Template-Liste.""" templates: List[TemplateListItem] total: int class PreviewRequest(BaseModel): """Request für Template-Vorschau.""" sample_count: int = Field(default=3, ge=1, le=5) class PreviewItem(BaseModel): """Ein Vorschau-Item.""" title: str snippet: str importance_level: str why_relevant: str source_name: str class PreviewResponse(BaseModel): """Response für Template-Vorschau.""" template_name: str sample_items: List[PreviewItem] estimated_daily_count: str message: str # ============================================================================ # Endpoints # ============================================================================ @router.get("", response_model=TemplateListResponse) async def list_templates( category: Optional[str] = Query(None, description="Filter nach Kategorie"), role: Optional[str] = Query(None, description="Filter nach Zielrolle"), include_premium: bool = Query(True, description="Premium-Templates einschließen"), db: DBSession = Depends(get_db) ): """ Liste alle verfügbaren Alert-Templates. Templates sind vorkonfigurierte Playbooks für bestimmte Themen (Förderprogramme, Datenschutz, IT-Security, etc.). """ query = db.query(AlertTemplateDB).filter(AlertTemplateDB.is_active == True) if category: query = query.filter(AlertTemplateDB.category == category) if not include_premium: query = query.filter(AlertTemplateDB.is_premium == False) templates = query.order_by(AlertTemplateDB.sort_order).all() # Filter nach Rolle (JSON-Feld) if role: templates = [t for t in templates if role in (t.target_roles or [])] return TemplateListResponse( templates=[ TemplateListItem( id=t.id, slug=t.slug, name=t.name, description=t.description, icon=t.icon or "", category=t.category or "", target_roles=t.target_roles or [], is_premium=t.is_premium or False, max_cards_per_day=t.max_cards_per_day or 10, sort_order=t.sort_order or 0, ) for t in templates ], total=len(templates) ) @router.get("/by-role/{role}", response_model=TemplateListResponse) async def get_templates_by_role( role: str, db: DBSession = Depends(get_db) ): """ Empfohlene Templates für eine bestimmte Rolle. Rollen: - lehrkraft: Fokus auf Unterricht, Fortbildungen, Wettbewerbe - schulleitung: Fokus auf Administration, Fördermittel, Recht - it_beauftragte: Fokus auf IT-Security, Datenschutz """ # Validiere Rolle valid_roles = ["lehrkraft", "schulleitung", "it_beauftragte"] if role not in valid_roles: raise HTTPException( status_code=400, detail=f"Ungültige Rolle. Erlaubt: {', '.join(valid_roles)}" ) templates = db.query(AlertTemplateDB).filter( AlertTemplateDB.is_active == True, AlertTemplateDB.is_premium == False # Nur kostenlose für Empfehlungen ).order_by(AlertTemplateDB.sort_order).all() # Filter nach Rolle filtered = [t for t in templates if role in (t.target_roles or [])] return TemplateListResponse( templates=[ TemplateListItem( id=t.id, slug=t.slug, name=t.name, description=t.description, icon=t.icon or "", category=t.category or "", target_roles=t.target_roles or [], is_premium=t.is_premium or False, max_cards_per_day=t.max_cards_per_day or 10, sort_order=t.sort_order or 0, ) for t in filtered ], total=len(filtered) ) @router.get("/{template_id}", response_model=TemplateDetail) async def get_template( template_id: str, db: DBSession = Depends(get_db) ): """ Vollständige Details eines Templates abrufen. Enthält alle Konfigurationen (Topics, Rules, Profile). """ template = db.query(AlertTemplateDB).filter( AlertTemplateDB.id == template_id ).first() if not template: # Versuche nach Slug zu suchen template = db.query(AlertTemplateDB).filter( AlertTemplateDB.slug == template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template nicht gefunden") return TemplateDetail( id=template.id, slug=template.slug, name=template.name, description=template.description, icon=template.icon or "", category=template.category or "", target_roles=template.target_roles or [], topics_config=template.topics_config or [], rules_config=template.rules_config or [], profile_config=template.profile_config or {}, importance_config=template.importance_config or {}, max_cards_per_day=template.max_cards_per_day or 10, digest_enabled=template.digest_enabled if template.digest_enabled is not None else True, digest_day=template.digest_day or "monday", is_premium=template.is_premium or False, is_active=template.is_active if template.is_active is not None else True, ) @router.post("/{template_id}/preview", response_model=PreviewResponse) async def preview_template( template_id: str, request: PreviewRequest = None, db: DBSession = Depends(get_db) ): """ Generiere eine Vorschau, wie Alerts für dieses Template aussehen würden. Zeigt Beispiel-Alerts mit Wichtigkeitsstufen und "Warum relevant?"-Erklärungen. """ template = db.query(AlertTemplateDB).filter( AlertTemplateDB.id == template_id ).first() if not template: template = db.query(AlertTemplateDB).filter( AlertTemplateDB.slug == template_id ).first() if not template: raise HTTPException(status_code=404, detail="Template nicht gefunden") # Generiere Beispiel-Alerts basierend auf Template-Konfiguration sample_items = _generate_preview_items(template) return PreviewResponse( template_name=template.name, sample_items=sample_items[:request.sample_count if request else 3], estimated_daily_count=f"Ca. {template.max_cards_per_day} Meldungen pro Tag", message=f"Diese Vorschau zeigt, wie Alerts für '{template.name}' aussehen würden." ) @router.post("/seed") async def seed_templates( force_update: bool = Query(False, description="Bestehende Templates aktualisieren"), db: DBSession = Depends(get_db) ): """ Fügt die vordefinierten Templates in die Datenbank ein. Nur für Entwicklung/Setup. """ from ..data.templates import seed_templates as do_seed count = do_seed(db, force_update=force_update) return { "status": "success", "templates_created": count, "message": f"{count} Templates wurden eingefügt/aktualisiert." } # ============================================================================ # Helper Functions # ============================================================================ def _generate_preview_items(template: AlertTemplateDB) -> List[PreviewItem]: """ Generiere Beispiel-Alerts für Template-Vorschau. Diese sind statisch/exemplarisch, nicht aus echten Daten. """ # Template-spezifische Beispiele examples = { "foerderprogramme": [ PreviewItem( title="DigitalPakt 2.0: Neue Antragsphase startet am 1. April", snippet="Das BMBF hat die zweite Phase des DigitalPakt Schule angekündigt...", importance_level="DRINGEND", why_relevant="Frist endet in 45 Tagen. Betrifft alle Schulen mit Förderbedarf.", source_name="Bundesministerium für Bildung" ), PreviewItem( title="Landesförderung: 50.000€ für innovative Schulprojekte", snippet="Das Kultusministerium fördert Schulen, die digitale Lernkonzepte...", importance_level="WICHTIG", why_relevant="Passende Förderung für Ihr Bundesland. Keine Eigenbeteiligung erforderlich.", source_name="Kultusministerium" ), PreviewItem( title="Erasmus+ Schulpartnerschaften: Jetzt bewerben", snippet="Für das Schuljahr 2026/27 können Schulen EU-Förderung beantragen...", importance_level="PRUEFEN", why_relevant="EU-Programm mit hoher Fördersumme. Bewerbungsfrist in 3 Monaten.", source_name="EU-Kommission" ), ], "abitur-updates": [ PreviewItem( title="Neue Operatoren für Abitur Deutsch ab 2027", snippet="Die KMK hat überarbeitete Operatoren für das Fach Deutsch beschlossen...", importance_level="WICHTIG", why_relevant="Betrifft die Oberstufenplanung. Anpassung der Klausuren erforderlich.", source_name="KMK" ), PreviewItem( title="Abiturtermine 2026: Prüfungsplan veröffentlicht", snippet="Das Kultusministerium hat die Termine für das Abitur 2026 bekannt gegeben...", importance_level="INFO", why_relevant="Planungsgrundlage für Schuljahreskalender.", source_name="Kultusministerium" ), ], "datenschutz-recht": [ PreviewItem( title="LfDI: Neue Handreichung zu Schülerfotos", snippet="Der Landesbeauftragte für Datenschutz hat eine aktualisierte...", importance_level="DRINGEND", why_relevant="Handlungsbedarf: Bestehende Einwilligungen müssen geprüft werden.", source_name="Datenschutzbeauftragter" ), PreviewItem( title="Microsoft 365 an Schulen: Neue Bewertung", snippet="Die Datenschutzkonferenz hat ihre Position zu Microsoft 365 aktualisiert...", importance_level="WICHTIG", why_relevant="Betrifft Schulen mit Microsoft-Lizenzen. Dokumentationspflicht.", source_name="DSK" ), ], "it-security": [ PreviewItem( title="CVE-2026-1234: Kritische Lücke in Moodle", snippet="Eine schwerwiegende Sicherheitslücke wurde in Moodle 4.x gefunden...", importance_level="KRITISCH", why_relevant="Sofortiges Update erforderlich. Exploit bereits aktiv.", source_name="BSI CERT-Bund" ), PreviewItem( title="Phishing-Welle: Gefälschte Schulportal-Mails", snippet="Aktuell werden vermehrt Phishing-Mails an Lehrkräfte versendet...", importance_level="DRINGEND", why_relevant="Warnung an Kollegium empfohlen. Erkennungsmerkmale beachten.", source_name="BSI" ), ], "fortbildungen": [ PreviewItem( title="Kostenlose Fortbildung: KI im Unterricht", snippet="Das Landesinstitut bietet eine Online-Fortbildung zu KI-Tools...", importance_level="PRUEFEN", why_relevant="Passt zu Ihrem Interessenprofil. Online-Format, 4 Stunden.", source_name="Landesinstitut" ), ], "wettbewerbe-projekte": [ PreviewItem( title="Jugend forscht: Anmeldung bis 30. November", snippet="Der größte deutsche MINT-Wettbewerb sucht wieder junge Forscher...", importance_level="WICHTIG", why_relevant="Frist in 60 Tagen. Für Schüler ab Klasse 4.", source_name="Jugend forscht e.V." ), ], } # Hole Beispiele für dieses Template oder generische slug = template.slug if slug in examples: return examples[slug] # Generische Beispiele return [ PreviewItem( title=f"Beispiel-Meldung für {template.name}", snippet=f"Dies ist eine Vorschau, wie Alerts für das Thema '{template.name}' aussehen würden.", importance_level="INFO", why_relevant="Passend zu Ihren ausgewählten Themen.", source_name="Beispielquelle" ) ]