""" 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 []