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

+95
View File
@@ -0,0 +1,95 @@
"""Privat-Modell: Kanban-Projekte nur für Mitglieder, Artikel lesen alle / bearbeiten Beteiligte, Admins dürfen alles."""
from app import db
def projekt(client, name="P", members=(), links=()):
res = client.post("/api/kanban/projekte", json={"name": name, "members": list(members), "links": list(links)})
assert res.status_code == 201, res.text
return res.json()["id"]
def artikel(client, title="Doku"):
res = client.post("/api/projects", json={"title": title})
assert res.status_code == 201, res.text
return res.json()["id"]
def test_kanban_privat(login, befriend):
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
pid = projekt(anna)
assert ben.get("/api/kanban").json()["projects"] == []
assert ben.get(f"/api/kanban/projekte/{pid}").status_code == 404
assert ben.post(f"/api/kanban/projekte/{pid}/aufgaben", json={"title": "x"}).status_code == 404
assert chef.get(f"/api/kanban/projekte/{pid}").status_code == 200 # Admin sieht alles
# Mitglied nur als Freund
assert anna.patch(f"/api/kanban/projekte/{pid}", json={"members": [ben.user["id"]]}).status_code == 400
befriend(anna, ben)
data = anna.patch(f"/api/kanban/projekte/{pid}", json={"members": [ben.user["id"]]}).json()
assert data["project"]["members"] == [ben.user["id"]]
assert ben.get(f"/api/kanban/projekte/{pid}").status_code == 200
# Mitglieder verwalten nur Ersteller/Admin
assert ben.patch(f"/api/kanban/projekte/{pid}", json={"members": []}).status_code == 403
def test_bearbeiter_nur_beteiligte(login, befriend):
anna, ben, carl = login("anna"), login("ben"), login("carl")
befriend(anna, ben)
pid = projekt(anna, members=[ben.user["id"]])
data = anna.post(f"/api/kanban/projekte/{pid}/aufgaben", json={"title": "A", "assignee": ben.user["id"]}).json()
task = data["tasks"][0]
assert [p["name"] for p in data["people"]] == ["Anna", "Ben"]
assert anna.patch(f"/api/kanban/aufgaben/{task['id']}", json={"assignee": carl.user["id"]}).status_code == 400
# Ben fliegt raus → seine Aufgaben sind niemandem mehr zugeteilt
data = anna.patch(f"/api/kanban/projekte/{pid}", json={"members": []}).json()
assert data["tasks"][0]["assignee"] is None
def test_artikel_lesen_alle_bearbeiten_beteiligte(login, befriend):
anna, ben, carl, chef = login("anna"), login("ben"), login("carl"), login("chef", admin=True)
aid = artikel(anna)
assert ben.get(f"/api/projects/{aid}").json()["canEdit"] is False
assert ben.patch(f"/api/projects/{aid}", json={"title": "x"}).status_code == 403
# Ben arbeitet an einem verknüpften Kanban-Projekt mit → darf bearbeiten
befriend(anna, ben)
projekt(anna, members=[ben.user["id"]], links=[aid])
assert ben.get(f"/api/projects/{aid}").json()["canEdit"] is True
assert ben.patch(f"/api/projects/{aid}", json={"title": "Neu"}).status_code == 200
assert carl.get(f"/api/projects/{aid}").status_code == 200 # lesen dürfen alle
assert carl.patch(f"/api/projects/{aid}", json={"title": "x"}).status_code == 403
assert chef.patch(f"/api/projects/{aid}", json={"title": "Admin"}).status_code == 200
# Löschen bleibt Autor/Admin
assert ben.delete(f"/api/projects/{aid}").status_code == 403
def test_verknuepfen_nur_bearbeitbare_artikel(login):
"""Sonst könnte man sich durch ein eigenes Kanban-Projekt Bearbeitungsrechte verschaffen."""
anna, carl = login("anna"), login("carl")
aid = artikel(anna)
res = carl.post("/api/kanban/projekte", json={"name": "Trick", "links": [aid]})
assert res.status_code == 400
assert carl.patch(f"/api/projects/{aid}", json={"title": "x"}).status_code == 403
def test_board_auf_artikelseite_nur_sichtbare(login):
anna, ben = login("anna"), login("ben")
aid = artikel(anna)
projekt(anna, name="Intern", links=[aid])
assert [b["name"] for b in anna.get(f"/api/projects/{aid}").json()["boards"]] == ["Intern"]
assert ben.get(f"/api/projects/{aid}").json()["boards"] == []
def test_umstellung_privat(conn, login):
"""Schritt 6: bisher gemeinsame Listen/Projekte bekommen alle Konten als Mitglieder, ohne Besitzer → erster Admin."""
anna, ben, chef = login("anna"), login("ben"), login("chef", admin=True)
with conn:
conn.execute("INSERT INTO shopping_lists (id, name, everyone, position, created_at, created_by) VALUES (50, 'Alt', 1, 1, 'x', ?)", (anna.user["id"],))
conn.execute("INSERT INTO shopping_lists (id, name, everyone, position, created_at) VALUES (51, 'Ohne', 1, 2, 'x')")
conn.execute("INSERT INTO kanban_projects (id, name, position, created_at, created_by, updated_at) VALUES (60, 'K', 1, 'x', ?, 'x')", (anna.user["id"],))
conn.execute("PRAGMA user_version = 5")
db.init()
members = lambda lid: sorted(r[0] for r in conn.execute("SELECT user_id FROM shopping_list_members WHERE list_id = ?", (lid,)))
assert members(50) == sorted([ben.user["id"], chef.user["id"]])
assert conn.execute("SELECT created_by FROM shopping_lists WHERE id = 51").fetchone()[0] == chef.user["id"]
assert sorted(r[0] for r in conn.execute("SELECT user_id FROM kanban_project_members WHERE project_id = 60")) == sorted([ben.user["id"], chef.user["id"]])
assert conn.execute("SELECT COUNT(*) FROM shopping_lists WHERE everyone = 1").fetchone()[0] == 0
assert any(l["id"] == 50 for l in ben.get("/api/haushalt").json()["lists"])