138 lines
6.3 KiB
Python
138 lines
6.3 KiB
Python
"""Lernschulden-Regeln: Progression und Deckel für offene Guides — die EINZIGE Quelle.
|
|
|
|
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_baustein_levels_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"}
|
|
# Welches Niveau die Vorstufe erreichen muss, um das Format freizuschalten.
|
|
FREISCHALT_LEVEL = {"Guide": "absolviert", "FullGuide": "verstanden"}
|
|
FORMATE = ("MiniGuide", "Guide", "FullGuide")
|
|
|
|
|
|
async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, dict[str, set[str]]]]:
|
|
"""Guides + Kapitel-Fortschritt + Bausteine je Meilenstein.
|
|
|
|
levels: {"absolviert"/"verstanden"/"gemeistert": {topic → normalisierte Titel}}.
|
|
"""
|
|
roh = await list_baustein_levels_all()
|
|
levels = {
|
|
stufe: {topic: {_norm_titel(b) for b in titel} for topic, titel in pro_topic.items()}
|
|
for stufe, pro_topic in roh.items()
|
|
}
|
|
return await list_guides(), await list_progress_all(), levels
|
|
|
|
|
|
def _content_json(topic: str, fmt: str) -> dict | None:
|
|
path = guide_content_path(topic, fmt)
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
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]:
|
|
"""Pro Thema der neueste fertige Guide dieses Formats."""
|
|
neueste: dict[str, dict] = {}
|
|
for g in guides:
|
|
if g["format"] == fmt and g["status"] == "done":
|
|
if g["topic"] not in neueste or g["created_at"] > neueste[g["topic"]]["created_at"]:
|
|
neueste[g["topic"]] = g
|
|
return neueste
|
|
|
|
|
|
def _guide_alle(g: dict, progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
|
|
"""Sind ALLE Bausteine (bzw. OnePager-Kapitel) des Guides auf dem geforderten Niveau?"""
|
|
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 <= levelset.get(g["topic"], set())
|
|
|
|
|
|
def ist_level(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levelset: dict[str, set[str]]) -> bool:
|
|
"""Neuester fertiger Guide (Thema+Format): alle Bausteine auf dem Niveau von levelset?"""
|
|
g = _neueste_done(guides, fmt).get(topic)
|
|
return g is not None and _guide_alle(g, progress, levelset)
|
|
|
|
|
|
def ist_absolviert(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
|
|
"""Alle Bausteine des neuesten fertigen Guides absolviert (≥3)?"""
|
|
return ist_level(topic, fmt, guides, progress, levels["absolviert"])
|
|
|
|
|
|
def thema_abgeschlossen(topic: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, dict[str, set[str]]]) -> bool:
|
|
"""Thema fertig: neuester fertiger FullGuide, alle Bausteine gemeistert (25)?"""
|
|
return ist_level(topic, "FullGuide", guides, progress, levels["gemeistert"])
|
|
|
|
|
|
def formate_stats(guides: list[dict], progress: dict[str, set[str]], levels: dict[str, 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_alle(g, progress, levels["absolviert"]))
|
|
formate[fmt] = {"erstellt": len(neueste), "absolviert": absolviert}
|
|
return formate
|
|
|
|
|
|
def guide_lock(topic: str, fmt: str, guides: list[dict], progress: dict[str, set[str]], levels: dict[str, 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,
|
|
Lernschulden nur für echte Neu-Erstellungen (Resume/Regenerieren frei).
|
|
"""
|
|
if fmt != "OnePager" and not bausteine_path(topic).exists():
|
|
return "Erst Bausteine erstellen"
|
|
for g in guides:
|
|
if g["topic"] == topic and g["format"] == fmt and g["status"] in ("queued", "generating"):
|
|
return "Generierung läuft bereits"
|
|
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:
|
|
stufe = FREISCHALT_LEVEL[fmt] # "absolviert" (alle 3) oder "verstanden" (alle 10)
|
|
if not ist_level(topic, vorstufe, guides, progress, levels[stufe]):
|
|
wort = "verstehen (alle Bausteine auf 10)" if stufe == "verstanden" else "absolvieren (alle Bausteine prüfen)"
|
|
return f"Erst den {vorstufe} dieses Themas {wort}"
|
|
stat = formate_stats(guides, progress, levels).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)"
|
|
return None
|