Files
breakpilot-lehrer/backend-lehrer/teacher_dashboard_analytics.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

268 lines
12 KiB
Python

# ==============================================
# Teacher Dashboard - Analytics & Progress Routes
# ==============================================
from fastapi import APIRouter, HTTPException, Query, Depends, Request
from typing import List, Optional, Dict, Any
from datetime import datetime, timedelta
import logging
from teacher_dashboard_models import (
UnitAssignmentStatus, TeacherControlSettings,
UnitAssignment, StudentUnitProgress, ClassUnitProgress,
MisconceptionReport, ClassAnalyticsSummary, ContentResource,
get_current_teacher, get_teacher_database,
get_classes_for_teacher, get_students_in_class,
REQUIRE_AUTH,
)
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Teacher Dashboard"])
# Shared in-memory store reference (set from teacher_dashboard_api)
_assignments_store: Dict[str, Dict[str, Any]] = {}
def set_assignments_store(store: Dict[str, Dict[str, Any]]):
"""Share the in-memory assignments store from the main module."""
global _assignments_store
_assignments_store = store
# ==============================================
# API Endpoints - Progress & Analytics
# ==============================================
@router.get("/assignments/{assignment_id}/progress", response_model=ClassUnitProgress)
async def get_assignment_progress(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> ClassUnitProgress:
"""Get detailed progress for an assignment."""
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")
students = await get_students_in_class(assignment["class_id"])
student_progress = []
total_completion = 0.0
total_precheck = 0.0
total_postcheck = 0.0
total_time = 0
precheck_count = 0
postcheck_count = 0
started = 0
completed = 0
for student in students:
student_id = student.get("id", student.get("student_id"))
progress = StudentUnitProgress(
student_id=student_id,
student_name=student.get("name", f"Student {student_id[:8]}"),
status="not_started", completion_rate=0.0, stops_completed=0, total_stops=0,
)
if db:
try:
session_data = await db.get_student_unit_session(
student_id=student_id, unit_id=assignment["unit_id"]
)
if session_data:
progress.session_id = session_data.get("session_id")
progress.status = "completed" if session_data.get("completed_at") else "in_progress"
progress.completion_rate = session_data.get("completion_rate", 0.0)
progress.precheck_score = session_data.get("precheck_score")
progress.postcheck_score = session_data.get("postcheck_score")
progress.time_spent_minutes = session_data.get("duration_seconds", 0) // 60
progress.last_activity = session_data.get("updated_at")
progress.stops_completed = session_data.get("stops_completed", 0)
progress.total_stops = session_data.get("total_stops", 0)
if progress.precheck_score is not None and progress.postcheck_score is not None:
progress.learning_gain = progress.postcheck_score - progress.precheck_score
total_completion += progress.completion_rate
total_time += progress.time_spent_minutes
if progress.precheck_score is not None:
total_precheck += progress.precheck_score
precheck_count += 1
if progress.postcheck_score is not None:
total_postcheck += progress.postcheck_score
postcheck_count += 1
if progress.status != "not_started":
started += 1
if progress.status == "completed":
completed += 1
except Exception as e:
logger.error(f"Failed to get student progress: {e}")
student_progress.append(progress)
total_students = len(students) or 1
return ClassUnitProgress(
assignment_id=assignment_id, unit_id=assignment["unit_id"],
unit_title=f"Unit {assignment['unit_id']}", class_id=assignment["class_id"],
class_name=f"Class {assignment['class_id'][:8]}", total_students=len(students),
started_count=started, completed_count=completed,
avg_completion_rate=total_completion / total_students,
avg_precheck_score=total_precheck / precheck_count if precheck_count > 0 else None,
avg_postcheck_score=total_postcheck / postcheck_count if postcheck_count > 0 else None,
avg_learning_gain=(total_postcheck / postcheck_count - total_precheck / precheck_count)
if precheck_count > 0 and postcheck_count > 0 else None,
avg_time_minutes=total_time / started if started > 0 else 0,
students=student_progress,
)
@router.get("/classes/{class_id}/analytics", response_model=ClassAnalyticsSummary)
async def get_class_analytics(
class_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> ClassAnalyticsSummary:
"""Get summary analytics for a class."""
db = await get_teacher_database()
assignments = []
if db:
try:
assignments = await db.list_assignments(teacher_id=teacher["user_id"], class_id=class_id)
except Exception as e:
logger.error(f"Failed to list assignments: {e}")
if not assignments:
assignments = [
a for a in _assignments_store.values()
if a["class_id"] == class_id and a["teacher_id"] == teacher["user_id"]
]
total_units = len(assignments)
completed_units = sum(1 for a in assignments if a.get("status") == "completed")
active_units = sum(1 for a in assignments if a.get("status") == "active")
students = await get_students_in_class(class_id)
student_scores = {}
misconceptions = []
if db:
try:
for student in students:
student_id = student.get("id", student.get("student_id"))
analytics = await db.get_student_analytics(student_id)
if analytics:
student_scores[student_id] = {
"name": student.get("name", student_id[:8]),
"avg_score": analytics.get("avg_postcheck_score", 0),
"total_time": analytics.get("total_time_minutes", 0),
}
misconceptions_data = await db.get_class_misconceptions(class_id)
for m in misconceptions_data:
misconceptions.append(MisconceptionReport(
concept_id=m["concept_id"], concept_label=m["concept_label"],
misconception=m["misconception"], affected_students=m["affected_students"],
frequency=m["frequency"], unit_id=m["unit_id"], stop_id=m["stop_id"],
))
except Exception as e:
logger.error(f"Failed to aggregate analytics: {e}")
sorted_students = sorted(student_scores.items(), key=lambda x: x[1]["avg_score"], reverse=True)
top_performers = [s[1]["name"] for s in sorted_students[:3]]
struggling_students = [s[1]["name"] for s in sorted_students[-3:] if s[1]["avg_score"] < 0.6]
total_time = sum(s["total_time"] for s in student_scores.values())
avg_scores = [s["avg_score"] for s in student_scores.values() if s["avg_score"] > 0]
avg_completion = sum(avg_scores) / len(avg_scores) if avg_scores else 0
return ClassAnalyticsSummary(
class_id=class_id, class_name=f"Klasse {class_id[:8]}",
total_units_assigned=total_units, units_completed=completed_units,
active_units=active_units, avg_completion_rate=avg_completion,
avg_learning_gain=None, total_time_hours=total_time / 60,
top_performers=top_performers, struggling_students=struggling_students,
common_misconceptions=misconceptions[:5],
)
@router.get("/students/{student_id}/progress")
async def get_student_progress(
student_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""Get detailed progress for a specific student."""
db = await get_teacher_database()
if db:
try:
progress = await db.get_student_full_progress(student_id)
return progress
except Exception as e:
logger.error(f"Failed to get student progress: {e}")
return {
"student_id": student_id, "units_attempted": 0, "units_completed": 0,
"avg_score": 0.0, "total_time_minutes": 0, "sessions": [],
}
# ==============================================
# API Endpoints - Content Resources
# ==============================================
@router.get("/assignments/{assignment_id}/resources", response_model=List[ContentResource])
async def get_assignment_resources(
assignment_id: str,
teacher: Dict[str, Any] = Depends(get_current_teacher),
request: Request = None
) -> List[ContentResource]:
"""Get generated content resources for an assignment."""
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")
unit_id = assignment["unit_id"]
base_url = str(request.base_url).rstrip("/") if request else "http://localhost:8000"
return [
ContentResource(resource_type="h5p", title=f"{unit_id} - H5P Aktivitaeten",
url=f"{base_url}/api/units/content/{unit_id}/h5p",
generated_at=datetime.utcnow(), unit_id=unit_id),
ContentResource(resource_type="worksheet", title=f"{unit_id} - Arbeitsblatt (HTML)",
url=f"{base_url}/api/units/content/{unit_id}/worksheet",
generated_at=datetime.utcnow(), unit_id=unit_id),
ContentResource(resource_type="pdf", title=f"{unit_id} - Arbeitsblatt (PDF)",
url=f"{base_url}/api/units/content/{unit_id}/worksheet.pdf",
generated_at=datetime.utcnow(), unit_id=unit_id),
]
@router.post("/assignments/{assignment_id}/regenerate-content")
async def regenerate_content(
assignment_id: str,
resource_type: str = Query("all", description="h5p, pdf, or all"),
teacher: Dict[str, Any] = Depends(get_current_teacher)
) -> Dict[str, Any]:
"""Trigger regeneration of content resources."""
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")
logger.info(f"Content regeneration triggered for {assignment['unit_id']}: {resource_type}")
return {
"status": "queued", "assignment_id": assignment_id,
"unit_id": assignment["unit_id"], "resource_type": resource_type,
"message": "Content regeneration has been queued",
}