""" Classroom API - Materials Routes Phase materials management endpoints (Feature f19). """ from uuid import uuid4 from typing import Dict, Optional from datetime import datetime import logging from fastapi import APIRouter, HTTPException, Query from classroom_engine import ( PhaseMaterial, MaterialType, ) from ..models import ( CreateMaterialRequest, UpdateMaterialRequest, MaterialResponse, MaterialListResponse, ) from ..services.persistence import ( init_db_if_needed, DB_ENABLED, SessionLocal, ) logger = logging.getLogger(__name__) router = APIRouter(tags=["Materials"]) # In-Memory Storage fuer Materialien (Fallback) _materials: Dict[str, PhaseMaterial] = {} 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, ) @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(), ) # Persistieren wenn DB verfuegbar if DB_ENABLED: try: from classroom_engine.repository import MaterialRepository 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(..., description="ID des Lehrers"), phase: Optional[str] = Query(None, description="Filter nach Phase"), subject: Optional[str] = Query(None, description="Filter nach Fach"), include_public: bool = Query(True, description="Oeffentliche Materialien einbeziehen"), limit: int = Query(50, ge=1, le=100) ) -> MaterialListResponse: """ Listet Materialien eines Lehrers (Feature f19). """ init_db_if_needed() materials_list = [] # Aus DB laden wenn verfuegbar if DB_ENABLED: try: from classroom_engine.repository import MaterialRepository 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}") # Fallback auf Memory 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(..., description="ID des Lehrers"), subject: Optional[str] = Query(None, description="Filter nach Fach"), 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() # Aus Memory if material_id in _materials: return _build_material_response(_materials[material_id]) # Aus DB laden if DB_ENABLED: try: from classroom_engine.repository import MaterialRepository 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() # Aus Memory holen material = _materials.get(material_id) # Aus DB laden wenn nicht in Memory if not material and DB_ENABLED: try: from classroom_engine.repository import MaterialRepository 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") # Aktualisieren 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() # In DB aktualisieren if DB_ENABLED: try: from classroom_engine.repository import MaterialRepository 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: from classroom_engine.repository import MaterialRepository 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: from classroom_engine.repository import MaterialRepository 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: from classroom_engine.repository import MaterialRepository 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}