fix(web): hide edit/delete for others' events; persistent hide for shared calendars
Bug 1 — a calendar shared with me stayed visible after unchecking it: the hide was a one-shot cache filter the server undid on refetch. Add a per-device hidden set (localStorage 'hiddenLocalCalendars'), honoured in filterEvents (normal view) and used to drive the checkbox state, so it survives refetch/reload. Bug 2 — in the group combined view, other members' events showed edit/delete and 403'd on save. The combined endpoint now emits read_only (editable = the group calendar OR my own events), via a read_only param threaded through build_local_event_dict/expand_recurring_local. The event popup and edit modal now treat read_only events as read-only (copy still allowed). Test added. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,7 @@ def private_visibility_for(db: Session, user_id: int) -> str:
|
||||
# field (title/location/description/creator/calendar name/recurrence) can leak.
|
||||
_BUSY_KEEP = {
|
||||
"id", "url", "start", "end", "allDay", "calendar_id", "calendarColor",
|
||||
"source", "type", "owner", "is_group_event", "display_color",
|
||||
"source", "type", "owner", "is_group_event", "display_color", "read_only",
|
||||
}
|
||||
|
||||
|
||||
@@ -102,12 +102,14 @@ def build_local_event_dict(
|
||||
creator: Optional[dict] = None,
|
||||
owner: Optional[dict] = None,
|
||||
is_group_event: bool = False,
|
||||
read_only: bool = False,
|
||||
) -> dict:
|
||||
"""Build the unified dict for a single local event (or occurrence).
|
||||
|
||||
``start``/``end``/``all_day`` override the stored values (used when emitting
|
||||
an expanded recurrence occurrence). ``owner``/``is_group_event`` are only set
|
||||
by the group combined view.
|
||||
by the group combined view. ``read_only`` marks events the requester may not
|
||||
edit (someone else's calendar), so clients can hide edit/delete.
|
||||
"""
|
||||
d = {
|
||||
"id": ev.uid,
|
||||
@@ -134,6 +136,8 @@ def build_local_event_dict(
|
||||
d["owner"] = owner
|
||||
if is_group_event:
|
||||
d["is_group_event"] = True
|
||||
if read_only:
|
||||
d["read_only"] = True
|
||||
return d
|
||||
|
||||
|
||||
@@ -146,6 +150,7 @@ def expand_recurring_local(
|
||||
creator: Optional[dict] = None,
|
||||
owner: Optional[dict] = None,
|
||||
is_group_event: bool = False,
|
||||
read_only: bool = False,
|
||||
) -> list:
|
||||
"""Expand a recurring LocalEvent into individual occurrences in the range."""
|
||||
results = []
|
||||
@@ -177,6 +182,7 @@ def expand_recurring_local(
|
||||
ev, local_cal,
|
||||
start=occ_start.isoformat(), end=occ_end.isoformat(), all_day=True,
|
||||
creator=creator, owner=owner, is_group_event=is_group_event,
|
||||
read_only=read_only,
|
||||
))
|
||||
else:
|
||||
ev_start = dt_datetime.fromisoformat(ev_start_str)
|
||||
@@ -203,11 +209,13 @@ def expand_recurring_local(
|
||||
ev, local_cal,
|
||||
start=occ.isoformat(), end=occ_end.isoformat(), all_day=False,
|
||||
creator=creator, owner=owner, is_group_event=is_group_event,
|
||||
read_only=read_only,
|
||||
))
|
||||
except Exception as exc:
|
||||
logger.warning("Error expanding recurring event %s: %s", ev.uid, exc)
|
||||
# Fall back to a single event.
|
||||
results.append(build_local_event_dict(
|
||||
ev, local_cal, creator=creator, owner=owner, is_group_event=is_group_event,
|
||||
read_only=read_only,
|
||||
))
|
||||
return results
|
||||
|
||||
@@ -380,6 +380,9 @@ def combined_events(
|
||||
def emit_calendar(cal: models.LocalCalendar, owner_id: int, is_group: bool):
|
||||
owner_user = name_cache.get(owner_id)
|
||||
owner = {"id": owner_id, "display_name": owner_user}
|
||||
# Editable by the requester iff it's the shared group calendar (all members
|
||||
# may write) or the requester's own calendar; everyone else's is read-only.
|
||||
read_only = not (is_group or owner_id == current_user.id)
|
||||
events = (
|
||||
db.query(models.LocalEvent)
|
||||
.filter(
|
||||
@@ -405,9 +408,9 @@ def combined_events(
|
||||
creator = {"id": None, "display_name": f"{ev.creator_name_external} (importiert)"}
|
||||
|
||||
if ev.rrule:
|
||||
built = expand_recurring_local(ev, cal, start_dt, end_dt, creator=creator, owner=owner, is_group_event=is_group)
|
||||
built = expand_recurring_local(ev, cal, start_dt, end_dt, creator=creator, owner=owner, is_group_event=is_group, read_only=read_only)
|
||||
else:
|
||||
built = [build_local_event_dict(ev, cal, rrule=None, creator=creator, owner=owner, is_group_event=is_group)]
|
||||
built = [build_local_event_dict(ev, cal, rrule=None, creator=creator, owner=owner, is_group_event=is_group, read_only=read_only)]
|
||||
|
||||
for b in built:
|
||||
if ev.is_private and creator_owner_id != current_user.id and visibility_for(creator_owner_id) == "busy":
|
||||
|
||||
@@ -484,3 +484,31 @@ def test_directory_hidden_excludes_from_picker_but_not_admin(client):
|
||||
# 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())
|
||||
|
||||
|
||||
def test_combined_view_read_only_for_other_members(client):
|
||||
"""In the group combined view, events I may not edit carry read_only=True:
|
||||
other members' calendars are read-only; the group calendar + my own aren't."""
|
||||
admin = register_admin(client)
|
||||
b_id, b_tok = create_user(client, admin, "bob")
|
||||
group = client.post("/api/groups/", headers=auth(admin),
|
||||
json={"name": "Team", "member_ids": [b_id]}).json()
|
||||
gid = group["id"]
|
||||
gcal = group["group_calendar_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})
|
||||
_make_event(client, b_tok, b_cal, "Bobs Termin")
|
||||
_make_event(client, admin, gcal, "Gruppentermin")
|
||||
|
||||
# As admin: bob's event is read-only; the group calendar is editable.
|
||||
by = {e["title"]: e for e in
|
||||
client.get(f"/api/groups/{gid}/combined", headers=auth(admin), params=RANGE).json()["events"]}
|
||||
assert by["Bobs Termin"].get("read_only") is True
|
||||
assert by["Gruppentermin"].get("read_only") is not True
|
||||
|
||||
# As bob: his own event and the group calendar are both editable.
|
||||
by_b = {e["title"]: e for e in
|
||||
client.get(f"/api/groups/{gid}/combined", headers=auth(b_tok), params=RANGE).json()["events"]}
|
||||
assert by_b["Bobs Termin"].get("read_only") is not True
|
||||
assert by_b["Gruppentermin"].get("read_only") is not True
|
||||
|
||||
Reference in New Issue
Block a user