feat(sharing): group-shared calendars in every member's sidebar, person share picker, hidden profiles

- Backend: co-member group_visible calendars now surface in /local/calendars
  (owned=false, shared_by=owner, read-only, group_shared) and in the normal
  /caldav/events merge (via readable_local_calendar_ids), deduped against
  direct shares / group calendars so nothing appears twice.
- Backend: new User.directory_hidden — a user can hide from sharing/group
  pickers (/users/directory), while admin user management (/users/) still lists
  them. Migration + profile GET/PUT.
- Backend: /groups/{id} members carry shares_calendar so clients can drop
  phantom rows for members who share nothing.
- Frontend: reachable "Teilen" button on owned local calendars; share modal is
  now a checkbox multi-select of users (checked = shared). Hidden-profile toggle
  in Settings → Profile. Group member filter only lists members who actually
  share (phantom fix). Calendars shared with me moved to a dedicated read-only
  "shared with me" section in the manage table.
- Tests: group_visible propagation, no-share absence, dedup, directory_hidden.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-06 16:35:11 +02:00
parent f76d2783d9
commit 79fdf4f54a
13 changed files with 245 additions and 23 deletions

View File

@@ -409,3 +409,78 @@ def test_import_export_only_local(client):
cal_id = _make_calendar(client, admin, "Privat")
# Bob has no access -> 404 on export.
assert client.get(f"/api/local/calendars/{cal_id}/export", headers=auth(b_tok)).status_code == 404
# ── Group-visible calendars propagate to co-members' sidebars ─────────────
def test_group_visible_propagates_to_member_sidebar(client):
admin = register_admin(client)
b_id, b_tok = create_user(client, admin, "bob")
client.post("/api/groups/", headers=auth(admin),
json={"name": "Team", "member_ids": [b_id]})
# Bob designates a calendar as group-visible; admin (co-member) should see it.
b_cal = _make_calendar(client, b_tok, "Bobs Kalender")
client.put("/api/settings/", headers=auth(b_tok), json={"group_visible_calendar_id": b_cal})
_make_event(client, b_tok, b_cal, "Bobs Termin")
cals = client.get("/api/local/calendars", headers=auth(admin)).json()
shared = [c for c in cals if c["id"] == b_cal]
assert len(shared) == 1, shared
assert shared[0]["owned"] is False
assert shared[0]["shared_by"] == "bob"
assert shared[0]["permission"] == "read"
assert shared[0].get("group_shared") is True
# Its events appear in the normal merged read, read-only.
events = client.get("/api/caldav/events", headers=auth(admin), params=RANGE).json()["events"]
bob_ev = [e for e in events if e["title"] == "Bobs Termin"]
assert bob_ev and bob_ev[0].get("read_only") is True
def test_group_visible_absent_when_not_designated(client):
admin = register_admin(client)
b_id, b_tok = create_user(client, admin, "bob")
client.post("/api/groups/", headers=auth(admin),
json={"name": "Team", "member_ids": [b_id]})
b_cal = _make_calendar(client, b_tok, "Bobs Kalender") # never designated
cals = client.get("/api/local/calendars", headers=auth(admin)).json()
assert not any(c["id"] == b_cal for c in cals)
def test_group_visible_not_duplicated_with_direct_share(client):
admin = register_admin(client)
admin_id = client.get("/api/profile/", headers=auth(admin)).json()["id"]
b_id, b_tok = create_user(client, admin, "bob")
client.post("/api/groups/", headers=auth(admin),
json={"name": "Team", "member_ids": [b_id]})
b_cal = _make_calendar(client, b_tok, "Bobs Kalender")
client.put("/api/settings/", headers=auth(b_tok), json={"group_visible_calendar_id": b_cal})
# Also directly shared with admin (read_write) — must not double-list.
client.post(f"/api/local/calendars/{b_cal}/shares", headers=auth(b_tok),
json={"user_id": admin_id, "permission": "read_write"})
cals = client.get("/api/local/calendars", headers=auth(admin)).json()
matches = [c for c in cals if c["id"] == b_cal]
assert len(matches) == 1, matches
# The direct share wins (read_write, not flagged group_shared).
assert matches[0]["permission"] == "read_write"
assert matches[0].get("group_shared") is not True
# ── Hidden profile (directory opt-out) ────────────────────────────────────
def test_directory_hidden_excludes_from_picker_but_not_admin(client):
admin = register_admin(client)
b_id, b_tok = create_user(client, admin, "bob")
assert any(u["id"] == b_id for u in
client.get("/api/users/directory", headers=auth(admin)).json())
r = client.put("/api/profile/", headers=auth(b_tok), json={"directory_hidden": True})
assert r.status_code == 200, r.text
assert not any(u["id"] == b_id for u in
client.get("/api/users/directory", headers=auth(admin)).json())
# Admin user management still lists the hidden user.
assert any(u["id"] == b_id for u in
client.get("/api/users/", headers=auth(admin)).json())