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:
guyg1474andClaude Opus 5.5 committed 2026-10-15 21:49:45 +02:00
1 parent 1d7497bead
commit 0fb3521c2f
34 files changed
+833 -245

No files matched your search

+22 -17
View File
@@ -22,6 +22,7 @@ from .auth import current_user
from .config import WIKIPEDIA_CONTACT
from .common import ValidationError, clean_text, now_iso
from .db import transaction
from .freunde import friend_ids
from .projects import UPLOAD_DIR, UPLOAD_NAME, remove_unused
router = APIRouter()
@@ -51,15 +52,18 @@ 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."""
def _others(conn, user: dict) -> list:
"""Städte von Freunden (nur zum Anzeigen auf der Karte), gruppiert nach Konto; Administratoren sehen alle."""
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 != ? AND c.wish = 0 ORDER BY u.display_name COLLATE NOCASE, u.id, c.name""",
(user_id,),
(user["id"],),
).fetchall()
allowed = None if user["isAdmin"] else friend_ids(conn, user["id"])
people = {}
for r in rows:
if allowed is not None and r["user_id"] not in allowed:
continue
person = people.setdefault(r["user_id"], {"id": r["user_id"], "name": r["display_name"], "cities": []})
person["cities"].append(_city(r))
return list(people.values())
@@ -88,7 +92,8 @@ def _visits(conn, user_id: int) -> list:
]
def _state(conn, user_id: int) -> dict:
def _state(conn, user: dict) -> dict:
user_id = user["id"]
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()
homes = conn.execute(
@@ -103,7 +108,7 @@ def _state(conn, user_id: int) -> dict:
for h in homes
],
"visits": _visits(conn, user_id),
"others": _others(conn, user_id),
"others": _others(conn, user),
}
@@ -113,7 +118,7 @@ def _state(conn, user_id: int) -> dict:
@router.get("/api/reisen")
def get_travel(user: dict = Depends(current_user)):
with transaction() as conn:
return _state(conn, user["id"])
return _state(conn, user)
def _number(value, field: str, low: float, high: float) -> float:
@@ -156,7 +161,7 @@ def add_city(data: dict = Body(...), user: dict = Depends(current_user)):
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (user_id, geoname_id) DO NOTHING""",
values,
)
return _state(conn, user["id"])
return _state(conn, user)
@router.delete("/api/reisen/staedte/{city_id}")
@@ -171,7 +176,7 @@ def delete_city(city_id: int, user: dict = Depends(current_user)):
if not cur.rowcount:
raise HTTPException(404, "Stadt nicht gefunden.")
remove_unused(conn, photos)
return _state(conn, user["id"])
return _state(conn, user)
@router.patch("/api/reisen/staedte/{city_id}")
@@ -184,7 +189,7 @@ def update_city(city_id: int, data: dict = Body(...), user: dict = Depends(curre
if data["wish"] and conn.execute("SELECT 1 FROM travel_visits WHERE city_id = ?", (city_id,)).fetchone():
raise ValidationError("Für diese Stadt sind schon Besuche eingetragen.")
conn.execute("UPDATE travel_cities SET wish = ? WHERE id = ?", (int(data["wish"]), city_id))
return _state(conn, user["id"])
return _state(conn, user)
# ---------- Wohnorte (mit "seit"-Datum) ----------
@@ -228,7 +233,7 @@ def add_home(data: dict = Body(...), user: dict = Depends(current_user)):
since = _date(data.get("since"), "Seit", False)
with transaction() as conn:
_add_home(conn, user["id"], city, since)
return _state(conn, user["id"])
return _state(conn, user)
@router.delete("/api/reisen/wohnorte/{home_id}")
@@ -237,7 +242,7 @@ def delete_home(home_id: int, user: dict = Depends(current_user)):
cur = conn.execute("DELETE FROM travel_homes WHERE id = ? AND user_id = ?", (home_id, user["id"]))
if not cur.rowcount:
raise HTTPException(404, "Wohnort nicht gefunden.")
return _state(conn, user["id"])
return _state(conn, user)
@router.post("/api/reisen/besuche/{visit_id}/gewohnt")
@@ -258,7 +263,7 @@ def lived_there(visit_id: int, user: dict = Depends(current_user)):
_add_home(conn, user["id"], dict(city), visit["date_from"])
if before and not later and before["geoname_id"] != city["geoname_id"]:
_add_home(conn, user["id"], dict(before), after)
return _state(conn, user["id"])
return _state(conn, user)
# ---------- Besuche: Zeitraum, Begleitung, Notizen, Fotos ----------
@@ -345,7 +350,7 @@ def add_visit(city_id: int, data: dict = Body(...), user: dict = Depends(current
).lastrowid
_save_photos(conn, visit_id, photos)
conn.execute("UPDATE travel_cities SET wish = 0 WHERE id = ?", (city_id,))
return _state(conn, user["id"])
return _state(conn, user)
@router.put("/api/reisen/besuche/{visit_id}")
@@ -359,7 +364,7 @@ def update_visit(visit_id: int, data: dict = Body(...), user: dict = Depends(cur
(date_from, date_to, companions, notes, None if continued is None else int(continued), visit_id),
)
remove_unused(conn, _save_photos(conn, visit_id, photos))
return _state(conn, user["id"])
return _state(conn, user)
@router.delete("/api/reisen/besuche/{visit_id}")
@@ -369,7 +374,7 @@ def delete_visit(visit_id: int, user: dict = Depends(current_user)):
photos = [r["file"] for r in conn.execute("SELECT file FROM travel_visit_photos WHERE visit_id = ?", (visit_id,))]
conn.execute("DELETE FROM travel_visits WHERE id = ?", (visit_id,))
remove_unused(conn, photos)
return _state(conn, user["id"])
return _state(conn, user)
def _country_code(code: str) -> str:
@@ -384,14 +389,14 @@ def mark_country(code: str, user: dict = Depends(current_user)):
conn.execute(
"INSERT OR IGNORE INTO travel_countries (user_id, code) VALUES (?, ?)", (user["id"], _country_code(code))
)
return _state(conn, user["id"])
return _state(conn, user)
@router.delete("/api/reisen/laender/{code}")
def unmark_country(code: str, user: dict = Depends(current_user)):
with transaction() as conn:
conn.execute("DELETE FROM travel_countries WHERE user_id = ? AND code = ?", (user["id"], _country_code(code)))
return _state(conn, user["id"])
return _state(conn, user)
# ---------- Städtesuche ----------