""" Classroom API - Feedback Endpoints. Endpoints fuer Lehrer-Feedback (Feature Request Tracking). """ from uuid import uuid4 from typing import Dict, List, Optional, Any from datetime import datetime import logging from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from .shared import init_db_if_needed, DB_ENABLED, logger try: from classroom_engine.database import SessionLocal from classroom_engine.repository import TeacherFeedbackRepository except ImportError: pass router = APIRouter(tags=["Feedback"]) # In-Memory Storage (Fallback) _feedback: Dict[str, dict] = {} # === Pydantic Models === class CreateFeedbackRequest(BaseModel): """Request zum Erstellen von Feedback.""" teacher_id: str session_id: Optional[str] = None category: str = Field(..., description="bug, feature, usability, content, other") title: str = Field(..., min_length=1, max_length=200) description: str = Field(..., min_length=10, max_length=5000) priority: str = Field("medium", description="low, medium, high, critical") context_data: Optional[Dict[str, Any]] = None class FeedbackResponse(BaseModel): """Response fuer ein Feedback.""" feedback_id: str teacher_id: str session_id: Optional[str] category: str title: str description: str priority: str status: str context_data: Optional[Dict[str, Any]] admin_notes: Optional[str] created_at: str updated_at: Optional[str] class FeedbackListResponse(BaseModel): """Response fuer Feedback-Liste.""" feedback: List[FeedbackResponse] total_count: int class FeedbackStatsResponse(BaseModel): """Response fuer Feedback-Statistiken.""" total: int by_category: Dict[str, int] by_status: Dict[str, int] by_priority: Dict[str, int] # === Endpoints === @router.post("/feedback", response_model=FeedbackResponse, status_code=201) async def create_feedback(request: CreateFeedbackRequest) -> FeedbackResponse: """Erstellt ein neues Feedback.""" init_db_if_needed() valid_categories = ["bug", "feature", "usability", "content", "other"] if request.category not in valid_categories: raise HTTPException(status_code=400, detail=f"Invalid category. Must be one of: {valid_categories}") valid_priorities = ["low", "medium", "high", "critical"] if request.priority not in valid_priorities: raise HTTPException(status_code=400, detail=f"Invalid priority. Must be one of: {valid_priorities}") feedback_id = str(uuid4()) now = datetime.utcnow() feedback_data = { "feedback_id": feedback_id, "teacher_id": request.teacher_id, "session_id": request.session_id, "category": request.category, "title": request.title, "description": request.description, "priority": request.priority, "status": "open", "context_data": request.context_data, "admin_notes": None, "created_at": now.isoformat(), "updated_at": None, } if DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) repo.create(feedback_data) db.close() except Exception as e: logger.warning(f"DB persist failed for feedback: {e}") _feedback[feedback_id] = feedback_data return FeedbackResponse(**feedback_data) @router.get("/feedback", response_model=FeedbackListResponse) async def list_feedback( teacher_id: Optional[str] = Query(None), category: Optional[str] = Query(None), status: Optional[str] = Query(None), priority: Optional[str] = Query(None), limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0) ) -> FeedbackListResponse: """Listet Feedback (optional gefiltert).""" init_db_if_needed() feedback_list = [] if DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) db_feedback = repo.get_all( teacher_id=teacher_id, category=category, status=status, priority=priority, limit=limit, offset=offset ) for fb in db_feedback: feedback_list.append(FeedbackResponse(**fb)) total = repo.count(teacher_id=teacher_id, category=category, status=status) db.close() return FeedbackListResponse(feedback=feedback_list, total_count=total) except Exception as e: logger.warning(f"DB read failed for feedback: {e}") # Fallback auf Memory for fb in _feedback.values(): if teacher_id and fb["teacher_id"] != teacher_id: continue if category and fb["category"] != category: continue if status and fb["status"] != status: continue if priority and fb["priority"] != priority: continue feedback_list.append(FeedbackResponse(**fb)) total = len(feedback_list) feedback_list = feedback_list[offset:offset + limit] return FeedbackListResponse(feedback=feedback_list, total_count=total) @router.get("/feedback/stats", response_model=FeedbackStatsResponse) async def get_feedback_stats() -> FeedbackStatsResponse: """Gibt Feedback-Statistiken zurueck.""" init_db_if_needed() if DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) stats = repo.get_stats() db.close() return FeedbackStatsResponse(**stats) except Exception as e: logger.warning(f"DB read failed for feedback stats: {e}") # Fallback auf Memory by_category: Dict[str, int] = {} by_status: Dict[str, int] = {} by_priority: Dict[str, int] = {} for fb in _feedback.values(): cat = fb["category"] by_category[cat] = by_category.get(cat, 0) + 1 st = fb["status"] by_status[st] = by_status.get(st, 0) + 1 pr = fb["priority"] by_priority[pr] = by_priority.get(pr, 0) + 1 return FeedbackStatsResponse( total=len(_feedback), by_category=by_category, by_status=by_status, by_priority=by_priority, ) @router.get("/feedback/{feedback_id}") async def get_feedback(feedback_id: str) -> FeedbackResponse: """Ruft ein einzelnes Feedback ab.""" init_db_if_needed() if feedback_id in _feedback: return FeedbackResponse(**_feedback[feedback_id]) if DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) fb = repo.get_by_id(feedback_id) db.close() if fb: return FeedbackResponse(**fb) except Exception as e: logger.warning(f"DB read failed: {e}") raise HTTPException(status_code=404, detail="Feedback nicht gefunden") @router.put("/feedback/{feedback_id}/status") async def update_feedback_status( feedback_id: str, status: str = Query(..., description="open, in_progress, resolved, closed, wont_fix") ) -> FeedbackResponse: """Aktualisiert den Status eines Feedbacks.""" init_db_if_needed() valid_statuses = ["open", "in_progress", "resolved", "closed", "wont_fix"] if status not in valid_statuses: raise HTTPException(status_code=400, detail=f"Invalid status. Must be one of: {valid_statuses}") feedback_data = _feedback.get(feedback_id) if not feedback_data and DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) feedback_data = repo.get_by_id(feedback_id) db.close() except Exception as e: logger.warning(f"DB read failed: {e}") if not feedback_data: raise HTTPException(status_code=404, detail="Feedback nicht gefunden") feedback_data["status"] = status feedback_data["updated_at"] = datetime.utcnow().isoformat() if DB_ENABLED: try: db = SessionLocal() repo = TeacherFeedbackRepository(db) repo.update_status(feedback_id, status) db.close() except Exception as e: logger.warning(f"DB update failed: {e}") _feedback[feedback_id] = feedback_data return FeedbackResponse(**feedback_data)