This commit is contained in:
team3
2026-06-12 17:18:42 +02:00
parent cfc666055c
commit 78d5833fe4
38 changed files with 1854 additions and 740 deletions

View File

@@ -3,35 +3,64 @@
Regeln (nur Neu-Erstellungen; Themen, Bausteine, OnePager unbegrenzt):
- JE Format (MiniGuide/Guide/FullGuide) höchstens 3 erstellte, nicht absolvierte Guides
- Progression pro Thema: Guide erst nach absolviertem MiniGuide, FullGuide erst nach absolviertem Guide
- Absolviert (Mini/Guide/FullGuide): ALLE Bausteine (Section-Titel) des neuesten
fertigen Guides haben eine bestandene Prüfung (baustein_progress). Das
Kapitel-Häkchen ist nur noch eine Lese-Markierung. OnePager: Kapitel-Häkchen.
Alle Funktionen arbeiten auf einmal geladenen Daten (lade_lernstand) — keine
Query-Schleifen mehr pro Guide.
"""
import json
from database import list_guides, list_progress_all
from database import list_baustein_absolviert_all, list_guides, list_progress_all
from guide import guide_slot_dateien
from paths import bausteine_path, guide_content_path
from textkit import _norm_titel
MAX_OFFENE_GUIDES = 3
VORSTUFE = {"Guide": "MiniGuide", "FullGuide": "Guide"}
FORMATE = ("MiniGuide", "Guide", "FullGuide")
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]]]:
"""Guides + kompletter Kapitel-Fortschritt in zwei Queries."""
return await list_guides(), await list_progress_all()
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, set[str]]]:
"""Guides + Kapitel-Fortschritt + absolvierte Bausteine in drei Queries.
bausteine_done: topic → normalisierte Titel der Bausteine mit bestandener Prüfung.
"""
bausteine_done = {
topic: {_norm_titel(b) for b in titel}
for topic, titel in (await list_baustein_absolviert_all()).items()
}
return await list_guides(), await list_progress_all(), bausteine_done
def _kapitel_titel(topic: str, fmt: str) -> set[str] | None:
def _content_json(topic: str, fmt: str) -> dict | None:
path = guide_content_path(topic, fmt)
if not path.exists():
return None
try:
chapters = json.loads(path.read_text(encoding="utf-8")).get("chapters", [])
return json.loads(path.read_text(encoding="utf-8"))
except ValueError:
return None
return {c.get("title") for c in chapters}
def _kapitel_titel(topic: str, fmt: str) -> set[str] | None:
content = _content_json(topic, fmt)
if content is None:
return None
return {c.get("title") for c in content.get("chapters", [])}
def _section_titel(topic: str, fmt: str) -> set[str] | None:
"""Normalisierte Baustein-Titel (Sections) aus dem Guide-Content."""
content = _content_json(topic, fmt)
if content is None:
return None
return {
_norm_titel(s.get("title", ""))
for ch in content.get("chapters", [])
for s in ch.get("sections", [])
}
def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]:
@@ -44,28 +73,31 @@ def _neueste_done(guides: list[dict], fmt: str) -> dict[str, dict]:
return neueste
def _guide_absolviert(g: dict, progress: dict[str, set[str]]) -> bool:
titles = _kapitel_titel(g["topic"], g["format"])
return bool(titles) and titles <= progress.get(g["id"], set())
def _guide_absolviert(g: dict, progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> bool:
if g["format"] == "OnePager":
titles = _kapitel_titel(g["topic"], g["format"])
return bool(titles) and titles <= progress.get(g["id"], set())
sections = _section_titel(g["topic"], g["format"])
return bool(sections) and sections <= bausteine_done.get(g["topic"], set())
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> bool:
"""Alle Kapitel des neuesten fertigen Guides (Thema+Format) abgehakt?"""
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> bool:
"""Alle Bausteine des neuesten fertigen Guides (Thema+Format) per Prüfung absolviert?"""
g = _neueste_done(guides, fmt).get(topic)
return g is not None and _guide_absolviert(g, progress)
return g is not None and _guide_absolviert(g, progress, bausteine_done)
def formate_stats(guides: list[dict], progress: dict[str, set[str]]) -> dict:
def formate_stats(guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> dict:
"""Pro Format erstellt/absolviert — pro Thema zählt nur der neueste fertige Guide."""
formate = {}
for fmt in FORMATE:
neueste = _neueste_done(guides, fmt)
absolviert = sum(1 for g in neueste.values() if _guide_absolviert(g, progress))
absolviert = sum(1 for g in neueste.values() if _guide_absolviert(g, progress, bausteine_done))
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
return formate
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]]) -> str | None:
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], bausteine_done: dict[str, set[str]]) -> str | None:
"""Grund, warum ein Neu-Start für Thema+Format gesperrt ist — None = erlaubt.
Exakt die Regeln aus POST /guides: Bausteine nötig, kein Duplikat-Start,
@@ -79,9 +111,9 @@ def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set
content = guide_content_path(topic, fmt)
if fmt != "OnePager" and not content.exists() and not guide_slot_dateien(content):
vorstufe = VORSTUFE.get(fmt)
if vorstufe and not ist_absolviert(topic, vorstufe, guides, progress):
return f"Erst den {vorstufe} dieses Themas absolvieren"
stat = formate_stats(guides, progress).get(fmt, {"erstellt": 0, "absolviert": 0})
if vorstufe and not ist_absolviert(topic, vorstufe, guides, progress, bausteine_done):
return f"Erst den {vorstufe} dieses Themas absolvieren (alle Bausteine prüfen)"
stat = formate_stats(guides, progress, bausteine_done).get(fmt, {"erstellt": 0, "absolviert": 0})
offen = stat["erstellt"] - stat["absolviert"]
if offen >= MAX_OFFENE_GUIDES:
return f"Erst {fmt}s absolvieren — maximal {MAX_OFFENE_GUIDES} offene erlaubt ({offen} offen)"