164 lines
5.5 KiB
Python
164 lines
5.5 KiB
Python
"""Eine Planung: der Kartensatz, wie er nach der nächsten Umsetzung aussehen soll.
|
|
|
|
Die Übersicht beantwortet „was kann mein Projekt". Eine geplante Funktion gehört
|
|
dort nicht hin — sie wäre gelogen. Deshalb liegt die Planung in einer eigenen
|
|
Kopie und der Projektordner bleibt unberührt, bis sie umgesetzt wird.
|
|
|
|
Kopiert wird der ganze Ordner, nicht nur die Karten. Gespräch und
|
|
Urteilsgedächtnis gehören zur Planung: was während des Planens besprochen und
|
|
beurteilt wurde, gilt für den geplanten Stand, nicht für den heutigen.
|
|
|
|
Solange eine Planung läuft, arbeitet `projekte.wurzel()` in der Kopie. Keine
|
|
Schreibfunktion muss davon wissen.
|
|
|
|
Es gibt höchstens eine Planung je Projekt. Sie endet auf zwei Arten: umgesetzt —
|
|
dann ersetzt die Kopie den Projektstand — oder abgebrochen, dann verschwindet sie
|
|
spurlos.
|
|
"""
|
|
|
|
import shutil
|
|
|
|
import parser
|
|
import projekte
|
|
|
|
# Was beim Vergleich zählt: der sichtbare Inhalt einer Funktion, nicht ihre Datei.
|
|
# Wird eine Karte umbenannt, wandert ihre Datei — der Vergleich hinge sonst an
|
|
# einem Dateinamen statt an dem, was dasteht.
|
|
FELDER = ("ziel", "regeln", "testen", "stand")
|
|
|
|
|
|
def laeuft(projekt: str) -> bool:
|
|
return (projekte.stand(projekt) / projekte.PLANUNG).is_dir()
|
|
|
|
|
|
def _kopie(projekt: str):
|
|
return projekte.stand(projekt) / projekte.PLANUNG
|
|
|
|
|
|
def starte(projekt: str):
|
|
"""Arbeitskopie anlegen. Ab hier zeigt `wurzel()` dorthin."""
|
|
ziel = _kopie(projekt)
|
|
if ziel.is_dir():
|
|
raise ValueError("Für dieses Projekt läuft schon eine Planung")
|
|
shutil.copytree(projekte.stand(projekt), ziel,
|
|
ignore=shutil.ignore_patterns(projekte.PLANUNG))
|
|
|
|
|
|
def brich_ab(projekt: str):
|
|
"""Planung verwerfen. Der Projektstand hat sie nie gesehen."""
|
|
shutil.rmtree(_kopie(projekt), ignore_errors=True)
|
|
|
|
|
|
def setze_um(projekt: str):
|
|
"""Die Kopie wird der neue Projektstand.
|
|
|
|
Erst die Kopie herausheben, dann den alten Stand räumen, dann die Kopie an
|
|
seine Stelle. Ein Absturz mitten im Räumen ließe sonst ein Projekt ohne Karten
|
|
zurück — und die Planung gleich mit.
|
|
"""
|
|
kopie = _kopie(projekt)
|
|
if not kopie.is_dir():
|
|
raise ValueError("Für dieses Projekt läuft keine Planung")
|
|
ordner = projekte.stand(projekt)
|
|
daneben = ordner.parent / f"{ordner.name}.neu"
|
|
shutil.rmtree(daneben, ignore_errors=True)
|
|
kopie.rename(daneben)
|
|
for eintrag in ordner.iterdir():
|
|
shutil.rmtree(eintrag) if eintrag.is_dir() else eintrag.unlink()
|
|
for eintrag in daneben.iterdir():
|
|
eintrag.rename(ordner / eintrag.name)
|
|
daneben.rmdir()
|
|
|
|
|
|
def _karten(ordner) -> dict:
|
|
"""titel -> Sektionen. Verglichen wird über den Titel, nicht über die Datei."""
|
|
raus = {}
|
|
for datei in sorted(ordner.glob("*.md")):
|
|
s = parser.zerlege(datei.read_text())
|
|
raus[s["titel"]] = s
|
|
return raus
|
|
|
|
|
|
def _funktionstext(f: dict) -> str:
|
|
return "\n".join(str(f.get(feld) or "") for feld in FELDER)
|
|
|
|
|
|
def _funktionen(karte: dict) -> dict:
|
|
return {f["text"]: _funktionstext(f) for f in karte["funktionen"]}
|
|
|
|
|
|
def _kartentext(karte: dict) -> str:
|
|
return "\n".join((karte.get("art") or "", karte.get("ziel") or "",
|
|
karte.get("regeln") or ""))
|
|
|
|
|
|
def aenderungen(projekt: str) -> dict:
|
|
"""Was die Planung am Projektstand ändert — je Karte und je Funktion.
|
|
|
|
Ohne laufende Planung ist das leer. Die Übersicht zeigt dann nichts an, und
|
|
die Karten sehen aus wie immer.
|
|
"""
|
|
if not laeuft(projekt):
|
|
return {}
|
|
alt = _karten(projekte.stand(projekt))
|
|
neu = _karten(_kopie(projekt))
|
|
|
|
raus = {}
|
|
for titel in set(alt) | set(neu):
|
|
a, b = alt.get(titel), neu.get(titel)
|
|
if a is None:
|
|
raus[titel] = {"art": "neu", "funktionen": {
|
|
f["text"]: "neu" for f in b["funktionen"]}}
|
|
continue
|
|
if b is None:
|
|
raus[titel] = {"art": "weg", "funktionen": {
|
|
f["text"]: "weg" for f in a["funktionen"]}}
|
|
continue
|
|
|
|
fa, fb = _funktionen(a), _funktionen(b)
|
|
funktionen = {}
|
|
for name in set(fa) | set(fb):
|
|
if name not in fa:
|
|
funktionen[name] = "neu"
|
|
elif name not in fb:
|
|
funktionen[name] = "weg"
|
|
elif fa[name] != fb[name]:
|
|
funktionen[name] = "geaendert"
|
|
kopf = _kartentext(a) != _kartentext(b)
|
|
if funktionen or kopf:
|
|
raus[titel] = {"art": "geaendert", "funktionen": funktionen,
|
|
"kopf": kopf}
|
|
return raus
|
|
|
|
|
|
def _datei_zu(ordner, titel: str):
|
|
for datei in ordner.glob("*.md"):
|
|
if parser.zerlege(datei.read_text())["titel"] == titel:
|
|
return datei
|
|
return None
|
|
|
|
|
|
def zuruecksetzen(projekt: str, titel: str):
|
|
"""Eine Karte auf den Projektstand zurückholen.
|
|
|
|
Drei Fälle: neu dazugekommen (löschen), weggefallen (zurückholen), geändert
|
|
(überschreiben). Der Rest der Planung bleibt stehen.
|
|
"""
|
|
if not laeuft(projekt):
|
|
raise ValueError("Für dieses Projekt läuft keine Planung")
|
|
ordner, kopie = projekte.stand(projekt), _kopie(projekt)
|
|
urspruenglich = _datei_zu(ordner, titel)
|
|
geplant = _datei_zu(kopie, titel)
|
|
|
|
if urspruenglich is None and geplant is None:
|
|
raise FileNotFoundError(f"Karte „{titel}“ nicht gefunden")
|
|
if urspruenglich is None:
|
|
geplant.unlink()
|
|
return
|
|
ziel = geplant or (kopie / urspruenglich.name)
|
|
ziel.write_text(urspruenglich.read_text())
|
|
|
|
|
|
def stand(projekt: str) -> dict:
|
|
return {"laeuft": laeuft(projekt), "aenderungen": aenderungen(projekt)}
|