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

227 lines
6.7 KiB
Python

"""
Teacher Dashboard - Pydantic Models, Auth Dependency, and Service Helpers.
"""
import os
import logging
from datetime import datetime
from typing import List, Optional, Dict, Any
from enum import Enum
from fastapi import HTTPException, Request
from pydantic import BaseModel
import httpx
logger = logging.getLogger(__name__)
# Feature flags
USE_DATABASE = os.getenv("GAME_USE_DATABASE", "true").lower() == "true"
REQUIRE_AUTH = os.getenv("TEACHER_REQUIRE_AUTH", "true").lower() == "true"
SCHOOL_SERVICE_URL = os.getenv("SCHOOL_SERVICE_URL", "http://school-service:8084")
# ==============================================
# Pydantic Models
# ==============================================
class UnitAssignmentStatus(str, Enum):
"""Status of a unit assignment"""
DRAFT = "draft"
ACTIVE = "active"
COMPLETED = "completed"
ARCHIVED = "archived"
class TeacherControlSettings(BaseModel):
"""Unit settings that teachers can configure"""
allow_skip: bool = True
allow_replay: bool = True
max_time_per_stop_sec: int = 90
show_hints: bool = True
require_precheck: bool = True
require_postcheck: bool = True
class AssignUnitRequest(BaseModel):
"""Request to assign a unit to a class"""
unit_id: str
class_id: str
due_date: Optional[datetime] = None
settings: Optional[TeacherControlSettings] = None
notes: Optional[str] = None
class UnitAssignment(BaseModel):
"""Unit assignment record"""
assignment_id: str
unit_id: str
class_id: str
teacher_id: str
status: UnitAssignmentStatus
settings: TeacherControlSettings
due_date: Optional[datetime] = None
notes: Optional[str] = None
created_at: datetime
updated_at: datetime
class StudentUnitProgress(BaseModel):
"""Progress of a single student on a unit"""
student_id: str
student_name: str
session_id: Optional[str] = None
status: str # "not_started", "in_progress", "completed"
completion_rate: float = 0.0
precheck_score: Optional[float] = None
postcheck_score: Optional[float] = None
learning_gain: Optional[float] = None
time_spent_minutes: int = 0
last_activity: Optional[datetime] = None
current_stop: Optional[str] = None
stops_completed: int = 0
total_stops: int = 0
class ClassUnitProgress(BaseModel):
"""Overall progress of a class on a unit"""
assignment_id: str
unit_id: str
unit_title: str
class_id: str
class_name: str
total_students: int
started_count: int
completed_count: int
avg_completion_rate: float
avg_precheck_score: Optional[float] = None
avg_postcheck_score: Optional[float] = None
avg_learning_gain: Optional[float] = None
avg_time_minutes: float
students: List[StudentUnitProgress]
class MisconceptionReport(BaseModel):
"""Report of detected misconceptions"""
concept_id: str
concept_label: str
misconception: str
affected_students: List[str]
frequency: int
unit_id: str
stop_id: str
class ClassAnalyticsSummary(BaseModel):
"""Summary analytics for a class"""
class_id: str
class_name: str
total_units_assigned: int
units_completed: int
active_units: int
avg_completion_rate: float
avg_learning_gain: Optional[float]
total_time_hours: float
top_performers: List[str]
struggling_students: List[str]
common_misconceptions: List[MisconceptionReport]
class ContentResource(BaseModel):
"""Generated content resource"""
resource_type: str # "h5p", "pdf", "worksheet"
title: str
url: str
generated_at: datetime
unit_id: str
# ==============================================
# Auth Dependency
# ==============================================
async def get_current_teacher(request: Request) -> Dict[str, Any]:
"""Get current teacher from JWT token."""
if not REQUIRE_AUTH:
return {
"user_id": "e9484ad9-32ee-4f2b-a4e1-d182e02ccf20",
"email": "demo@breakpilot.app",
"role": "teacher",
"name": "Demo Lehrer"
}
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing authorization token")
try:
import jwt
token = auth_header[7:]
secret = os.getenv("JWT_SECRET", "dev-secret-key")
payload = jwt.decode(token, secret, algorithms=["HS256"])
if payload.get("role") not in ["teacher", "admin"]:
raise HTTPException(status_code=403, detail="Teacher or admin role required")
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# ==============================================
# Database Integration
# ==============================================
_teacher_db = None
async def get_teacher_database():
"""Get teacher database instance with lazy initialization."""
global _teacher_db
if not USE_DATABASE:
return None
if _teacher_db is None:
try:
from unit.database import get_teacher_db
_teacher_db = await get_teacher_db()
logger.info("Teacher database initialized")
except ImportError:
logger.warning("Teacher database module not available")
except Exception as e:
logger.warning(f"Teacher database not available: {e}")
return _teacher_db
# ==============================================
# School Service Integration
# ==============================================
async def get_classes_for_teacher(teacher_id: str) -> List[Dict[str, Any]]:
"""Get classes assigned to a teacher from school service."""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes",
headers={"X-Teacher-ID": teacher_id}
)
if response.status_code == 200:
return response.json()
except Exception as e:
logger.error(f"Failed to get classes from school service: {e}")
return []
async def get_students_in_class(class_id: str) -> List[Dict[str, Any]]:
"""Get students in a class from school service."""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(
f"{SCHOOL_SERVICE_URL}/api/v1/school/classes/{class_id}/students"
)
if response.status_code == 200:
return response.json()
except Exception as e:
logger.error(f"Failed to get students from school service: {e}")
return []