""" API Routes für User Alert Subscriptions. Verwaltet Nutzer-Abonnements für Templates und Digest-Einstellungen. Endpoints: - POST /subscriptions - Neue Subscription erstellen - GET /subscriptions - User-Subscriptions auflisten - GET /subscriptions/{id} - Subscription-Details - PUT /subscriptions/{id} - Subscription aktualisieren - DELETE /subscriptions/{id} - Subscription deaktivieren - POST /subscriptions/{id}/activate-template - Template aktivieren """ import uuid from typing import Optional, List from datetime import datetime 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 ( UserAlertSubscriptionDB, AlertTemplateDB, AlertProfileDB, AlertTopicDB, AlertRuleDB, AlertModeEnum, UserRoleEnum ) router = APIRouter(prefix="/subscriptions", tags=["subscriptions"]) # ============================================================================ # Request/Response Models # ============================================================================ class SubscriptionCreate(BaseModel): """Request für neue Subscription.""" mode: str = Field(default="guided", description="'guided' oder 'expert'") user_role: Optional[str] = Field(default=None, description="lehrkraft, schulleitung, it_beauftragte") template_ids: List[str] = Field(default=[], description="Ausgewählte Template-IDs (max. 3)") notification_email: Optional[str] = Field(default=None) digest_enabled: bool = Field(default=True) digest_frequency: str = Field(default="weekly") class SubscriptionUpdate(BaseModel): """Request für Subscription-Update.""" template_ids: Optional[List[str]] = None notification_email: Optional[str] = None digest_enabled: Optional[bool] = None digest_frequency: Optional[str] = None is_active: Optional[bool] = None class SubscriptionResponse(BaseModel): """Response für eine Subscription.""" id: str user_id: str mode: str user_role: Optional[str] selected_template_ids: List[str] template_names: List[str] notification_email: Optional[str] digest_enabled: bool digest_frequency: str wizard_completed: bool is_active: bool created_at: datetime updated_at: datetime class SubscriptionListResponse(BaseModel): """Response für Subscription-Liste.""" subscriptions: List[SubscriptionResponse] total: int class ActivateTemplateRequest(BaseModel): """Request für Template-Aktivierung.""" create_topics: bool = Field(default=True, description="Topics aus Template-Config erstellen") create_rules: bool = Field(default=True, description="Rules aus Template-Config erstellen") class ActivateTemplateResponse(BaseModel): """Response für Template-Aktivierung.""" status: str topics_created: int rules_created: int profile_updated: bool message: str # ============================================================================ # Helper Functions # ============================================================================ def get_user_id_from_request() -> str: """ Extrahiert User-ID aus Request. TODO: JWT-Token auswerten, aktuell Dummy. """ return "demo-user" def _get_template_names(db: DBSession, template_ids: List[str]) -> List[str]: """Hole Template-Namen für IDs.""" if not template_ids: return [] templates = db.query(AlertTemplateDB).filter( AlertTemplateDB.id.in_(template_ids) ).all() return [t.name for t in templates] def _subscription_to_response(sub: UserAlertSubscriptionDB, db: DBSession) -> SubscriptionResponse: """Konvertiere DB-Model zu Response.""" template_ids = sub.selected_template_ids or [] return SubscriptionResponse( id=sub.id, user_id=sub.user_id, mode=sub.mode.value if sub.mode else "guided", user_role=sub.user_role.value if sub.user_role else None, selected_template_ids=template_ids, template_names=_get_template_names(db, template_ids), notification_email=sub.notification_email, digest_enabled=sub.digest_enabled if sub.digest_enabled is not None else True, digest_frequency=sub.digest_frequency or "weekly", wizard_completed=sub.wizard_completed if sub.wizard_completed is not None else False, is_active=sub.is_active if sub.is_active is not None else True, created_at=sub.created_at, updated_at=sub.updated_at, ) # ============================================================================ # Endpoints # ============================================================================ @router.post("", response_model=SubscriptionResponse) async def create_subscription( request: SubscriptionCreate, db: DBSession = Depends(get_db) ): """ Erstelle eine neue Alert-Subscription. Im Guided Mode werden 1-3 Templates ausgewählt. Im Expert Mode wird ein eigenes Profil konfiguriert. """ user_id = get_user_id_from_request() # Validiere Modus try: mode = AlertModeEnum(request.mode) except ValueError: raise HTTPException(status_code=400, detail="Ungültiger Modus. Erlaubt: 'guided', 'expert'") # Validiere Rolle user_role = None if request.user_role: try: user_role = UserRoleEnum(request.user_role) except ValueError: raise HTTPException( status_code=400, detail="Ungültige Rolle. Erlaubt: 'lehrkraft', 'schulleitung', 'it_beauftragte'" ) # Validiere Template-IDs if request.template_ids: if len(request.template_ids) > 3: raise HTTPException(status_code=400, detail="Maximal 3 Templates erlaubt") # Prüfe ob Templates existieren existing = db.query(AlertTemplateDB).filter( AlertTemplateDB.id.in_(request.template_ids) ).count() if existing != len(request.template_ids): raise HTTPException(status_code=400, detail="Eine oder mehrere Template-IDs sind ungültig") # Erstelle Subscription subscription = UserAlertSubscriptionDB( id=str(uuid.uuid4()), user_id=user_id, mode=mode, user_role=user_role, selected_template_ids=request.template_ids, notification_email=request.notification_email, digest_enabled=request.digest_enabled, digest_frequency=request.digest_frequency, wizard_completed=len(request.template_ids) > 0, # Abgeschlossen wenn Templates gewählt is_active=True, ) db.add(subscription) db.commit() db.refresh(subscription) return _subscription_to_response(subscription, db) @router.get("", response_model=SubscriptionListResponse) async def list_subscriptions( active_only: bool = Query(True, description="Nur aktive Subscriptions"), db: DBSession = Depends(get_db) ): """Liste alle Subscriptions des aktuellen Users.""" user_id = get_user_id_from_request() query = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.user_id == user_id ) if active_only: query = query.filter(UserAlertSubscriptionDB.is_active == True) subscriptions = query.order_by(UserAlertSubscriptionDB.created_at.desc()).all() return SubscriptionListResponse( subscriptions=[_subscription_to_response(s, db) for s in subscriptions], total=len(subscriptions) ) @router.get("/{subscription_id}", response_model=SubscriptionResponse) async def get_subscription( subscription_id: str, db: DBSession = Depends(get_db) ): """Hole Details einer Subscription.""" user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.id == subscription_id, UserAlertSubscriptionDB.user_id == user_id ).first() if not subscription: raise HTTPException(status_code=404, detail="Subscription nicht gefunden") return _subscription_to_response(subscription, db) @router.put("/{subscription_id}", response_model=SubscriptionResponse) async def update_subscription( subscription_id: str, request: SubscriptionUpdate, db: DBSession = Depends(get_db) ): """Aktualisiere eine Subscription.""" user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.id == subscription_id, UserAlertSubscriptionDB.user_id == user_id ).first() if not subscription: raise HTTPException(status_code=404, detail="Subscription nicht gefunden") # Update Felder if request.template_ids is not None: if len(request.template_ids) > 3: raise HTTPException(status_code=400, detail="Maximal 3 Templates erlaubt") subscription.selected_template_ids = request.template_ids if request.notification_email is not None: subscription.notification_email = request.notification_email if request.digest_enabled is not None: subscription.digest_enabled = request.digest_enabled if request.digest_frequency is not None: subscription.digest_frequency = request.digest_frequency if request.is_active is not None: subscription.is_active = request.is_active subscription.updated_at = datetime.utcnow() db.commit() db.refresh(subscription) return _subscription_to_response(subscription, db) @router.delete("/{subscription_id}") async def deactivate_subscription( subscription_id: str, db: DBSession = Depends(get_db) ): """Deaktiviere eine Subscription (Soft-Delete).""" user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.id == subscription_id, UserAlertSubscriptionDB.user_id == user_id ).first() if not subscription: raise HTTPException(status_code=404, detail="Subscription nicht gefunden") subscription.is_active = False subscription.updated_at = datetime.utcnow() db.commit() return {"status": "success", "message": "Subscription deaktiviert"} @router.post("/{subscription_id}/activate-template", response_model=ActivateTemplateResponse) async def activate_template( subscription_id: str, request: ActivateTemplateRequest = None, db: DBSession = Depends(get_db) ): """ Aktiviere die gewählten Templates für eine Subscription. Erstellt: - Topics aus Template.topics_config (RSS-Feeds) - Rules aus Template.rules_config (Filter-Regeln) - Aktualisiert das User-Profil mit Template.profile_config """ user_id = get_user_id_from_request() subscription = db.query(UserAlertSubscriptionDB).filter( UserAlertSubscriptionDB.id == subscription_id, UserAlertSubscriptionDB.user_id == user_id ).first() if not subscription: raise HTTPException(status_code=404, detail="Subscription nicht gefunden") if not subscription.selected_template_ids: raise HTTPException(status_code=400, detail="Keine Templates ausgewählt") # Lade Templates templates = db.query(AlertTemplateDB).filter( AlertTemplateDB.id.in_(subscription.selected_template_ids) ).all() topics_created = 0 rules_created = 0 profile_updated = False for template in templates: # Topics erstellen if request is None or request.create_topics: for topic_config in (template.topics_config or []): topic = AlertTopicDB( id=str(uuid.uuid4()), user_id=user_id, name=topic_config.get("name", f"Topic from {template.name}"), description=f"Automatisch erstellt aus Template: {template.name}", is_active=True, fetch_interval_minutes=60, ) db.add(topic) topics_created += 1 # Rules erstellen if request is None or request.create_rules: for rule_config in (template.rules_config or []): rule = AlertRuleDB( id=str(uuid.uuid4()), user_id=user_id, name=rule_config.get("name", f"Rule from {template.name}"), description=f"Automatisch erstellt aus Template: {template.name}", conditions=rule_config.get("conditions", []), action_type=rule_config.get("action_type", "keep"), action_config=rule_config.get("action_config", {}), priority=rule_config.get("priority", 50), is_active=True, ) db.add(rule) rules_created += 1 # Profil aktualisieren if template.profile_config: profile = db.query(AlertProfileDB).filter( AlertProfileDB.user_id == user_id ).first() if not profile: profile = AlertProfileDB( id=str(uuid.uuid4()), user_id=user_id, name=f"Profil für {user_id}", ) db.add(profile) # Merge priorities existing_priorities = profile.priorities or [] new_priorities = template.profile_config.get("priorities", []) for p in new_priorities: if p not in existing_priorities: existing_priorities.append(p) profile.priorities = existing_priorities # Merge exclusions existing_exclusions = profile.exclusions or [] new_exclusions = template.profile_config.get("exclusions", []) for e in new_exclusions: if e not in existing_exclusions: existing_exclusions.append(e) profile.exclusions = existing_exclusions profile_updated = True # Markiere Wizard als abgeschlossen subscription.wizard_completed = True subscription.updated_at = datetime.utcnow() db.commit() return ActivateTemplateResponse( status="success", topics_created=topics_created, rules_created=rules_created, profile_updated=profile_updated, message=f"Templates aktiviert: {topics_created} Topics, {rules_created} Rules erstellt." )