#!/usr/bin/env python3 """Prüft, dass jedes Feld eines Listeneintrags auch bearbeitbar ist. python3 tools/check-editable.py Hintergrund: Ein Feld hinzuzufügen bedeutet, es an fünf Stellen einzutragen - Modell, Eingabeschema, Ausgabeschema, Router und Oberfläche. Fehlt eine davon, merkt es niemand: Die Anwendung startet, die Oberfläche zeigt ein Feld, der Server verwirft es stillschweigend. Genau das ist beim Artikelnamen passiert - die Ergänzung im Schema fand ihr Suchmuster nicht und lief kommentarlos durch. Rein textlich, ohne SQLAlchemy zu laden. """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent MODELS = ROOT / "backend" / "app" / "models.py" SCHEMAS = ROOT / "backend" / "app" / "schemas_shopping.py" SYNC = ROOT / "backend" / "app" / "routers" / "sync.py" ITEMS = ROOT / "backend" / "app" / "routers" / "items.py" DETAIL = ROOT / "web" / "html" / "js" / "views" / "list-detail.js" # Spalten von list_item, die der Nutzer NICHT ändern können soll. INTERNAL = { "id", "list_id", "created_by", "row_rev", "created_at", "updated_at", "deleted_at", } # Felder, die im Eingabeschema anders heißen als in der Tabelle. # article_id ist bewusst NICHT intern: Der zugeordnete Artikel lässt # sich ändern - über seinen Namen, nicht über die ID. ALIASES = {"article_id": "article_name"} def model_columns(table: str) -> set[str]: source = MODELS.read_text(encoding="utf-8") block = source[source.index(f'__tablename__ = "{table}"'):] end = block.find("\nclass ") if end > 0: block = block[:end] columns = set() for line in block.splitlines(): # Beziehungen sind keine Spalten - sie stehen zwar ebenfalls als # "name: Mapped[...]" da, tragen aber relationship() statt # mapped_column(). if "relationship(" in line: continue match = re.match(r"^ (\w+):\s*Mapped\[", line) if match: columns.add(match.group(1)) return columns def schema_fields(name: str) -> set[str]: source = SCHEMAS.read_text(encoding="utf-8") start = source.index(f"class {name}(BaseModel):") rest = source[start:] end = rest.find("\nclass ") block = rest[:end] if end > 0 else rest return set(re.findall(r"^ (\w+):", block, re.M)) def main() -> int: problems: list[str] = [] columns = model_columns("list_item") editable = {ALIASES.get(c, c) for c in columns - INTERNAL} update_in = schema_fields("ItemUpdateIn") item_out = schema_fields("ItemOut") detail = DETAIL.read_text(encoding="utf-8") menu_start = detail.find("function itemMenu") menu_end = detail.find("function marketSection") menu = detail[menu_start:menu_end] if menu_start > 0 else "" row_start = detail.find("function itemRow") row = detail[row_start:menu_start] if row_start > 0 else "" sync_source = SYNC.read_text(encoding="utf-8") items_source = ITEMS.read_text(encoding="utf-8") for field in sorted(editable): if field not in update_in: problems.append(f"{field}: fehlt in ItemUpdateIn") if field not in sync_source: problems.append(f"{field}: wird in sync.py nicht verarbeitet") if field not in items_source: problems.append(f"{field}: wird in items.py nicht verarbeitet") if field not in menu and field not in row: problems.append(f"{field}: in der Oberfläche nicht bearbeitbar") # Was der Nutzer ändern kann, muss er auch sehen können. for field in sorted(editable): if field == "article_name": continue # heißt in der Ausgabe article_name - passt if field not in item_out: problems.append(f"{field}: fehlt in ItemOut (nicht sichtbar)") if problems: print(f"{len(problems)} Lücke(n):\n") for entry in problems: print(f" - {entry}") return 1 print( f"{len(editable)} bearbeitbare Felder je Eintrag " f"({', '.join(sorted(editable))}) – " "überall eingetragen" ) return 0 if __name__ == "__main__": raise SystemExit(main())