Files
einkaufsapp/tools/check-editable.py

120 lines
4.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())