Files
breakpilot-lehrer/backend-lehrer/teacher_dashboard_api.py
Benjamin Admin b6983ab1dc [split-required] Split 500-1000 LOC files across all services
backend-lehrer (5 files):
- alerts_agent/db/repository.py (992 → 5), abitur_docs_api.py (956 → 3)
- teacher_dashboard_api.py (951 → 3), services/pdf_service.py (916 → 3)
- mail/mail_db.py (987 → 6)

klausur-service (5 files):
- legal_templates_ingestion.py (942 → 3), ocr_pipeline_postprocess.py (929 → 4)
- ocr_pipeline_words.py (876 → 3), ocr_pipeline_ocr_merge.py (616 → 2)
- KorrekturPage.tsx (956 → 6)

website (5 pages):
- mail (985 → 9), edu-search (958 → 8), mac-mini (950 → 7)
- ocr-labeling (946 → 7), audit-workspace (871 → 4)

studio-v2 (5 files + 1 deleted):
- page.tsx (946 → 5), MessagesContext.tsx (925 → 4)
- korrektur (914 → 6), worksheet-cleanup (899 → 6)
- useVocabWorksheet.ts (888 → 3)
- Deleted dead page-original.tsx (934 LOC)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 23:35:37 +02:00

330 lines
13 KiB
Python

# ==============================================
# Breakpilot Drive - Teacher Dashboard API
# ==============================================
# Lehrer-Dashboard fuer Unit-Zuweisung und Analytics.
#
# Split structure:
# - teacher_dashboard_models.py: Models, Auth, DB/School helpers
# - teacher_dashboard_analytics.py: Progress, analytics, content routes
# - teacher_dashboard_api.py: Assignment CRUD, dashboard, units (this file)
from fastapi import APIRouter, HTTPException, Query, Depends
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
import uuid
import logging
from teacher_dashboard_models import (
UnitAssignmentStatus, TeacherControlSettings, AssignUnitRequest,
UnitAssignment,
get_current_teacher, get_teacher_database,
get_classes_for_teacher,
REQUIRE_AUTH,
)
from teacher_dashboard_analytics import (
router as analytics_router,
set_assignments_store,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/teacher", tags=["Teacher Dashboard"])
# In-Memory Storage (Fallback)
_assignments_store: Dict[str, Dict[str, Any]] = {}
# Share the store with the analytics module and include its routes
set_assignments_store(_assignments_store)
router.include_router(analytics_router)
# ==============================================
# API Endpoints - Unit Assignment
# ==============================================
@router.post("/assignments", response_model=UnitAssignment)
async def assign_unit_to_class(
request_data: AssignUnitRequest,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""Assign a unit to a class."""
assignment_id = str(uuid.uuid4())
now = datetime.utcnow()
settings = request_data.settings or TeacherControlSettings()
assignment = {
"assignment_id": assignment_id, "unit_id": request_data.unit_id,
"class_id": request_data.class_id, "teacher_id": teacher["user_id"],
"status": UnitAssignmentStatus.ACTIVE, "settings": settings.model_dump(),
"due_date": request_data.due_date, "notes": request_data.notes,
"created_at": now, "updated_at": now,
}
db = await get_teacher_database()
if db:
try:
await db.create_assignment(assignment)
except Exception as e:
logger.error(f"Failed to store assignment: {e}")
_assignments_store[assignment_id] = assignment
logger.info(f"Unit {request_data.unit_id} assigned to class {request_data.class_id}")
return UnitAssignment(
assignment_id=assignment_id, unit_id=request_data.unit_id,
class_id=request_data.class_id, teacher_id=teacher["user_id"],
status=UnitAssignmentStatus.ACTIVE, settings=settings,
due_date=request_data.due_date, notes=request_data.notes,
created_at=now, updated_at=now,
)
@router.get("/assignments", response_model=List[UnitAssignment])
async def list_assignments(
class_id: Optional[str] = Query(None, description="Filter by class"),
status: Optional[UnitAssignmentStatus] = Query(None, description="Filter by status"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> List[UnitAssignment]:
"""List all unit assignments for the teacher."""
db = await get_teacher_database()
assignments = []
if db:
try:
assignments = await db.list_assignments(
teacher_id=teacher["user_id"],
class_id=class_id,
status=status.value if status else None
)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not assignments:
for assignment in _assignments_store.values():
if assignment["teacher_id"] != teacher["user_id"]:
continue
if class_id and assignment["class_id"] != class_id:
continue
if status and assignment["status"] != status.value:
continue
assignments.append(assignment)
return [
UnitAssignment(
assignment_id=a["assignment_id"], unit_id=a["unit_id"],
class_id=a["class_id"], teacher_id=a["teacher_id"],
status=a["status"], settings=TeacherControlSettings(**a["settings"]),
due_date=a.get("due_date"), notes=a.get("notes"),
created_at=a["created_at"], updated_at=a["updated_at"],
)
for a in assignments
]
@router.get("/assignments/{assignment_id}", response_model=UnitAssignment)
async def get_assignment(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""Get details of a specific assignment."""
db = await get_teacher_database()
if db:
try:
assignment = await db.get_assignment(assignment_id)
if assignment and assignment["teacher_id"] == teacher["user_id"]:
return UnitAssignment(
assignment_id=assignment["assignment_id"], unit_id=assignment["unit_id"],
class_id=assignment["class_id"], teacher_id=assignment["teacher_id"],
status=assignment["status"],
settings=TeacherControlSettings(**assignment["settings"]),
due_date=assignment.get("due_date"), notes=assignment.get("notes"),
created_at=assignment["created_at"], updated_at=assignment["updated_at"],
)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if assignment_id in _assignments_store:
a = _assignments_store[assignment_id]
if a["teacher_id"] == teacher["user_id"]:
return UnitAssignment(
assignment_id=a["assignment_id"], unit_id=a["unit_id"],
class_id=a["class_id"], teacher_id=a["teacher_id"],
status=a["status"], settings=TeacherControlSettings(**a["settings"]),
due_date=a.get("due_date"), notes=a.get("notes"),
created_at=a["created_at"], updated_at=a["updated_at"],
)
raise HTTPException(status_code=404, detail="Assignment not found")
@router.put("/assignments/{assignment_id}")
async def update_assignment(
assignment_id: str,
settings: Optional[TeacherControlSettings] = None,
status: Optional[UnitAssignmentStatus] = None,
due_date: Optional[datetime] = None,
notes: Optional[str] = None,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> UnitAssignment:
"""Update assignment settings or status."""
db = await get_teacher_database()
assignment = None
if db:
try:
assignment = await db.get_assignment(assignment_id)
except Exception as e:
logger.error(f"Failed to get assignment: {e}")
if not assignment and assignment_id in _assignments_store:
assignment = _assignments_store[assignment_id]
if not assignment or assignment["teacher_id"] != teacher["user_id"]:
raise HTTPException(status_code=404, detail="Assignment not found")
if settings:
assignment["settings"] = settings.model_dump()
if status:
assignment["status"] = status.value
if due_date:
assignment["due_date"] = due_date
if notes is not None:
assignment["notes"] = notes
assignment["updated_at"] = datetime.utcnow()
if db:
try:
await db.update_assignment(assignment)
except Exception as e:
logger.error(f"Failed to update assignment: {e}")
_assignments_store[assignment_id] = assignment
return UnitAssignment(
assignment_id=assignment["assignment_id"], unit_id=assignment["unit_id"],
class_id=assignment["class_id"], teacher_id=assignment["teacher_id"],
status=assignment["status"], settings=TeacherControlSettings(**assignment["settings"]),
due_date=assignment.get("due_date"), notes=assignment.get("notes"),
created_at=assignment["created_at"], updated_at=assignment["updated_at"],
)
@router.delete("/assignments/{assignment_id}")
async def delete_assignment(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, str]:
"""Delete/archive an assignment."""
db = await get_teacher_database()
if db:
try:
assignment = await db.get_assignment(assignment_id)
if assignment and assignment["teacher_id"] == teacher["user_id"]:
await db.delete_assignment(assignment_id)
if assignment_id in _assignments_store:
del _assignments_store[assignment_id]
return {"status": "deleted", "assignment_id": assignment_id}
except Exception as e:
logger.error(f"Failed to delete assignment: {e}")
if assignment_id in _assignments_store:
a = _assignments_store[assignment_id]
if a["teacher_id"] == teacher["user_id"]:
del _assignments_store[assignment_id]
return {"status": "deleted", "assignment_id": assignment_id}
raise HTTPException(status_code=404, detail="Assignment not found")
# ==============================================
# API Endpoints - Available Units
# ==============================================
@router.get("/units/available")
async def list_available_units(
grade: Optional[str] = Query(None, description="Filter by grade level"),
template: Optional[str] = Query(None, description="Filter by template type"),
locale: str = Query("de-DE", description="Locale"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> List[Dict[str, Any]]:
"""List all available units for assignment."""
db = await get_teacher_database()
if db:
try:
units = await db.list_available_units(grade=grade, template=template, locale=locale)
return units
except Exception as e:
logger.error(f"Failed to list units: {e}")
return [
{
"unit_id": "bio_eye_lightpath_v1", "title": "Auge - Lichtstrahl-Flug",
"template": "flight_path", "grade_band": ["5", "6", "7"],
"duration_minutes": 8, "difficulty": "base",
"description": "Reise durch das Auge und folge dem Lichtstrahl",
"learning_objectives": ["Verstehen des Lichtwegs durch das Auge",
"Funktionen der Augenbestandteile benennen"],
},
{
"unit_id": "math_pizza_equivalence_v1",
"title": "Pizza-Boxenstopp - Brueche und Prozent",
"template": "station_loop", "grade_band": ["5", "6"],
"duration_minutes": 10, "difficulty": "base",
"description": "Entdecke die Verbindung zwischen Bruechen, Dezimalzahlen und Prozent",
"learning_objectives": ["Brueche in Prozent umrechnen", "Aequivalenzen erkennen"],
},
]
# ==============================================
# API Endpoints - Dashboard Overview
# ==============================================
@router.get("/dashboard")
async def get_dashboard(
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""Get teacher dashboard overview."""
db = await get_teacher_database()
classes = await get_classes_for_teacher(teacher["user_id"])
active_assignments = []
if db:
try:
active_assignments = await db.list_assignments(
teacher_id=teacher["user_id"], status="active"
)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not active_assignments:
active_assignments = [
a for a in _assignments_store.values()
if a["teacher_id"] == teacher["user_id"] and a.get("status") == "active"
]
alerts = []
for assignment in active_assignments:
if assignment.get("due_date") and assignment["due_date"] < datetime.utcnow() + timedelta(days=2):
alerts.append({
"type": "due_soon", "assignment_id": assignment["assignment_id"],
"message": "Zuweisung endet in weniger als 2 Tagen",
})
return {
"teacher": {"id": teacher["user_id"], "name": teacher.get("name", "Lehrer"),
"email": teacher.get("email")},
"classes": len(classes), "active_assignments": len(active_assignments),
"total_students": sum(c.get("student_count", 0) for c in classes),
"alerts": alerts, "recent_activity": [],
}
@router.get("/health")
async def health_check() -> Dict[str, Any]:
"""Health check for teacher dashboard API."""
db = await get_teacher_database()
db_status = "connected" if db else "in-memory"
return {
"status": "healthy", "service": "teacher-dashboard",
"database": db_status, "auth_required": REQUIRE_AUTH,
}