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>
393 lines
13 KiB
Python
393 lines
13 KiB
Python
"""
|
|
Classroom API - Template Endpoints.
|
|
|
|
Endpoints fuer Stunden-Vorlagen (Feature f37).
|
|
"""
|
|
|
|
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 (
|
|
LessonSession,
|
|
LessonTemplate,
|
|
SYSTEM_TEMPLATES,
|
|
get_default_durations,
|
|
)
|
|
|
|
from .models import SessionResponse
|
|
from .shared import (
|
|
init_db_if_needed,
|
|
get_sessions,
|
|
persist_session,
|
|
DB_ENABLED,
|
|
logger,
|
|
)
|
|
from .sessions import build_session_response
|
|
|
|
try:
|
|
from classroom_engine.database import SessionLocal
|
|
from classroom_engine.repository import TemplateRepository
|
|
except ImportError:
|
|
pass
|
|
|
|
router = APIRouter(tags=["Templates"])
|
|
|
|
|
|
# === Pydantic Models ===
|
|
|
|
class TemplateCreate(BaseModel):
|
|
"""Request zum Erstellen einer Vorlage."""
|
|
name: str = Field(..., min_length=1, max_length=200)
|
|
description: str = Field("", max_length=1000)
|
|
subject: str = Field("", max_length=100)
|
|
grade_level: str = Field("", max_length=50)
|
|
phase_durations: Dict[str, int] = Field(default_factory=get_default_durations)
|
|
default_topic: str = Field("", max_length=500)
|
|
default_notes: str = Field("")
|
|
is_public: bool = Field(False)
|
|
|
|
|
|
class TemplateUpdate(BaseModel):
|
|
"""Request zum Aktualisieren einer Vorlage."""
|
|
name: Optional[str] = Field(None, min_length=1, max_length=200)
|
|
description: Optional[str] = Field(None, max_length=1000)
|
|
subject: Optional[str] = Field(None, max_length=100)
|
|
grade_level: Optional[str] = Field(None, max_length=50)
|
|
phase_durations: Optional[Dict[str, int]] = None
|
|
default_topic: Optional[str] = Field(None, max_length=500)
|
|
default_notes: Optional[str] = None
|
|
is_public: Optional[bool] = None
|
|
|
|
|
|
class TemplateResponse(BaseModel):
|
|
"""Response fuer eine einzelne Vorlage."""
|
|
template_id: str
|
|
teacher_id: str
|
|
name: str
|
|
description: str
|
|
subject: str
|
|
grade_level: str
|
|
phase_durations: Dict[str, int]
|
|
default_topic: str
|
|
default_notes: str
|
|
is_public: bool
|
|
usage_count: int
|
|
total_duration_minutes: int
|
|
created_at: Optional[str]
|
|
updated_at: Optional[str]
|
|
is_system_template: bool = False
|
|
|
|
|
|
class TemplateListResponse(BaseModel):
|
|
"""Response fuer Template-Liste."""
|
|
templates: List[TemplateResponse]
|
|
total_count: int
|
|
|
|
|
|
# === Helper Functions ===
|
|
|
|
def build_template_response(template: LessonTemplate, is_system: bool = False) -> TemplateResponse:
|
|
"""Baut eine Template-Response."""
|
|
return TemplateResponse(
|
|
template_id=template.template_id,
|
|
teacher_id=template.teacher_id,
|
|
name=template.name,
|
|
description=template.description,
|
|
subject=template.subject,
|
|
grade_level=template.grade_level,
|
|
phase_durations=template.phase_durations,
|
|
default_topic=template.default_topic,
|
|
default_notes=template.default_notes,
|
|
is_public=template.is_public,
|
|
usage_count=template.usage_count,
|
|
total_duration_minutes=sum(template.phase_durations.values()),
|
|
created_at=template.created_at.isoformat() if template.created_at else None,
|
|
updated_at=template.updated_at.isoformat() if template.updated_at else None,
|
|
is_system_template=is_system,
|
|
)
|
|
|
|
|
|
def get_system_templates() -> List[TemplateResponse]:
|
|
"""Gibt die vordefinierten System-Templates zurueck."""
|
|
templates = []
|
|
for t in SYSTEM_TEMPLATES:
|
|
template = LessonTemplate(
|
|
template_id=t["template_id"],
|
|
teacher_id="system",
|
|
name=t["name"],
|
|
description=t.get("description", ""),
|
|
phase_durations=t["phase_durations"],
|
|
is_public=True,
|
|
usage_count=0,
|
|
)
|
|
templates.append(build_template_response(template, is_system=True))
|
|
return templates
|
|
|
|
|
|
# === Endpoints ===
|
|
|
|
@router.get("/templates", response_model=TemplateListResponse)
|
|
async def list_templates(
|
|
teacher_id: Optional[str] = Query(None),
|
|
subject: Optional[str] = Query(None),
|
|
include_system: bool = Query(True)
|
|
) -> TemplateListResponse:
|
|
"""Listet verfuegbare Stunden-Vorlagen (Feature f37)."""
|
|
init_db_if_needed()
|
|
|
|
templates: List[TemplateResponse] = []
|
|
|
|
if include_system:
|
|
templates.extend(get_system_templates())
|
|
|
|
if DB_ENABLED:
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
|
|
if subject:
|
|
db_templates = repo.get_by_subject(subject, teacher_id)
|
|
elif teacher_id:
|
|
db_templates = repo.get_by_teacher(teacher_id, include_public=True)
|
|
else:
|
|
db_templates = repo.get_public_templates()
|
|
|
|
for db_t in db_templates:
|
|
template = repo.to_dataclass(db_t)
|
|
templates.append(build_template_response(template))
|
|
|
|
db.close()
|
|
except Exception as e:
|
|
logger.error(f"Failed to load templates from DB: {e}")
|
|
|
|
return TemplateListResponse(templates=templates, total_count=len(templates))
|
|
|
|
|
|
@router.get("/templates/{template_id}", response_model=TemplateResponse)
|
|
async def get_template(template_id: str) -> TemplateResponse:
|
|
"""Ruft eine einzelne Vorlage ab."""
|
|
init_db_if_needed()
|
|
|
|
for t in SYSTEM_TEMPLATES:
|
|
if t["template_id"] == template_id:
|
|
template = LessonTemplate(
|
|
template_id=t["template_id"],
|
|
teacher_id="system",
|
|
name=t["name"],
|
|
description=t.get("description", ""),
|
|
phase_durations=t["phase_durations"],
|
|
is_public=True,
|
|
)
|
|
return build_template_response(template, is_system=True)
|
|
|
|
if DB_ENABLED:
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
db_template = repo.get_by_id(template_id)
|
|
if db_template:
|
|
template = repo.to_dataclass(db_template)
|
|
db.close()
|
|
return build_template_response(template)
|
|
db.close()
|
|
except Exception as e:
|
|
logger.error(f"Failed to get template {template_id}: {e}")
|
|
|
|
raise HTTPException(status_code=404, detail="Vorlage nicht gefunden")
|
|
|
|
|
|
@router.post("/templates", response_model=TemplateResponse, status_code=201)
|
|
async def create_template(
|
|
request: TemplateCreate,
|
|
teacher_id: str = Query(...)
|
|
) -> TemplateResponse:
|
|
"""Erstellt eine neue Stunden-Vorlage."""
|
|
init_db_if_needed()
|
|
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
template = LessonTemplate(
|
|
template_id=str(uuid4()),
|
|
teacher_id=teacher_id,
|
|
name=request.name,
|
|
description=request.description,
|
|
subject=request.subject,
|
|
grade_level=request.grade_level,
|
|
phase_durations=request.phase_durations,
|
|
default_topic=request.default_topic,
|
|
default_notes=request.default_notes,
|
|
is_public=request.is_public,
|
|
created_at=datetime.utcnow(),
|
|
)
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
db_template = repo.create(template)
|
|
template = repo.to_dataclass(db_template)
|
|
db.close()
|
|
return build_template_response(template)
|
|
except Exception as e:
|
|
logger.error(f"Failed to create template: {e}")
|
|
raise HTTPException(status_code=500, detail="Fehler beim Erstellen der Vorlage")
|
|
|
|
|
|
@router.put("/templates/{template_id}", response_model=TemplateResponse)
|
|
async def update_template(
|
|
template_id: str,
|
|
request: TemplateUpdate,
|
|
teacher_id: str = Query(...)
|
|
) -> TemplateResponse:
|
|
"""Aktualisiert eine Stunden-Vorlage."""
|
|
init_db_if_needed()
|
|
|
|
for t in SYSTEM_TEMPLATES:
|
|
if t["template_id"] == template_id:
|
|
raise HTTPException(status_code=403, detail="System-Vorlagen koennen nicht bearbeitet werden")
|
|
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
|
|
db_template = repo.get_by_id(template_id)
|
|
if not db_template:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail="Vorlage nicht gefunden")
|
|
|
|
if db_template.teacher_id != teacher_id:
|
|
db.close()
|
|
raise HTTPException(status_code=403, detail="Keine Berechtigung")
|
|
|
|
template = repo.to_dataclass(db_template)
|
|
if request.name is not None:
|
|
template.name = request.name
|
|
if request.description is not None:
|
|
template.description = request.description
|
|
if request.subject is not None:
|
|
template.subject = request.subject
|
|
if request.grade_level is not None:
|
|
template.grade_level = request.grade_level
|
|
if request.phase_durations is not None:
|
|
template.phase_durations = request.phase_durations
|
|
if request.default_topic is not None:
|
|
template.default_topic = request.default_topic
|
|
if request.default_notes is not None:
|
|
template.default_notes = request.default_notes
|
|
if request.is_public is not None:
|
|
template.is_public = request.is_public
|
|
|
|
db_template = repo.update(template)
|
|
template = repo.to_dataclass(db_template)
|
|
db.close()
|
|
return build_template_response(template)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to update template {template_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Fehler beim Aktualisieren der Vorlage")
|
|
|
|
|
|
@router.delete("/templates/{template_id}")
|
|
async def delete_template(
|
|
template_id: str,
|
|
teacher_id: str = Query(...)
|
|
) -> Dict[str, str]:
|
|
"""Loescht eine Stunden-Vorlage."""
|
|
init_db_if_needed()
|
|
|
|
for t in SYSTEM_TEMPLATES:
|
|
if t["template_id"] == template_id:
|
|
raise HTTPException(status_code=403, detail="System-Vorlagen koennen nicht geloescht werden")
|
|
|
|
if not DB_ENABLED:
|
|
raise HTTPException(status_code=503, detail="Datenbank nicht verfuegbar")
|
|
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
|
|
db_template = repo.get_by_id(template_id)
|
|
if not db_template:
|
|
db.close()
|
|
raise HTTPException(status_code=404, detail="Vorlage nicht gefunden")
|
|
|
|
if db_template.teacher_id != teacher_id:
|
|
db.close()
|
|
raise HTTPException(status_code=403, detail="Keine Berechtigung")
|
|
|
|
repo.delete(template_id)
|
|
db.close()
|
|
return {"status": "deleted", "template_id": template_id}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to delete template {template_id}: {e}")
|
|
raise HTTPException(status_code=500, detail="Fehler beim Loeschen der Vorlage")
|
|
|
|
|
|
@router.post("/sessions/from-template", response_model=SessionResponse)
|
|
async def create_session_from_template(
|
|
template_id: str = Query(...),
|
|
teacher_id: str = Query(...),
|
|
class_id: str = Query(...),
|
|
topic: Optional[str] = Query(None)
|
|
) -> SessionResponse:
|
|
"""Erstellt eine neue Session basierend auf einer Vorlage."""
|
|
init_db_if_needed()
|
|
|
|
template_data = None
|
|
is_system = False
|
|
|
|
for t in SYSTEM_TEMPLATES:
|
|
if t["template_id"] == template_id:
|
|
template_data = t
|
|
is_system = True
|
|
break
|
|
|
|
if not template_data and DB_ENABLED:
|
|
try:
|
|
db = SessionLocal()
|
|
repo = TemplateRepository(db)
|
|
db_template = repo.get_by_id(template_id)
|
|
if db_template:
|
|
template_data = {
|
|
"phase_durations": db_template.phase_durations or get_default_durations(),
|
|
"subject": db_template.subject or "",
|
|
"default_topic": db_template.default_topic or "",
|
|
"default_notes": db_template.default_notes or "",
|
|
}
|
|
repo.increment_usage(template_id)
|
|
db.close()
|
|
except Exception as e:
|
|
logger.error(f"Failed to load template {template_id}: {e}")
|
|
|
|
if not template_data:
|
|
raise HTTPException(status_code=404, detail="Vorlage nicht gefunden")
|
|
|
|
session = LessonSession(
|
|
session_id=str(uuid4()),
|
|
teacher_id=teacher_id,
|
|
class_id=class_id,
|
|
subject=template_data.get("subject", ""),
|
|
topic=topic or template_data.get("default_topic", ""),
|
|
phase_durations=template_data["phase_durations"],
|
|
notes=template_data.get("default_notes", ""),
|
|
)
|
|
|
|
sessions = get_sessions()
|
|
sessions[session.session_id] = session
|
|
persist_session(session)
|
|
|
|
return build_session_response(session)
|