This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

344 lines
11 KiB
Python

"""
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}