neue Archivfunktion und Optimierung an Artikelbearbeitung

This commit is contained in:
2026-08-09 18:24:04 +02:00
parent dd51532533
commit b80e2d5a6c
23 changed files with 1185 additions and 77 deletions

119
tools/check-editable.py Normal file
View 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())