Files
planer2/backend/features.py
2026-09-07 07:44:00 +02:00

90 lines
2.9 KiB
Python

"""Feature-Dateien: eine .md je Feature, Rohtext wird verbatim gespeichert."""
import re
import parser
import projekte
_UMLAUTE = str.maketrans({"ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss"})
# Dateisysteme lassen 255 Zeichen je Name zu. Der Titel darf länger sein — er steht
# in der Datei, nicht im Namen. Ohne Deckel warf ein langer Titel einen Serverfehler.
NAMENSLAENGE = 80
def _slug(titel: str) -> str:
s = titel.lower().translate(_UMLAUTE)
s = re.sub(r"[^a-z0-9]+", "-", s).strip("-")
return s[:NAMENSLAENGE].strip("-") or "feature"
def _eintrag(datei) -> dict:
roh = datei.read_text()
return {"id": datei.stem, "roh": roh, "sektionen": parser.zerlege(roh)}
def _datei(projekt: str, id: str):
datei = projekte.wurzel(projekt) / f"{id}.md"
if not datei.is_file():
raise FileNotFoundError(f"Feature „{id}“ nicht gefunden")
return datei
def liste(projekt: str, nur_stand: bool = False) -> list[dict]:
"""Die Karten. Standardmäßig die, an denen gerade gearbeitet wird.
`nur_stand` liest stattdessen den Projektordner — die Übersicht soll sagen,
was das Projekt heute kann, nicht was jemand gerade plant.
"""
ordner = projekte.stand(projekt) if nur_stand else projekte.wurzel(projekt)
dateien = ordner.glob("*.md")
eintraege = [_eintrag(d) for d in sorted(dateien)]
return sorted(eintraege, key=lambda e: e["sektionen"]["titel"].lower())
def pruefbar(projekt: str) -> list[dict]:
"""Alles außer der Vision. Die hält fest, wie es gedacht ist — sie ist die
Vorlage, nicht der Prüfling, und keine Prüfung schlägt Änderungen daran vor."""
return [f for f in liste(projekt) if f["sektionen"]["art"] != parser.VISION]
def _frei(ordner, slug: str):
"""Der erste freie Dateiname zum Slug; gleiche Namen bekommen eine Nummer."""
datei = ordner / f"{slug}.md"
n = 2
while datei.exists():
datei = ordner / f"{slug}-{n}.md"
n += 1
return datei
def anlegen(projekt: str, roh: str) -> dict:
titel = parser.zerlege(roh)["titel"]
if not titel:
raise ValueError("Titel fehlt")
datei = _frei(projekte.wurzel(projekt), _slug(titel))
datei.write_text(roh)
return _eintrag(datei)
def _folge_titel(datei, titel: str):
"""Benennt die Datei um, wenn der Slug nicht mehr zum Titel passt."""
slug = _slug(titel)
if re.fullmatch(re.escape(slug) + r"(-\d+)?", datei.stem):
return datei # passt schon (inkl. Kollisions-Suffix)
ziel = _frei(datei.parent, slug)
datei.rename(ziel)
return ziel
def speichern(projekt: str, id: str, roh: str) -> dict:
datei = _datei(projekt, id)
titel = parser.zerlege(roh)["titel"]
if not titel:
raise ValueError("Titel fehlt")
datei.write_text(roh)
return _eintrag(_folge_titel(datei, titel))
def loeschen(projekt: str, id: str):
_datei(projekt, id).unlink()