fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
280
backend/classroom/routes/feedback.py
Normal file
280
backend/classroom/routes/feedback.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
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")
|
||||
Reference in New Issue
Block a user