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>
281 lines
9.3 KiB
Python
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")
|