fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
343
backend/api/classroom/materials.py
Normal file
343
backend/api/classroom/materials.py
Normal file
@@ -0,0 +1,343 @@
|
||||
"""
|
||||
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}
|
||||
Reference in New Issue
Block a user