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 Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +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)