neue Archivfunktion und Optimierung an Artikelbearbeitung
This commit is contained in:
@@ -29,6 +29,7 @@ run() {
|
||||
run "Konfigurationsvorlage" bash tools/check-env.sh .env.example
|
||||
run "Berechtigungen" python3 tools/check-routes.py
|
||||
run "Datenbankschema" python3 tools/check-schema.py
|
||||
run "Bearbeitbare Felder" python3 tools/check-editable.py
|
||||
run "nginx-Konfiguration" python3 tools/check-nginx.py web
|
||||
run "JavaScript-Module" node tools/check-js.mjs
|
||||
run "Strichcode-Decoder" node tools/test-barcode.mjs
|
||||
|
||||
119
tools/check-editable.py
Normal file
119
tools/check-editable.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user