Files
breakpilot-lehrer/backend-lehrer/classroom/routes/materials.py
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
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>
2026-02-11 23:47:26 +01:00

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}