This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/api/classroom/feedback.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

272 lines
8.2 KiB
Python

"""
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)