Restructure: Move 43 files into 8 domain packages (backend-lehrer)
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 40s
CI / test-python-klausur (push) Failing after 2m30s
CI / test-python-agent-core (push) Successful in 28s
CI / test-nodejs-website (push) Successful in 20s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,329 +1,4 @@
|
||||
# ==============================================
|
||||
# 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,
|
||||
}
|
||||
# Backward-compat shim -- module moved to dashboard/api.py
|
||||
import importlib as _importlib
|
||||
import sys as _sys
|
||||
_sys.modules[__name__] = _importlib.import_module("dashboard.api")
|
||||
|
||||
Reference in New Issue
Block a user