Freunde, privates Teilen, Artikelrechte, Reiserouten
- Freunde (freunde.py, Tabelle friendships): suchen, anfragen, annehmen, ablehnen, entfernen; neue Seite "Freunde" im Menü - Vorratslisten und Kanban-Projekte immer privat: sichtbar für Ersteller und Mitglieder; teilen nur mit Freunden (Admins: mit allen); Kanban-Mitglieder (Tabelle kanban_project_members), Bearbeiter nur aus dem Projekt, ausgetretene Mitglieder verlieren ihre Zuteilung - Artikel: lesen alle; bearbeiten Autor, Mitarbeitende verknüpfter Kanban-Projekte und Admins; verknüpfen nur bearbeitbare Artikel (sonst ließen sich Rechte erschleichen); Board-Liste auf der Artikelseite zeigt nur sichtbare Boards - Administratoren sehen und bearbeiten alles - Reisen: Städte nur von Freunden (Admins: alle); "Reiserouten zeigen" zeichnet die Strecken der Kilometer-Rechnung als Bögen - Migration Schritt 6: bisher gemeinsame Listen und alle Projekte bekommen die vorhandenen Konten als Mitglieder, ohne Ersteller → erster Admin; erste Liste einer neuen Installation gehört dem ersten Konto - Tests: test_freunde.py, test_zugriff.py, bestehende auf Privat-Modell umgestellt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d7497bead
commit
0fb3521c2f
34 files changed
+833
-245
No files matched your search
+19
-20
@@ -1,8 +1,8 @@
|
||||
"""Vorrat und Einkauf in mehreren Listen sowie Produktkatalog für einheitliche Namen.
|
||||
|
||||
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
|
||||
Listen sind privat: Sehen dürfen sie der Ersteller und die Freunde, mit denen er sie teilt (Mitglieder).
|
||||
Ändern (Name, Mitglieder) und löschen dürfen der Ersteller und Administratoren; Administratoren sehen alles. 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".
|
||||
"""
|
||||
@@ -14,6 +14,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from .auth import current_user
|
||||
from .common import ValidationError, clean_text, now_iso, user_list
|
||||
from .db import transaction
|
||||
from .freunde import check_shareable, shareable
|
||||
from .kategorien import CATEGORIES, KEYS as CATEGORY_KEYS, guess
|
||||
|
||||
router = APIRouter()
|
||||
@@ -147,11 +148,9 @@ def _add(conn, name: str, list_: str, list_id: int, quantity, unit: str, user_id
|
||||
|
||||
# ---------- 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))"""
|
||||
# Sichtbar: selbst angelegt oder als Mitglied eingetragen; Administratoren sehen alle Listen.
|
||||
_VISIBLE = """(:admin 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))"""
|
||||
|
||||
|
||||
def _who(user: dict) -> dict:
|
||||
@@ -185,7 +184,6 @@ def _shopping_list(conn, row, user: dict) -> dict:
|
||||
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),
|
||||
@@ -206,12 +204,10 @@ def _category_order(value) -> list:
|
||||
return order + [k for k in CATEGORY_KEYS if k not in order]
|
||||
|
||||
|
||||
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."""
|
||||
def _list_fields(conn, data: dict, user: dict, owner_id, current=()) -> tuple:
|
||||
"""Name und Mitglieder prüfen. Mitglieder müssen Freunde sein (Admins: beliebig); schon eingetragene dürfen bleiben.
|
||||
Der Ersteller sieht seine Liste immer und steht nicht bei den Mitgliedern."""
|
||||
name = clean_text(data.get("name"), "Name der Liste", MAX_LIST_NAME)
|
||||
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.")
|
||||
@@ -220,7 +216,8 @@ def _list_fields(conn, data: dict, user: dict, owner_id) -> tuple:
|
||||
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 []
|
||||
check_shareable(conn, user, members, keep=current)
|
||||
return name, sorted(members)
|
||||
|
||||
|
||||
def _set_members(conn, list_id: int, members: list) -> None:
|
||||
@@ -253,7 +250,8 @@ def _state(conn, user: dict) -> dict:
|
||||
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": user_list(conn),
|
||||
"users": user_list(conn), # Namen für die Anzeige der Mitglieder
|
||||
"shareable": shareable(conn, user), # wählbar beim Teilen: Freunde (Admins: alle)
|
||||
"products": [r["name"] for r in products],
|
||||
"productCategories": {r["name"]: r["category"] or guess(r["name"]) for r in products},
|
||||
"categories": [{"key": k, "title": t} for k, t in CATEGORIES],
|
||||
@@ -394,13 +392,13 @@ def delete_item(item_id: int, user: dict = Depends(current_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"])
|
||||
name, 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"]),
|
||||
"INSERT INTO shopping_lists (name, everyone, position, created_at, created_by) VALUES (?, 0, ?, ?, ?)",
|
||||
(name, position, now_iso(), user["id"]),
|
||||
).lastrowid
|
||||
_set_members(conn, list_id, members)
|
||||
return {**_state(conn, user), "created": list_id}
|
||||
@@ -412,8 +410,9 @@ def update_list(list_id: int, data: dict = Body(...), user: dict = Depends(curre
|
||||
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))
|
||||
current = [r["user_id"] for r in conn.execute("SELECT user_id FROM shopping_list_members WHERE list_id = ?", (list_id,))]
|
||||
name, members = _list_fields(conn, data, user, row["created_by"], current)
|
||||
conn.execute("UPDATE shopping_lists SET name = ? WHERE id = ?", (name, list_id))
|
||||
_set_members(conn, list_id, members)
|
||||
return _state(conn, user)
|
||||
|
||||
|
||||
Reference in new issue
Block a user