Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
All services: admin-v2, studio-v2, website, ai-compliance-sdk, consent-service, klausur-service, voice-service, and infrastructure. Large PDFs and compiled binaries excluded via .gitignore.
179 lines
5.3 KiB
Python
179 lines
5.3 KiB
Python
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
|
||
|