Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
330 lines
10 KiB
Python
330 lines
10 KiB
Python
"""
|
|
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}
|