120 lines
4.1 KiB
Python
120 lines
4.1 KiB
Python
#!/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())
|