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:
407
backend/classroom_engine/models.py
Normal file
407
backend/classroom_engine/models.py
Normal file
@@ -0,0 +1,407 @@
|
||||
"""
|
||||
Datenmodelle fuer die Classroom State Machine.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class LessonPhase(str, Enum):
|
||||
"""Unterrichtsphasen als Enum."""
|
||||
NOT_STARTED = "not_started"
|
||||
EINSTIEG = "einstieg"
|
||||
ERARBEITUNG = "erarbeitung"
|
||||
SICHERUNG = "sicherung"
|
||||
TRANSFER = "transfer"
|
||||
REFLEXION = "reflexion"
|
||||
ENDED = "ended"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseConfig:
|
||||
"""Konfiguration einer einzelnen Phase."""
|
||||
phase: LessonPhase
|
||||
display_name: str
|
||||
duration_minutes: int
|
||||
activities: List[str]
|
||||
icon: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
# Phasen-Definitionen mit Default-Werten
|
||||
LESSON_PHASES: Dict[str, Dict[str, Any]] = {
|
||||
"einstieg": {
|
||||
"display_name": "Einstieg",
|
||||
"default_duration_minutes": 8,
|
||||
"next_phase": "erarbeitung",
|
||||
"activities": ["warmup", "motivation", "problem_introduction"],
|
||||
"icon": "play_circle",
|
||||
"description": "Motivation und Problemstellung"
|
||||
},
|
||||
"erarbeitung": {
|
||||
"display_name": "Erarbeitung",
|
||||
"default_duration_minutes": 20,
|
||||
"next_phase": "sicherung",
|
||||
"activities": ["instruction", "individual_work", "partner_work", "group_work"],
|
||||
"icon": "edit",
|
||||
"description": "Hauptarbeitsphase mit Input und Uebungen"
|
||||
},
|
||||
"sicherung": {
|
||||
"display_name": "Sicherung",
|
||||
"default_duration_minutes": 10,
|
||||
"next_phase": "transfer",
|
||||
"activities": ["summary", "visualization", "documentation"],
|
||||
"icon": "save",
|
||||
"description": "Ergebnisse festhalten und zusammenfassen"
|
||||
},
|
||||
"transfer": {
|
||||
"display_name": "Transfer",
|
||||
"default_duration_minutes": 7,
|
||||
"next_phase": "reflexion",
|
||||
"activities": ["application", "real_world_connection", "differentiation"],
|
||||
"icon": "compare_arrows",
|
||||
"description": "Anwendung auf neue Kontexte"
|
||||
},
|
||||
"reflexion": {
|
||||
"display_name": "Reflexion",
|
||||
"default_duration_minutes": 5,
|
||||
"next_phase": None,
|
||||
"activities": ["feedback", "homework", "preview"],
|
||||
"icon": "lightbulb",
|
||||
"description": "Rueckblick, Hausaufgaben und Ausblick"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_default_durations() -> Dict[str, int]:
|
||||
"""Gibt die Standard-Phasendauern zurueck."""
|
||||
return {
|
||||
phase_id: config["default_duration_minutes"]
|
||||
for phase_id, config in LESSON_PHASES.items()
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class LessonSession:
|
||||
"""Repräsentiert eine laufende Unterrichtsstunde."""
|
||||
session_id: str
|
||||
teacher_id: str
|
||||
class_id: str
|
||||
subject: str
|
||||
topic: Optional[str] = None
|
||||
|
||||
# State
|
||||
current_phase: LessonPhase = LessonPhase.NOT_STARTED
|
||||
phase_started_at: Optional[datetime] = None
|
||||
lesson_started_at: Optional[datetime] = None
|
||||
lesson_ended_at: Optional[datetime] = None
|
||||
|
||||
# Pause state (Feature f26/f27)
|
||||
is_paused: bool = False
|
||||
pause_started_at: Optional[datetime] = None
|
||||
total_paused_seconds: int = 0 # Cumulative pause time for current phase
|
||||
|
||||
# Phase durations (customizable per session)
|
||||
phase_durations: Dict[str, int] = field(default_factory=get_default_durations)
|
||||
|
||||
# History
|
||||
phase_history: List[Dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
# Metadata
|
||||
notes: str = ""
|
||||
homework: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert die Session in ein Dictionary."""
|
||||
return {
|
||||
"session_id": self.session_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"class_id": self.class_id,
|
||||
"subject": self.subject,
|
||||
"topic": self.topic,
|
||||
"current_phase": self.current_phase.value,
|
||||
"phase_started_at": self.phase_started_at.isoformat() if self.phase_started_at else None,
|
||||
"lesson_started_at": self.lesson_started_at.isoformat() if self.lesson_started_at else None,
|
||||
"lesson_ended_at": self.lesson_ended_at.isoformat() if self.lesson_ended_at else None,
|
||||
"is_paused": self.is_paused,
|
||||
"pause_started_at": self.pause_started_at.isoformat() if self.pause_started_at else None,
|
||||
"total_paused_seconds": self.total_paused_seconds,
|
||||
"phase_durations": self.phase_durations,
|
||||
"phase_history": self.phase_history,
|
||||
"notes": self.notes,
|
||||
"homework": self.homework,
|
||||
}
|
||||
|
||||
def get_phase_display_name(self) -> str:
|
||||
"""Gibt den Anzeigenamen der aktuellen Phase zurueck."""
|
||||
if self.current_phase == LessonPhase.NOT_STARTED:
|
||||
return "Nicht gestartet"
|
||||
elif self.current_phase == LessonPhase.ENDED:
|
||||
return "Beendet"
|
||||
else:
|
||||
return LESSON_PHASES.get(self.current_phase.value, {}).get("display_name", "Unbekannt")
|
||||
|
||||
def get_total_duration_minutes(self) -> int:
|
||||
"""Gibt die Gesamtdauer aller Phasen in Minuten zurueck."""
|
||||
return sum(self.phase_durations.values())
|
||||
|
||||
|
||||
@dataclass
|
||||
class LessonTemplate:
|
||||
"""
|
||||
Vorlage fuer Unterrichtsstunden (Feature f37).
|
||||
|
||||
Ermoeglicht Lehrern, haeufig genutzte Stundenkonfigurationen zu speichern.
|
||||
"""
|
||||
template_id: str
|
||||
teacher_id: str # Ersteller der Vorlage
|
||||
name: str
|
||||
description: str = ""
|
||||
subject: str = ""
|
||||
grade_level: str = "" # z.B. "7", "10", "Oberstufe"
|
||||
|
||||
# Phasenkonfiguration
|
||||
phase_durations: Dict[str, int] = field(default_factory=get_default_durations)
|
||||
|
||||
# Optionale Vorbelegungen
|
||||
default_topic: str = ""
|
||||
default_notes: str = ""
|
||||
|
||||
# Metadaten
|
||||
is_public: bool = False # Oeffentlich fuer alle Lehrer?
|
||||
usage_count: int = 0
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert das Template in ein Dictionary."""
|
||||
return {
|
||||
"template_id": self.template_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"subject": self.subject,
|
||||
"grade_level": self.grade_level,
|
||||
"phase_durations": self.phase_durations,
|
||||
"default_topic": self.default_topic,
|
||||
"default_notes": self.default_notes,
|
||||
"is_public": self.is_public,
|
||||
"usage_count": self.usage_count,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
"total_duration_minutes": sum(self.phase_durations.values()),
|
||||
}
|
||||
|
||||
|
||||
# Vordefinierte System-Templates
|
||||
SYSTEM_TEMPLATES: List[Dict[str, Any]] = [
|
||||
{
|
||||
"template_id": "system_standard_45",
|
||||
"name": "Standard 45 Min",
|
||||
"description": "Klassische Unterrichtsstunde mit 45 Minuten",
|
||||
"phase_durations": {
|
||||
"einstieg": 5,
|
||||
"erarbeitung": 20,
|
||||
"sicherung": 10,
|
||||
"transfer": 5,
|
||||
"reflexion": 5,
|
||||
},
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"template_id": "system_standard_90",
|
||||
"name": "Doppelstunde 90 Min",
|
||||
"description": "Ausfuehrliche Doppelstunde mit mehr Zeit fuer Erarbeitung",
|
||||
"phase_durations": {
|
||||
"einstieg": 10,
|
||||
"erarbeitung": 40,
|
||||
"sicherung": 20,
|
||||
"transfer": 10,
|
||||
"reflexion": 10,
|
||||
},
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"template_id": "system_workshop",
|
||||
"name": "Workshop-Stil",
|
||||
"description": "Praxisorientiert mit langer Erarbeitungsphase",
|
||||
"phase_durations": {
|
||||
"einstieg": 5,
|
||||
"erarbeitung": 30,
|
||||
"sicherung": 5,
|
||||
"transfer": 5,
|
||||
"reflexion": 5,
|
||||
},
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"template_id": "system_discussion",
|
||||
"name": "Diskussion & Reflexion",
|
||||
"description": "Fuer Themen mit viel Diskussionsbedarf",
|
||||
"phase_durations": {
|
||||
"einstieg": 8,
|
||||
"erarbeitung": 15,
|
||||
"sicherung": 7,
|
||||
"transfer": 10,
|
||||
"reflexion": 10,
|
||||
},
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"template_id": "system_test_prep",
|
||||
"name": "Pruefungsvorbereitung",
|
||||
"description": "Kompakte Wiederholung vor Tests",
|
||||
"phase_durations": {
|
||||
"einstieg": 3,
|
||||
"erarbeitung": 25,
|
||||
"sicherung": 12,
|
||||
"transfer": 3,
|
||||
"reflexion": 2,
|
||||
},
|
||||
"is_public": True,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseSuggestion:
|
||||
"""Ein Vorschlag fuer eine Aktivitaet in einer Phase (Feature f18)."""
|
||||
id: str
|
||||
title: str
|
||||
description: str
|
||||
activity_type: str # warmup, instruction, exercise, etc.
|
||||
estimated_minutes: int
|
||||
icon: str
|
||||
content_url: Optional[str] = None # Link to learning unit
|
||||
subjects: Optional[List[str]] = None # Faecher fuer die der Vorschlag passt (None = alle)
|
||||
grade_levels: Optional[List[str]] = None # Klassenstufen (None = alle)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert den Vorschlag in ein Dictionary."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
"activity_type": self.activity_type,
|
||||
"estimated_minutes": self.estimated_minutes,
|
||||
"icon": self.icon,
|
||||
"content_url": self.content_url,
|
||||
"subjects": self.subjects,
|
||||
"grade_levels": self.grade_levels,
|
||||
}
|
||||
|
||||
|
||||
# ==================== Homework Tracker (Feature f20) ====================
|
||||
|
||||
class HomeworkStatus(Enum):
|
||||
"""Status einer Hausaufgabe."""
|
||||
ASSIGNED = "assigned" # Aufgegeben
|
||||
IN_PROGRESS = "in_progress" # In Bearbeitung
|
||||
COMPLETED = "completed" # Erledigt
|
||||
OVERDUE = "overdue" # Ueberfaellig
|
||||
|
||||
|
||||
@dataclass
|
||||
class Homework:
|
||||
"""
|
||||
Eine Hausaufgabe (Feature f20).
|
||||
|
||||
Ermoeglicht das Tracking von Hausaufgaben ueber Sessions hinweg.
|
||||
"""
|
||||
homework_id: str
|
||||
teacher_id: str
|
||||
class_id: str
|
||||
subject: str
|
||||
title: str
|
||||
description: str = ""
|
||||
session_id: Optional[str] = None # Verknuepfte Session
|
||||
due_date: Optional[datetime] = None
|
||||
status: HomeworkStatus = HomeworkStatus.ASSIGNED
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert die Hausaufgabe in ein Dictionary."""
|
||||
return {
|
||||
"homework_id": self.homework_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"class_id": self.class_id,
|
||||
"subject": self.subject,
|
||||
"title": self.title,
|
||||
"description": self.description,
|
||||
"session_id": self.session_id,
|
||||
"due_date": self.due_date.isoformat() if self.due_date else None,
|
||||
"status": self.status.value,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
"is_overdue": self.is_overdue,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_overdue(self) -> bool:
|
||||
"""Prueft ob die Hausaufgabe ueberfaellig ist."""
|
||||
if not self.due_date:
|
||||
return False
|
||||
if self.status == HomeworkStatus.COMPLETED:
|
||||
return False
|
||||
return datetime.now() > self.due_date
|
||||
|
||||
|
||||
# ==================== Phase Materials (Feature f19) ====================
|
||||
|
||||
class MaterialType(Enum):
|
||||
"""Typ des Materials."""
|
||||
DOCUMENT = "document" # PDF, Word, etc.
|
||||
LINK = "link" # URL
|
||||
VIDEO = "video" # Video-Link
|
||||
IMAGE = "image" # Bild
|
||||
WORKSHEET = "worksheet" # Arbeitsblatt
|
||||
PRESENTATION = "presentation" # Praesentation
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhaseMaterial:
|
||||
"""
|
||||
Material fuer eine Unterrichtsphase (Feature f19).
|
||||
|
||||
Ermoeglicht das Anhaengen von Dokumenten, Links und Medien
|
||||
an bestimmte Phasen einer Unterrichtsstunde.
|
||||
"""
|
||||
material_id: str
|
||||
teacher_id: str
|
||||
title: str
|
||||
material_type: MaterialType = MaterialType.DOCUMENT
|
||||
url: Optional[str] = None # URL oder Dateipfad
|
||||
description: str = ""
|
||||
phase: Optional[str] = None # Phasen-ID (einstieg, erarbeitung, etc.)
|
||||
subject: str = ""
|
||||
grade_level: str = ""
|
||||
tags: List[str] = field(default_factory=list)
|
||||
is_public: bool = False # Fuer Sharing mit anderen Lehrern
|
||||
usage_count: int = 0
|
||||
session_id: Optional[str] = None # Verknuepfung mit einer Session
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Konvertiert das Material in ein Dictionary."""
|
||||
return {
|
||||
"material_id": self.material_id,
|
||||
"teacher_id": self.teacher_id,
|
||||
"title": self.title,
|
||||
"material_type": self.material_type.value,
|
||||
"url": self.url,
|
||||
"description": self.description,
|
||||
"phase": self.phase,
|
||||
"subject": self.subject,
|
||||
"grade_level": self.grade_level,
|
||||
"tags": self.tags,
|
||||
"is_public": self.is_public,
|
||||
"usage_count": self.usage_count,
|
||||
"session_id": self.session_id,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
Reference in New Issue
Block a user