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
@@ -0,0 +1,112 @@
|
||||
"""Freunde: Anfrage schicken, annehmen, ablehnen, entfernen.
|
||||
|
||||
Freunde sind die Grundlage fürs Teilen: Vorratslisten und Kanban-Projekte lassen sich nur mit Freunden teilen,
|
||||
auf der Reise-Karte erscheinen nur Städte von Freunden. Administratoren gelten diese Einschränkungen nicht.
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from .auth import current_user
|
||||
from .common import now_iso
|
||||
from .db import transaction
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
MIN_SEARCH = 2
|
||||
|
||||
|
||||
def _pair(a: int, b: int) -> tuple:
|
||||
return (a, b) if a < b else (b, a)
|
||||
|
||||
|
||||
def friend_ids(conn, user_id: int) -> set:
|
||||
"""IDs aller angenommenen Freunde."""
|
||||
rows = conn.execute(
|
||||
"SELECT user_a, user_b FROM friendships WHERE accepted = 1 AND (user_a = ? OR user_b = ?)", (user_id, user_id)
|
||||
)
|
||||
return {r["user_b"] if r["user_a"] == user_id else r["user_a"] for r in rows}
|
||||
|
||||
|
||||
def shareable(conn, user: dict) -> list:
|
||||
"""Konten, mit denen man teilen darf: Freunde – Administratoren alle (außer sich selbst)."""
|
||||
rows = conn.execute("SELECT id, display_name FROM users WHERE id != ? ORDER BY display_name COLLATE NOCASE", (user["id"],))
|
||||
allowed = None if user["isAdmin"] else friend_ids(conn, user["id"])
|
||||
return [{"id": r["id"], "name": r["display_name"]} for r in rows if allowed is None or r["id"] in allowed]
|
||||
|
||||
|
||||
def check_shareable(conn, user: dict, ids, keep=()) -> None:
|
||||
"""Neue Mitglieder müssen Freunde sein (Admins: beliebig). keep: schon eingetragene Mitglieder dürfen bleiben."""
|
||||
allowed = {u["id"] for u in shareable(conn, user)} | set(keep)
|
||||
if any(i not in allowed for i in ids):
|
||||
raise HTTPException(400, "Teilen geht nur mit Freunden – bitte zuerst unter „Freunde“ hinzufügen.")
|
||||
|
||||
|
||||
def _person(row) -> dict:
|
||||
return {"id": row["id"], "name": row["display_name"], "username": row["username"]}
|
||||
|
||||
|
||||
def _state(conn, user_id: int) -> dict:
|
||||
rows = conn.execute(
|
||||
"""SELECT u.id, u.username, u.display_name, f.accepted, f.requested_by FROM friendships f
|
||||
JOIN users u ON u.id = CASE WHEN f.user_a = ? THEN f.user_b ELSE f.user_a END
|
||||
WHERE f.user_a = ? OR f.user_b = ? ORDER BY u.display_name COLLATE NOCASE""",
|
||||
(user_id, user_id, user_id),
|
||||
).fetchall()
|
||||
return {
|
||||
"friends": [_person(r) for r in rows if r["accepted"]],
|
||||
"incoming": [_person(r) for r in rows if not r["accepted"] and r["requested_by"] != user_id],
|
||||
"outgoing": [_person(r) for r in rows if not r["accepted"] and r["requested_by"] == user_id],
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/freunde")
|
||||
def get_friends(user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
return _state(conn, user["id"])
|
||||
|
||||
|
||||
@router.get("/api/freunde/suche")
|
||||
def search_users(q: str = "", user: dict = Depends(current_user)):
|
||||
"""Konten nach Name oder Benutzername suchen (ab 2 Zeichen), mit Stand der Freundschaft."""
|
||||
q = q.strip()
|
||||
if len(q) < MIN_SEARCH:
|
||||
return []
|
||||
like = "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
||||
with transaction() as conn:
|
||||
rows = conn.execute(
|
||||
"""SELECT id, username, display_name FROM users
|
||||
WHERE id != ? AND (display_name LIKE ? ESCAPE '\\' OR username LIKE ? ESCAPE '\\')
|
||||
ORDER BY display_name COLLATE NOCASE LIMIT 20""",
|
||||
(user["id"], like, like),
|
||||
).fetchall()
|
||||
state = _state(conn, user["id"])
|
||||
status = {p["id"]: s for s, key in (("freund", "friends"), ("angefragt", "outgoing"), ("eingeladen", "incoming")) for p in state[key]}
|
||||
return [{**_person(r), "status": status.get(r["id"])} for r in rows]
|
||||
|
||||
|
||||
@router.post("/api/freunde/{other_id}")
|
||||
def befriend(other_id: int, user: dict = Depends(current_user)):
|
||||
"""Anfrage schicken – oder eine offene Anfrage der anderen Person annehmen."""
|
||||
if other_id == user["id"]:
|
||||
raise HTTPException(400, "Mit sich selbst befreundet sein geht nicht.")
|
||||
a, b = _pair(user["id"], other_id)
|
||||
with transaction() as conn:
|
||||
if not conn.execute("SELECT 1 FROM users WHERE id = ?", (other_id,)).fetchone():
|
||||
raise HTTPException(404, "Konto nicht gefunden.")
|
||||
row = conn.execute("SELECT * FROM friendships WHERE user_a = ? AND user_b = ?", (a, b)).fetchone()
|
||||
if not row:
|
||||
conn.execute(
|
||||
"INSERT INTO friendships (user_a, user_b, requested_by, accepted, created_at) VALUES (?, ?, ?, 0, ?)",
|
||||
(a, b, user["id"], now_iso()),
|
||||
)
|
||||
elif not row["accepted"] and row["requested_by"] != user["id"]:
|
||||
conn.execute("UPDATE friendships SET accepted = 1 WHERE user_a = ? AND user_b = ?", (a, b))
|
||||
return _state(conn, user["id"])
|
||||
|
||||
|
||||
@router.delete("/api/freunde/{other_id}")
|
||||
def unfriend(other_id: int, user: dict = Depends(current_user)):
|
||||
"""Anfrage zurückziehen oder ablehnen bzw. Freundschaft beenden."""
|
||||
a, b = _pair(user["id"], other_id)
|
||||
with transaction() as conn:
|
||||
conn.execute("DELETE FROM friendships WHERE user_a = ? AND user_b = ?", (a, b))
|
||||
return _state(conn, user["id"])
|
||||
Reference in new issue
Block a user