Listen mit eigenem Vorrat und Einkauf, Städte anderer auf der Reise-Karte, Design "Fabrik"
Vorrat & Einkauf: - Mehrere Listen (z. B. "Zuhause", "Urlaub"), jede mit eigenem Vorrat und eigener Einkaufsliste; Auswahl oben auf der Seite, gemerkt pro Browser - Beim Anlegen festlegen, wer die Liste sieht: alle Konten oder nur ausgewählte (Ersteller sieht sie immer); ändern/löschen nur Ersteller und Administratoren - Gekauft, aufgebraucht, Nachkaufen und Rezeptabgleich wirken innerhalb der gewählten Liste - Migration: bisheriger Vorrat und Einkauf werden zur Liste "Zuhause" (für alle sichtbar) Reisen: - Schalter "Städte anderer zeigen": Städte der übrigen Konten in je einer eigenen Farbe, einzelne Personen ein-/ausblendbar, nur lesend - Info-Karte zeigt, wer noch in einer Stadt bzw. einem Land war Design "Fabrik" (Industrie-Inventar à la Satisfactory): - Stahlplatten mit Schrauben, Titelleisten mit Fenstersymbol, gelb leuchtende Hauptbuttons, orange Mengen-Zähler, Warnstreifen, Riffelblech und drehende Zahnräder im Hintergrund - Schriften Roboto / Roboto Condensed (OFL), lokal eingebunden Doku: README verlinkt das zugehörige Repository P42-S.M. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f16c8c73c6
commit
b30acb652a
20 files changed
+1182
-82
No files matched your search
+45
-8
@@ -10,18 +10,20 @@ from .config import DATA_DIR
|
||||
|
||||
DB_FILE = DATA_DIR / "p42.db"
|
||||
|
||||
# Geteilte Vorrats- und Einkaufsliste; key = Name in casefold() für den Vergleich.
|
||||
# Pro Produkt, Liste und Einheit eine Zeile ("Mehl 1 kg" und "Mehl 2 Stück" dürfen nebeneinander stehen).
|
||||
# Vorrat und Einkauf; key = Name in casefold() für den Vergleich.
|
||||
# Jeder Eintrag gehört zu einer Liste (shopping_lists, z. B. "Zuhause", "Urlaub") mit eigenem Vorrat und Einkauf.
|
||||
# Pro Liste, Produkt, Bereich und Einheit eine Zeile ("Mehl 1 kg" und "Mehl 2 Stück" dürfen nebeneinander stehen).
|
||||
HOUSEHOLD_TABLE = """(
|
||||
id INTEGER PRIMARY KEY,
|
||||
key TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
list TEXT NOT NULL CHECK (list IN ('vorrat', 'einkauf')),
|
||||
list_id INTEGER NOT NULL REFERENCES shopping_lists(id) ON DELETE CASCADE,
|
||||
quantity REAL NOT NULL CHECK (quantity > 0),
|
||||
unit TEXT NOT NULL DEFAULT 'stk' CHECK (unit IN ('stk', 'kg', 'l')),
|
||||
updated_at TEXT NOT NULL,
|
||||
updated_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
UNIQUE (key, list, unit)
|
||||
UNIQUE (list_id, key, list, unit)
|
||||
)"""
|
||||
|
||||
# Kanban-Projekte (je ein Board), gemeinsam für alle Konten
|
||||
@@ -115,6 +117,23 @@ CREATE TABLE IF NOT EXISTS monitor_samples (
|
||||
PRIMARY KEY (monitor_id, ts)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Listen mit je eigenem Vorrat und Einkauf (z. B. "Zuhause", "Urlaub");
|
||||
-- everyone = 1: alle Konten sehen sie, sonst nur Ersteller und Mitglieder
|
||||
CREATE TABLE IF NOT EXISTS shopping_lists (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
everyone INTEGER NOT NULL DEFAULT 1,
|
||||
position REAL NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shopping_list_members (
|
||||
list_id INTEGER NOT NULL REFERENCES shopping_lists(id) ON DELETE CASCADE,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (list_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS household_items {HOUSEHOLD_TABLE};
|
||||
|
||||
-- Produktkatalog für die Vorschläge
|
||||
@@ -244,15 +263,33 @@ def _import_legacy(conn) -> None:
|
||||
|
||||
|
||||
def _migrate(conn) -> None:
|
||||
# Vorrat/Einkauf aus der Zeit vor den Einheiten: Tabelle mit Einheit und Kommazahlen neu aufbauen
|
||||
columns = {r["name"] for r in conn.execute("PRAGMA table_info(household_items)")}
|
||||
if "unit" in columns:
|
||||
"""Vorrat/Einkauf aus früheren Versionen (ohne Einheiten, ohne Listen oder Vorrat ohne Liste) neu aufbauen.
|
||||
|
||||
Alles, was noch keiner Liste gehört, kommt in die erste Liste bzw. in eine neue Liste "Zuhause",
|
||||
die alle Konten sehen.
|
||||
"""
|
||||
info = {r["name"]: r for r in conn.execute("PRAGMA table_info(household_items)")}
|
||||
if "list_id" in info and info["list_id"]["notnull"]:
|
||||
return
|
||||
row = conn.execute("SELECT id FROM shopping_lists ORDER BY position, id LIMIT 1").fetchone()
|
||||
if row:
|
||||
default = row["id"]
|
||||
# Die Zwischenversion nannte die gemeinsame Liste "Einkaufsliste"; jetzt enthält sie auch den Vorrat
|
||||
conn.execute("UPDATE shopping_lists SET name = 'Zuhause' WHERE id = ? AND name = 'Einkaufsliste' AND created_by IS NULL", (default,))
|
||||
else:
|
||||
default = conn.execute(
|
||||
"INSERT INTO shopping_lists (name, everyone, position, created_at) VALUES ('Zuhause', 1, 1, ?)", (now_iso(),)
|
||||
).lastrowid
|
||||
unit = "unit" if "unit" in info else "'stk'"
|
||||
list_id = "COALESCE(list_id, ?)" if "list_id" in info else "?"
|
||||
conn.execute("DROP INDEX IF EXISTS household_vorrat")
|
||||
conn.execute("DROP INDEX IF EXISTS household_einkauf")
|
||||
conn.execute("ALTER TABLE household_items RENAME TO household_items_alt")
|
||||
conn.execute("CREATE TABLE household_items " + HOUSEHOLD_TABLE)
|
||||
conn.execute(
|
||||
"""INSERT INTO household_items (id, key, name, list, quantity, unit, updated_at, updated_by)
|
||||
SELECT id, key, name, list, quantity, 'stk', updated_at, updated_by FROM household_items_alt"""
|
||||
f"""INSERT INTO household_items (id, key, name, list, list_id, quantity, unit, updated_at, updated_by)
|
||||
SELECT id, key, name, list, {list_id}, quantity, {unit}, updated_at, updated_by FROM household_items_alt""",
|
||||
(default,),
|
||||
)
|
||||
conn.execute("DROP TABLE household_items_alt")
|
||||
|
||||
|
||||
+157
-32
@@ -1,6 +1,8 @@
|
||||
"""Geteilte Vorrats- und Einkaufsliste mit Produktkatalog für einheitliche Namen.
|
||||
"""Vorrat und Einkauf in mehreren Listen sowie Produktkatalog für einheitliche Namen.
|
||||
|
||||
Alle Konten sehen und bearbeiten dieselben Listen. Jedes Produkt steht pro Liste und Einheit
|
||||
Jede Liste (z. B. "Zuhause", "Urlaub") hat ihren eigenen Vorrat und ihre eigene Einkaufsliste.
|
||||
Wer eine Liste anlegt, legt fest, ob alle Konten sie sehen oder nur ausgewählte. Ändern
|
||||
(Name, Sichtbarkeit) und löschen dürfen der Ersteller und Administratoren. Jedes Produkt steht pro Liste und Einheit
|
||||
(Anzahl, kg, l) nur einmal; erneutes Hinzufügen in derselben Einheit erhöht die Menge. Neue Produktnamen landen im Katalog und werden als
|
||||
Vorschläge angeboten – "milch" wird dabei automatisch zum bereits bekannten "Milch".
|
||||
"""
|
||||
@@ -20,6 +22,8 @@ UNITS = ("stk", "kg", "l")
|
||||
MAX_NAME = 60
|
||||
MAX_QUANTITY = 999
|
||||
MAX_PRODUCTS = 2000
|
||||
MAX_LIST_NAME = 40
|
||||
MAX_LISTS = 50
|
||||
|
||||
|
||||
def _item(row) -> dict:
|
||||
@@ -27,6 +31,7 @@ def _item(row) -> dict:
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"list": row["list"],
|
||||
"listId": row["list_id"],
|
||||
"quantity": _out_quantity(row["quantity"], row["unit"]),
|
||||
"unit": row["unit"],
|
||||
"updatedAt": row["updated_at"],
|
||||
@@ -96,43 +101,124 @@ def _canonical_name(conn, name: str) -> str:
|
||||
return name
|
||||
|
||||
|
||||
def _add(conn, name: str, list_: str, quantity, unit: str, user_id: int) -> None:
|
||||
"""Produkt auf eine Liste setzen; steht es dort schon in derselben Einheit, wird die Menge erhöht."""
|
||||
def _add(conn, name: str, list_: str, list_id: int, quantity, unit: str, user_id: int) -> None:
|
||||
"""Produkt in den Vorrat bzw. Einkauf einer Liste setzen; steht es dort schon in derselben Einheit, wird die Menge erhöht."""
|
||||
conn.execute(
|
||||
"""INSERT INTO household_items (key, name, list, quantity, unit, updated_at, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (key, list, unit) DO UPDATE SET
|
||||
"""INSERT INTO household_items (key, name, list, list_id, quantity, unit, updated_at, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (list_id, key, list, unit) DO UPDATE SET
|
||||
quantity = MIN(ROUND(quantity + excluded.quantity, 2), ?),
|
||||
updated_at = excluded.updated_at,
|
||||
updated_by = excluded.updated_by""",
|
||||
(_key(name), name, list_, quantity, unit, now_iso(), user_id, MAX_QUANTITY),
|
||||
(_key(name), name, list_, list_id, quantity, unit, now_iso(), user_id, MAX_QUANTITY),
|
||||
)
|
||||
|
||||
|
||||
def _get_item(conn, item_id: int):
|
||||
row = conn.execute("SELECT * FROM household_items WHERE id = ?", (item_id,)).fetchone()
|
||||
# ---------- Listen und wer sie sieht ----------
|
||||
|
||||
# Sichtbar: für alle freigegeben, selbst angelegt oder als Mitglied eingetragen.
|
||||
# Listen eines gelöschten Kontos ohne Mitglieder sehen Administratoren, damit sie nicht verwaist bleiben.
|
||||
_VISIBLE = """(l.everyone = 1 OR l.created_by = :uid
|
||||
OR EXISTS (SELECT 1 FROM shopping_list_members m WHERE m.list_id = l.id AND m.user_id = :uid)
|
||||
OR (:admin AND l.created_by IS NULL))"""
|
||||
|
||||
|
||||
def _who(user: dict) -> dict:
|
||||
return {"uid": user["id"], "admin": int(user["isAdmin"])}
|
||||
|
||||
|
||||
def _visible_lists(conn, user: dict) -> list:
|
||||
return conn.execute(
|
||||
f"""SELECT l.*, u.display_name AS created_by_name FROM shopping_lists l
|
||||
LEFT JOIN users u ON u.id = l.created_by
|
||||
WHERE {_VISIBLE} ORDER BY l.position, l.id""",
|
||||
_who(user),
|
||||
).fetchall()
|
||||
|
||||
|
||||
def _get_list(conn, list_id, user: dict):
|
||||
if isinstance(list_id, bool) or not isinstance(list_id, int):
|
||||
raise ValidationError("Bitte eine Liste wählen.")
|
||||
row = conn.execute(f"SELECT l.* FROM shopping_lists l WHERE l.id = :id AND {_VISIBLE}", {"id": list_id, **_who(user)}).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden – vielleicht hat ihn gerade jemand anderes geändert.")
|
||||
raise HTTPException(404, "Liste nicht gefunden – vielleicht wurde sie gelöscht oder nicht mit dir geteilt.")
|
||||
return row
|
||||
|
||||
|
||||
def _state(conn) -> dict:
|
||||
def _can_manage(row, user: dict) -> bool:
|
||||
return row["created_by"] == user["id"] or user["isAdmin"]
|
||||
|
||||
|
||||
def _shopping_list(conn, row, user: dict) -> dict:
|
||||
members = [r["user_id"] for r in conn.execute("SELECT user_id FROM shopping_list_members WHERE list_id = ?", (row["id"],))]
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"everyone": bool(row["everyone"]),
|
||||
"members": members,
|
||||
"createdBy": {"id": row["created_by"], "name": row["created_by_name"]} if row["created_by"] else None,
|
||||
"canManage": _can_manage(row, user),
|
||||
}
|
||||
|
||||
|
||||
def _list_fields(conn, data: dict, user: dict, owner_id) -> tuple:
|
||||
"""Name, "für alle" und Mitglieder prüfen. Der Ersteller sieht seine Liste immer und steht nicht bei den Mitgliedern."""
|
||||
name = re.sub(r"\s+", " ", data.get("name")).strip() if isinstance(data.get("name"), str) else ""
|
||||
if not name:
|
||||
raise ValidationError("Name der Liste fehlt.")
|
||||
if len(name) > MAX_LIST_NAME:
|
||||
raise ValidationError(f"Name ist zu lang (max. {MAX_LIST_NAME} Zeichen).")
|
||||
everyone = data.get("everyone", True)
|
||||
if not isinstance(everyone, bool):
|
||||
raise ValidationError("Ungültige Sichtbarkeit.")
|
||||
members = data.get("members", [])
|
||||
if not isinstance(members, list) or any(isinstance(m, bool) or not isinstance(m, int) for m in members):
|
||||
raise ValidationError("Ungültige Auswahl der Konten.")
|
||||
members = {m for m in members if m != owner_id}
|
||||
if members:
|
||||
known = {r["id"] for r in conn.execute(f"SELECT id FROM users WHERE id IN ({','.join('?' * len(members))})", list(members))}
|
||||
if known != members:
|
||||
raise ValidationError("Ein ausgewähltes Konto gibt es nicht mehr.")
|
||||
return name, everyone, sorted(members) if not everyone else []
|
||||
|
||||
|
||||
def _set_members(conn, list_id: int, members: list) -> None:
|
||||
conn.execute("DELETE FROM shopping_list_members WHERE list_id = ?", (list_id,))
|
||||
conn.executemany("INSERT INTO shopping_list_members (list_id, user_id) VALUES (?, ?)", [(list_id, m) for m in members])
|
||||
|
||||
|
||||
def _get_item(conn, item_id: int, user: dict):
|
||||
row = conn.execute("SELECT * FROM household_items WHERE id = ?", (item_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Eintrag nicht gefunden – vielleicht hat ihn gerade jemand anderes geändert.")
|
||||
_get_list(conn, row["list_id"], user)
|
||||
return row
|
||||
|
||||
|
||||
def _state(conn, user: dict) -> dict:
|
||||
lists = _visible_lists(conn, user)
|
||||
ids = [r["id"] for r in lists]
|
||||
items = conn.execute(
|
||||
"""SELECT i.*, u.display_name AS updated_by_name FROM household_items i
|
||||
LEFT JOIN users u ON u.id = i.updated_by"""
|
||||
f"""SELECT i.*, u.display_name AS updated_by_name FROM household_items i
|
||||
LEFT JOIN users u ON u.id = i.updated_by
|
||||
WHERE i.list_id IN ({','.join('?' * len(ids))})""",
|
||||
ids,
|
||||
).fetchall()
|
||||
# Häufig genutzte Produkte zuerst vorschlagen
|
||||
products = conn.execute("SELECT name, uses FROM products").fetchall()
|
||||
users = conn.execute("SELECT id, display_name FROM users ORDER BY display_name COLLATE NOCASE").fetchall()
|
||||
return {
|
||||
"items": [_item(r) for r in sorted(items, key=lambda r: _sort_key(r["name"]))],
|
||||
"lists": [_shopping_list(conn, r, user) for r in lists],
|
||||
"users": [{"id": u["id"], "name": u["display_name"]} for u in users],
|
||||
"products": [r["name"] for r in sorted(products, key=lambda r: (-r["uses"], _sort_key(r["name"])))],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/haushalt")
|
||||
def get_household(_: dict = Depends(current_user)):
|
||||
def get_household(user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
return _state(conn)
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.post("/api/haushalt/items", status_code=201)
|
||||
@@ -144,15 +230,16 @@ def add_item(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
unit = _unit(data.get("unit"))
|
||||
quantity = _quantity(data.get("quantity", 1), unit)
|
||||
with transaction() as conn:
|
||||
_add(conn, _canonical_name(conn, name), list_, quantity, unit, user["id"])
|
||||
return _state(conn)
|
||||
list_id = _get_list(conn, data.get("listId"), user)["id"]
|
||||
_add(conn, _canonical_name(conn, name), list_, list_id, quantity, unit, user["id"])
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.patch("/api/haushalt/items/{item_id}")
|
||||
def update_item(item_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
"""Menge und/oder Einheit ändern. Beim Einheitenwechsel wird mit einer bestehenden Zeile zusammengeführt."""
|
||||
with transaction() as conn:
|
||||
row = _get_item(conn, item_id)
|
||||
row = _get_item(conn, item_id, user)
|
||||
unit = _unit(data.get("unit", row["unit"]))
|
||||
if "quantity" in data:
|
||||
quantity = _quantity(data["quantity"], unit)
|
||||
@@ -160,46 +247,84 @@ def update_item(item_id: int, data: dict = Body(...), user: dict = Depends(curre
|
||||
# Nur Einheit gewechselt: Zahl bleibt, bei "Anzahl" auf ganze Stück aufrunden
|
||||
quantity = max(1, math.ceil(row["quantity"])) if unit == "stk" else row["quantity"]
|
||||
other = conn.execute(
|
||||
"SELECT id FROM household_items WHERE key = ? AND list = ? AND unit = ? AND id != ?",
|
||||
(row["key"], row["list"], unit, item_id),
|
||||
"SELECT id FROM household_items WHERE list_id = ? AND key = ? AND list = ? AND unit = ? AND id != ?",
|
||||
(row["list_id"], row["key"], row["list"], unit, item_id),
|
||||
).fetchone()
|
||||
if other:
|
||||
conn.execute("DELETE FROM household_items WHERE id = ?", (item_id,))
|
||||
_add(conn, row["name"], row["list"], quantity, unit, user["id"])
|
||||
_add(conn, row["name"], row["list"], row["list_id"], quantity, unit, user["id"])
|
||||
else:
|
||||
conn.execute(
|
||||
"UPDATE household_items SET quantity = ?, unit = ?, updated_at = ?, updated_by = ? WHERE id = ?",
|
||||
(quantity, unit, now_iso(), user["id"], item_id),
|
||||
)
|
||||
return _state(conn)
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.post("/api/haushalt/items/{item_id}/move")
|
||||
def move_item(item_id: int, user: dict = Depends(current_user)):
|
||||
"""Einkauf → Vorrat ("gekauft") bzw. Vorrat → Einkauf ("aufgebraucht"), mit Zusammenführen."""
|
||||
"""Innerhalb einer Liste: Einkauf → Vorrat ("gekauft") bzw. Vorrat → Einkauf ("aufgebraucht"), mit Zusammenführen."""
|
||||
with transaction() as conn:
|
||||
row = _get_item(conn, item_id)
|
||||
row = _get_item(conn, item_id, user)
|
||||
target = "vorrat" if row["list"] == "einkauf" else "einkauf"
|
||||
# Aufgebraucht heißt: einmal nachkaufen (1 Stück, 1 kg, 1 l), egal wie viel vorher da war
|
||||
quantity = row["quantity"] if target == "vorrat" else 1
|
||||
conn.execute("DELETE FROM household_items WHERE id = ?", (item_id,))
|
||||
_add(conn, row["name"], target, quantity, row["unit"], user["id"])
|
||||
return _state(conn)
|
||||
_add(conn, row["name"], target, row["list_id"], quantity, row["unit"], user["id"])
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.delete("/api/haushalt/items/{item_id}")
|
||||
def delete_item(item_id: int, _: dict = Depends(current_user)):
|
||||
def delete_item(item_id: int, user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
_get_item(conn, item_id)
|
||||
_get_item(conn, item_id, user)
|
||||
conn.execute("DELETE FROM household_items WHERE id = ?", (item_id,))
|
||||
return _state(conn)
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.post("/api/haushalt/listen", status_code=201)
|
||||
def create_list(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
name, everyone, members = _list_fields(conn, data, user, user["id"])
|
||||
if conn.execute("SELECT COUNT(*) FROM shopping_lists").fetchone()[0] >= MAX_LISTS:
|
||||
raise ValidationError(f"Es gibt schon {MAX_LISTS} Listen. Bitte erst alte löschen.")
|
||||
position = conn.execute("SELECT COALESCE(MAX(position), 0) + 1 FROM shopping_lists").fetchone()[0]
|
||||
list_id = conn.execute(
|
||||
"INSERT INTO shopping_lists (name, everyone, position, created_at, created_by) VALUES (?, ?, ?, ?, ?)",
|
||||
(name, int(everyone), position, now_iso(), user["id"]),
|
||||
).lastrowid
|
||||
_set_members(conn, list_id, members)
|
||||
return {**_state(conn, user), "created": list_id}
|
||||
|
||||
|
||||
@router.patch("/api/haushalt/listen/{list_id}")
|
||||
def update_list(list_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
row = _get_list(conn, list_id, user)
|
||||
if not _can_manage(row, user):
|
||||
raise HTTPException(403, "Nur wer die Liste angelegt hat (oder ein Administrator) kann sie ändern.")
|
||||
name, everyone, members = _list_fields(conn, data, user, row["created_by"])
|
||||
conn.execute("UPDATE shopping_lists SET name = ?, everyone = ? WHERE id = ?", (name, int(everyone), list_id))
|
||||
_set_members(conn, list_id, members)
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.delete("/api/haushalt/listen/{list_id}")
|
||||
def delete_list(list_id: int, user: dict = Depends(current_user)):
|
||||
"""Liste samt Vorrat und Einkauf löschen (ON DELETE CASCADE)."""
|
||||
with transaction() as conn:
|
||||
row = _get_list(conn, list_id, user)
|
||||
if not _can_manage(row, user):
|
||||
raise HTTPException(403, "Nur wer die Liste angelegt hat (oder ein Administrator) kann sie löschen.")
|
||||
conn.execute("DELETE FROM shopping_lists WHERE id = ?", (list_id,))
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
@router.delete("/api/haushalt/produkte")
|
||||
def delete_product(name: str, _: dict = Depends(current_user)):
|
||||
def delete_product(name: str, user: dict = Depends(current_user)):
|
||||
"""Vorschlag aus dem Katalog entfernen (z. B. Tippfehler). Einträge auf den Listen bleiben."""
|
||||
with transaction() as conn:
|
||||
cur = conn.execute("DELETE FROM products WHERE key = ?", (_key(name),))
|
||||
if not cur.rowcount:
|
||||
raise HTTPException(404, "Vorschlag nicht gefunden.")
|
||||
return _state(conn)
|
||||
return _state(conn, user)
|
||||
+21
-1
@@ -1,5 +1,7 @@
|
||||
"""Reise-Karte pro Konto: besuchte Städte und Länder sowie kurze Infos dazu.
|
||||
|
||||
Die Städte der anderen Konten werden mitgeliefert und lassen sich auf der Karte einblenden (nur lesen).
|
||||
|
||||
Städte werden über die Geocoding-API von Open-Meteo gesucht (Daten: GeoNames, CC BY 4.0).
|
||||
Infotexte kommen aus der deutschen Wikipedia, Einwohnerzahlen von Ländern aus Wikidata.
|
||||
Alle Abrufe laufen über den Server und werden zwischengespeichert; der Browser fragt keine Fremdserver.
|
||||
@@ -40,10 +42,28 @@ def _city(row) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _others(conn, user_id: int) -> list:
|
||||
"""Städte der anderen Konten (nur zum Anzeigen auf der Karte), gruppiert nach Konto."""
|
||||
rows = conn.execute(
|
||||
"""SELECT c.*, u.display_name FROM travel_cities c JOIN users u ON u.id = c.user_id
|
||||
WHERE c.user_id != ? ORDER BY u.display_name COLLATE NOCASE, u.id, c.name""",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
people = {}
|
||||
for r in rows:
|
||||
person = people.setdefault(r["user_id"], {"id": r["user_id"], "name": r["display_name"], "cities": []})
|
||||
person["cities"].append(_city(r))
|
||||
return list(people.values())
|
||||
|
||||
|
||||
def _state(conn, user_id: int) -> dict:
|
||||
cities = conn.execute("SELECT * FROM travel_cities WHERE user_id = ? ORDER BY name", (user_id,)).fetchall()
|
||||
countries = conn.execute("SELECT code FROM travel_countries WHERE user_id = ? ORDER BY code", (user_id,)).fetchall()
|
||||
return {"cities": [_city(r) for r in cities], "countries": [r["code"] for r in countries]}
|
||||
return {
|
||||
"cities": [_city(r) for r in cities],
|
||||
"countries": [r["code"] for r in countries],
|
||||
"others": _others(conn, user_id),
|
||||
}
|
||||
|
||||
|
||||
# ---------- Verwaltung ----------
|
||||
|
||||
@@ -16,7 +16,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
|
||||
from .auth import current_user
|
||||
from .db import transaction
|
||||
from .haushalt import _add, _canonical_name, _clean_name, _key, _quantity, _state, _unit
|
||||
from .haushalt import _add, _canonical_name, _clean_name, _get_list, _key, _quantity, _state, _unit
|
||||
from .links import ValidationError
|
||||
|
||||
router = APIRouter()
|
||||
@@ -342,6 +342,7 @@ def take_over(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
checked.append((_clean_name(i.get("name")), _quantity(i.get("quantity"), unit), unit))
|
||||
learned = [(_clean_name(a.get("ingredient")), _clean_name(a.get("product"))) for a in aliases if isinstance(a, dict)]
|
||||
with transaction() as conn:
|
||||
list_id = _get_list(conn, data.get("listId"), user)["id"] if checked else None # Einkauf dieser Liste
|
||||
for ingredient, product in learned:
|
||||
product = _canonical_name(conn, product)
|
||||
if _key(ingredient) == _key(product):
|
||||
@@ -353,5 +354,5 @@ def take_over(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
(_key(ingredient), product),
|
||||
)
|
||||
for name, quantity, unit in checked:
|
||||
_add(conn, _canonical_name(conn, name), "einkauf", quantity, unit, user["id"])
|
||||
return _state(conn)
|
||||
_add(conn, _canonical_name(conn, name), "einkauf", list_id, quantity, unit, user["id"])
|
||||
return _state(conn, user)
|
||||
Reference in new issue
Block a user