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>
330 lines
13 KiB
Python
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,
|
|
}
|