[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>
This commit is contained in:
226
backend-lehrer/teacher_dashboard_models.py
Normal file
226
backend-lehrer/teacher_dashboard_models.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
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 []
|
||||
Reference in New Issue
Block a user