""" Classroom API - Materials Endpoints. Endpoints fuer Unterrichtsmaterialien (Feature f19). """ from uuid import uuid4 from typing import Dict, List, Optional from datetime import datetime import logging from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel, Field from classroom_engine import PhaseMaterial, MaterialType from .shared import init_db_if_needed, DB_ENABLED, logger try: from classroom_engine.database import SessionLocal from classroom_engine.repository import MaterialRepository except ImportError: pass router = APIRouter(tags=["Materials"]) # In-Memory Storage (Fallback) _materials: Dict[str, PhaseMaterial] = {} # === Pydantic Models === class CreateMaterialRequest(BaseModel): """Request zum Erstellen eines Materials.""" teacher_id: str title: str = Field(..., max_length=300) material_type: str = Field("document") url: Optional[str] = Field(None, max_length=2000) description: str = "" phase: Optional[str] = None subject: str = "" grade_level: str = "" tags: List[str] = [] is_public: bool = False session_id: Optional[str] = None class UpdateMaterialRequest(BaseModel): """Request zum Aktualisieren eines Materials.""" title: Optional[str] = Field(None, max_length=300) material_type: Optional[str] = None url: Optional[str] = Field(None, max_length=2000) description: Optional[str] = None phase: Optional[str] = None subject: Optional[str] = None grade_level: Optional[str] = None tags: Optional[List[str]] = None is_public: Optional[bool] = None class MaterialResponse(BaseModel): """Response fuer ein Material.""" material_id: str teacher_id: str title: str material_type: str url: Optional[str] description: str phase: Optional[str] subject: str grade_level: str tags: List[str] is_public: bool usage_count: int session_id: Optional[str] created_at: Optional[str] updated_at: Optional[str] class MaterialListResponse(BaseModel): """Response fuer Liste von Materialien.""" materials: List[MaterialResponse] total: int # === Helper Functions === def build_material_response(mat: PhaseMaterial) -> MaterialResponse: """Baut eine MaterialResponse aus einem PhaseMaterial-Objekt.""" return MaterialResponse( material_id=mat.material_id, teacher_id=mat.teacher_id, title=mat.title, material_type=mat.material_type.value, url=mat.url, description=mat.description, phase=mat.phase, subject=mat.subject, grade_level=mat.grade_level, tags=mat.tags, is_public=mat.is_public, usage_count=mat.usage_count, session_id=mat.session_id, created_at=mat.created_at.isoformat() if mat.created_at else None, updated_at=mat.updated_at.isoformat() if mat.updated_at else None, ) # === Endpoints === @router.post("/materials", response_model=MaterialResponse, status_code=201) async def create_material(request: CreateMaterialRequest) -> MaterialResponse: """Erstellt ein neues Material (Feature f19).""" init_db_if_needed() try: mat_type = MaterialType(request.material_type) except ValueError: mat_type = MaterialType.DOCUMENT material = PhaseMaterial( material_id=str(uuid4()), teacher_id=request.teacher_id, title=request.title, material_type=mat_type, url=request.url, description=request.description, phase=request.phase, subject=request.subject, grade_level=request.grade_level, tags=request.tags, is_public=request.is_public, usage_count=0, session_id=request.session_id, created_at=datetime.utcnow(), ) if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) repo.create(material) db.close() except Exception as e: logger.warning(f"DB persist failed for material: {e}") _materials[material.material_id] = material return build_material_response(material) @router.get("/materials", response_model=MaterialListResponse) async def list_materials( teacher_id: str = Query(...), phase: Optional[str] = Query(None), subject: Optional[str] = Query(None), include_public: bool = Query(True), limit: int = Query(50, ge=1, le=100) ) -> MaterialListResponse: """Listet Materialien eines Lehrers (Feature f19).""" init_db_if_needed() materials_list = [] if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) if phase: db_materials = repo.get_by_phase(phase, teacher_id, include_public) else: db_materials = repo.get_by_teacher(teacher_id, phase, subject, limit) for db_mat in db_materials: mat = repo.to_dataclass(db_mat) _materials[mat.material_id] = mat materials_list.append(build_material_response(mat)) db.close() return MaterialListResponse(materials=materials_list, total=len(materials_list)) except Exception as e: logger.warning(f"DB read failed for materials: {e}") for mat in _materials.values(): if mat.teacher_id != teacher_id and not (include_public and mat.is_public): continue if phase and mat.phase != phase: continue if subject and mat.subject != subject: continue materials_list.append(build_material_response(mat)) return MaterialListResponse(materials=materials_list[:limit], total=len(materials_list)) @router.get("/materials/by-phase/{phase}", response_model=MaterialListResponse) async def get_materials_by_phase( phase: str, teacher_id: str = Query(...), subject: Optional[str] = Query(None), limit: int = Query(50, ge=1, le=100) ) -> MaterialListResponse: """Holt Materialien fuer eine bestimmte Phase (Feature f19).""" return await list_materials(teacher_id=teacher_id, phase=phase, subject=subject, limit=limit) @router.get("/materials/{material_id}", response_model=MaterialResponse) async def get_material(material_id: str) -> MaterialResponse: """Ruft ein einzelnes Material ab (Feature f19).""" init_db_if_needed() if material_id in _materials: return build_material_response(_materials[material_id]) if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) db_mat = repo.get_by_id(material_id) db.close() if db_mat: mat = repo.to_dataclass(db_mat) _materials[mat.material_id] = mat return build_material_response(mat) except Exception as e: logger.warning(f"DB read failed: {e}") raise HTTPException(status_code=404, detail="Material nicht gefunden") @router.put("/materials/{material_id}", response_model=MaterialResponse) async def update_material(material_id: str, request: UpdateMaterialRequest) -> MaterialResponse: """Aktualisiert ein Material (Feature f19).""" init_db_if_needed() material = _materials.get(material_id) if not material and DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) db_mat = repo.get_by_id(material_id) db.close() if db_mat: material = repo.to_dataclass(db_mat) _materials[material.material_id] = material except Exception as e: logger.warning(f"DB read failed: {e}") if not material: raise HTTPException(status_code=404, detail="Material nicht gefunden") if request.title is not None: material.title = request.title if request.material_type is not None: try: material.material_type = MaterialType(request.material_type) except ValueError: raise HTTPException(status_code=400, detail="Ungueltiger Material-Typ") if request.url is not None: material.url = request.url if request.description is not None: material.description = request.description if request.phase is not None: material.phase = request.phase if request.subject is not None: material.subject = request.subject if request.grade_level is not None: material.grade_level = request.grade_level if request.tags is not None: material.tags = request.tags if request.is_public is not None: material.is_public = request.is_public material.updated_at = datetime.utcnow() if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) repo.update(material) db.close() except Exception as e: logger.warning(f"DB update failed: {e}") _materials[material_id] = material return build_material_response(material) @router.post("/materials/{material_id}/attach/{session_id}") async def attach_material_to_session(material_id: str, session_id: str) -> MaterialResponse: """Verknuepft ein Material mit einer Session (Feature f19).""" init_db_if_needed() material = _materials.get(material_id) if not material and DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) db_mat = repo.get_by_id(material_id) if db_mat: material = repo.to_dataclass(db_mat) db.close() except Exception as e: logger.warning(f"DB read failed: {e}") if not material: raise HTTPException(status_code=404, detail="Material nicht gefunden") material.session_id = session_id material.usage_count += 1 material.updated_at = datetime.utcnow() if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) repo.attach_to_session(material_id, session_id) db.close() except Exception as e: logger.warning(f"DB update failed: {e}") _materials[material_id] = material return build_material_response(material) @router.delete("/materials/{material_id}") async def delete_material(material_id: str) -> Dict[str, str]: """Loescht ein Material (Feature f19).""" init_db_if_needed() if material_id in _materials: del _materials[material_id] if DB_ENABLED: try: db = SessionLocal() repo = MaterialRepository(db) repo.delete(material_id) db.close() except Exception as e: logger.warning(f"DB delete failed: {e}") return {"status": "deleted", "material_id": material_id}