update
This commit is contained in:
163
backend/planung.py
Normal file
163
backend/planung.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""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)}
|
||||
Reference in New Issue
Block a user