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:
392
backend/api/classroom/templates.py
Normal file
392
backend/api/classroom/templates.py
Normal file
@@ -0,0 +1,392 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user