This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/learning_units.py
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

179 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
from pathlib import Path
from datetime import datetime
import uuid
import json
import threading
# Basisverzeichnis für Arbeitsblätter & Lerneinheiten
BASE_DIR = Path.home() / "Arbeitsblaetter"
LEARNING_UNITS_DIR = BASE_DIR / "Lerneinheiten"
LEARNING_UNITS_FILE = LEARNING_UNITS_DIR / "learning_units.json"
# Thread-Lock, damit Dateizugriffe sicher bleiben
_lock = threading.Lock()
class LearningUnitBase(BaseModel):
title: str = Field(..., description="Titel der Lerneinheit, z.B. 'Das Auge Klasse 7'")
description: Optional[str] = Field(None, description="Freitext-Beschreibung")
topic: Optional[str] = Field(None, description="Kurz-Thema, z.B. 'Auge'")
grade_level: Optional[str] = Field(None, description="Klassenstufe, z.B. '7'")
language: Optional[str] = Field("de", description="Hauptsprache der Lerneinheit (z.B. 'de', 'tr')")
worksheet_files: List[str] = Field(
default_factory=list,
description="Liste der zugeordneten Arbeitsblatt-Dateien (Basenames oder Pfade)"
)
status: str = Field(
"raw",
description="Pipeline-Status: raw, cleaned, qa_generated, mc_generated, cloze_generated"
)
class LearningUnitCreate(LearningUnitBase):
"""Payload zum Erstellen einer neuen Lerneinheit."""
pass
class LearningUnitUpdate(BaseModel):
"""Teil-Update für eine Lerneinheit."""
title: Optional[str] = None
description: Optional[str] = None
topic: Optional[str] = None
grade_level: Optional[str] = None
language: Optional[str] = None
worksheet_files: Optional[List[str]] = None
status: Optional[str] = None
class LearningUnit(LearningUnitBase):
id: str
created_at: datetime
updated_at: datetime
@classmethod
def from_dict(cls, data: Dict) -> "LearningUnit":
data = data.copy()
if isinstance(data.get("created_at"), str):
data["created_at"] = datetime.fromisoformat(data["created_at"])
if isinstance(data.get("updated_at"), str):
data["updated_at"] = datetime.fromisoformat(data["updated_at"])
return cls(**data)
def to_dict(self) -> Dict:
d = self.dict()
d["created_at"] = self.created_at.isoformat()
d["updated_at"] = self.updated_at.isoformat()
return d
def _ensure_storage():
"""Sorgt dafür, dass der Ordner und die JSON-Datei existieren."""
LEARNING_UNITS_DIR.mkdir(parents=True, exist_ok=True)
if not LEARNING_UNITS_FILE.exists():
with LEARNING_UNITS_FILE.open("w", encoding="utf-8") as f:
json.dump({}, f)
def _load_all_units() -> Dict[str, Dict]:
_ensure_storage()
with LEARNING_UNITS_FILE.open("r", encoding="utf-8") as f:
try:
data = json.load(f)
if not isinstance(data, dict):
return {}
return data
except json.JSONDecodeError:
return {}
def _save_all_units(raw: Dict[str, Dict]) -> None:
_ensure_storage()
with LEARNING_UNITS_FILE.open("w", encoding="utf-8") as f:
json.dump(raw, f, ensure_ascii=False, indent=2)
def list_learning_units() -> List[LearningUnit]:
with _lock:
raw = _load_all_units()
return [LearningUnit.from_dict(v) for v in raw.values()]
def get_learning_unit(unit_id: str) -> Optional[LearningUnit]:
with _lock:
raw = _load_all_units()
data = raw.get(unit_id)
if not data:
return None
return LearningUnit.from_dict(data)
def create_learning_unit(payload: LearningUnitCreate) -> LearningUnit:
now = datetime.utcnow()
lu = LearningUnit(
id=str(uuid.uuid4()),
created_at=now,
updated_at=now,
**payload.dict()
)
with _lock:
raw = _load_all_units()
raw[lu.id] = lu.to_dict()
_save_all_units(raw)
return lu
def update_learning_unit(unit_id: str, payload: LearningUnitUpdate) -> Optional[LearningUnit]:
with _lock:
raw = _load_all_units()
existing = raw.get(unit_id)
if not existing:
return None
lu = LearningUnit.from_dict(existing)
update_data = payload.dict(exclude_unset=True)
for field, value in update_data.items():
setattr(lu, field, value)
lu.updated_at = datetime.utcnow()
raw[lu.id] = lu.to_dict()
_save_all_units(raw)
return lu
def delete_learning_unit(unit_id: str) -> bool:
with _lock:
raw = _load_all_units()
if unit_id not in raw:
return False
del raw[unit_id]
_save_all_units(raw)
return True
def attach_worksheets(unit_id: str, worksheet_files: List[str]) -> Optional[LearningUnit]:
"""
Hängt eine Liste von Arbeitsblatt-Dateien an eine bestehende Lerneinheit an.
Doppelte Einträge werden vermieden.
"""
with _lock:
raw = _load_all_units()
existing = raw.get(unit_id)
if not existing:
return None
lu = LearningUnit.from_dict(existing)
current_set = set(lu.worksheet_files)
for f in worksheet_files:
current_set.add(f)
lu.worksheet_files = sorted(current_set)
lu.updated_at = datetime.utcnow()
raw[lu.id] = lu.to_dict()
_save_all_units(raw)
return lu