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>
408 lines
13 KiB
Python
408 lines
13 KiB
Python
"""
|
|
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,
|
|
}
|