Files
breakpilot-lehrer/backend-lehrer/classroom/routes/feedback.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

281 lines
9.3 KiB
Python

"""
Classroom API - Feedback Routes
Teacher feedback endpoints (Phase 7).
"""
from uuid import uuid4
from typing import Dict, List, Any, Optional
from datetime import datetime
import logging
from fastapi import APIRouter, HTTPException, Query, Request
from ..models import (
FeedbackCreate,
FeedbackResponse,
FeedbackListResponse,
FeedbackStatsResponse,
)
from ..services.persistence import (
init_db_if_needed,
DB_ENABLED,
SessionLocal,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Feedback"])
# In-Memory Fallback wenn DB nicht verfuegbar
_feedback_store: List[Dict[str, Any]] = []
async def get_optional_current_user(request: Request) -> Dict[str, Any]:
"""Gets current user from auth token if available."""
# Simplified - in production this would check JWT token
return {
"user_id": "anonymous",
"name": "",
"email": "",
}
@router.post("/feedback", response_model=FeedbackResponse, status_code=201)
async def create_feedback(
data: FeedbackCreate,
request: Request,
teacher_id: Optional[str] = Query(None, description="Lehrer-ID (optional, wird aus Auth Token gelesen)")
) -> FeedbackResponse:
"""
Erstellt neues Lehrer-Feedback.
Ermoeglicht Lehrern, Bug-Reports, Feature-Requests und Verbesserungsvorschlaege
direkt aus dem Lehrer-Frontend zu senden.
Authentifizierung optional - wenn eingeloggt, wird User-ID automatisch verwendet.
"""
init_db_if_needed()
# Auth: User aus Token holen oder Demo-User verwenden
user = await get_optional_current_user(request)
effective_teacher_id = teacher_id or user.get("user_id", "anonymous")
feedback_id = str(uuid4())
created_at = datetime.utcnow()
if DB_ENABLED:
try:
from classroom_engine.repository import TeacherFeedbackRepository
with SessionLocal() as db:
repo = TeacherFeedbackRepository(db)
db_feedback = repo.create(
teacher_id=effective_teacher_id,
title=data.title,
description=data.description,
feedback_type=data.feedback_type,
priority=data.priority,
teacher_name=data.teacher_name or user.get("name", ""),
teacher_email=data.teacher_email or user.get("email", ""),
context_url=data.context_url,
context_phase=data.context_phase,
context_session_id=data.context_session_id,
related_feature=data.related_feature,
)
return FeedbackResponse(
id=db_feedback.id,
teacher_id=db_feedback.teacher_id,
teacher_name=db_feedback.teacher_name,
title=db_feedback.title,
description=db_feedback.description,
feedback_type=db_feedback.feedback_type.value,
priority=db_feedback.priority.value,
status=db_feedback.status.value,
created_at=db_feedback.created_at.isoformat(),
)
except Exception as e:
logger.error(f"Failed to create feedback in DB: {e}")
# Fallback: In-Memory Storage
feedback = {
"id": feedback_id,
"teacher_id": effective_teacher_id,
"teacher_name": data.teacher_name,
"teacher_email": data.teacher_email,
"title": data.title,
"description": data.description,
"feedback_type": data.feedback_type,
"priority": data.priority,
"status": "new",
"related_feature": data.related_feature,
"context_url": data.context_url,
"context_phase": data.context_phase,
"context_session_id": data.context_session_id,
"response": None,
"created_at": created_at.isoformat(),
"updated_at": created_at.isoformat(),
}
_feedback_store.append(feedback)
return FeedbackResponse(
id=feedback_id,
teacher_id=effective_teacher_id,
teacher_name=data.teacher_name or user.get("name", ""),
title=data.title,
description=data.description,
feedback_type=data.feedback_type,
priority=data.priority,
status="new",
created_at=created_at.isoformat(),
)
@router.get("/feedback", response_model=FeedbackListResponse)
async def list_feedback(
status: Optional[str] = Query(None, description="Filter nach Status"),
feedback_type: Optional[str] = Query(None, description="Filter nach Typ"),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0)
) -> FeedbackListResponse:
"""
Listet alle Feedbacks (fuer Developer Dashboard).
"""
init_db_if_needed()
if DB_ENABLED:
try:
from classroom_engine.repository import TeacherFeedbackRepository
with SessionLocal() as db:
repo = TeacherFeedbackRepository(db)
feedbacks = repo.get_all(
status=status,
feedback_type=feedback_type,
limit=limit,
offset=offset
)
return FeedbackListResponse(
feedbacks=[repo.to_dict(fb) for fb in feedbacks],
total=len(feedbacks)
)
except Exception as e:
logger.error(f"Failed to list feedback from DB: {e}")
# Fallback: In-Memory
result = _feedback_store
if status:
result = [fb for fb in result if fb.get("status") == status]
if feedback_type:
result = [fb for fb in result if fb.get("feedback_type") == feedback_type]
return FeedbackListResponse(
feedbacks=result[offset:offset + limit],
total=len(result)
)
@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:
from classroom_engine.repository import TeacherFeedbackRepository
with SessionLocal() as db:
repo = TeacherFeedbackRepository(db)
stats = repo.get_stats()
return FeedbackStatsResponse(**stats)
except Exception as e:
logger.error(f"Failed to get feedback stats: {e}")
# Fallback: In-Memory
stats = {
"total": len(_feedback_store),
"by_status": {},
"by_type": {},
"by_priority": {},
}
for fb in _feedback_store:
stats["by_status"][fb["status"]] = stats["by_status"].get(fb["status"], 0) + 1
stats["by_type"][fb["feedback_type"]] = stats["by_type"].get(fb["feedback_type"], 0) + 1
stats["by_priority"][fb["priority"]] = stats["by_priority"].get(fb["priority"], 0) + 1
return FeedbackStatsResponse(**stats)
@router.get("/feedback/{feedback_id}")
async def get_feedback(feedback_id: str) -> Dict[str, Any]:
"""
Ruft ein einzelnes Feedback ab.
"""
init_db_if_needed()
if DB_ENABLED:
try:
from classroom_engine.repository import TeacherFeedbackRepository
with SessionLocal() as db:
repo = TeacherFeedbackRepository(db)
db_feedback = repo.get_by_id(feedback_id)
if db_feedback:
return repo.to_dict(db_feedback)
except Exception as e:
logger.error(f"Failed to get feedback: {e}")
# Fallback: In-Memory
for fb in _feedback_store:
if fb["id"] == feedback_id:
return fb
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="Neuer Status"),
response: Optional[str] = Query(None, description="Antwort"),
responded_by: Optional[str] = Query(None, description="Wer antwortet")
) -> Dict[str, Any]:
"""
Aktualisiert den Status eines Feedbacks (fuer Entwickler).
"""
init_db_if_needed()
valid_statuses = ["new", "acknowledged", "planned", "implemented", "declined"]
if status not in valid_statuses:
raise HTTPException(
status_code=400,
detail=f"Ungueltiger Status. Erlaubt: {valid_statuses}"
)
if DB_ENABLED:
try:
from classroom_engine.repository import TeacherFeedbackRepository
with SessionLocal() as db:
repo = TeacherFeedbackRepository(db)
db_feedback = repo.update_status(
feedback_id=feedback_id,
status=status,
response=response,
responded_by=responded_by
)
if db_feedback:
return repo.to_dict(db_feedback)
except Exception as e:
logger.error(f"Failed to update feedback status: {e}")
# Fallback: In-Memory
for fb in _feedback_store:
if fb["id"] == feedback_id:
fb["status"] = status
if response:
fb["response"] = response
fb["responded_by"] = responded_by
fb["responded_at"] = datetime.utcnow().isoformat()
fb["updated_at"] = datetime.utcnow().isoformat()
return fb
raise HTTPException(status_code=404, detail="Feedback nicht gefunden")