feat(sharing): label shared personal calendars by owner + drop group title prefix
Two group/sharing display fixes, both server-side so every client benefits:
1. A personal calendar shared WITH a user showed only its raw name
("Persönlich"), indistinguishable from the user's own. The merge read now
relabels a shared *personal* calendar under the owner's display name (so
Guido's "Persönlich" reads as "Guido" for recipients) and adds read_only:true
when the share isn't read_write. Group calendars are excluded — they keep
their own name and stay writable for members.
2. The combined group view prefixed every foreign event with the owner's first
name ("Guido: …"). Each member already has a distinct display_color, so the
prefix was redundant. _decorate_title now returns the raw title; display_title
stays non-empty so clients' legacy prefix fallback never triggers. The change
takes effect on already-installed clients with no app update.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,21 @@ def get_events(
|
|||||||
.all()
|
.all()
|
||||||
) if readable_ids else []
|
) if readable_ids else []
|
||||||
name_cache = {u.id: (u.display_name or u.username) for u in db.query(models.User).all()}
|
name_cache = {u.id: (u.display_name or u.username) for u in db.query(models.User).all()}
|
||||||
|
# A personal calendar shared WITH the current user is relabelled under the
|
||||||
|
# owner's name (so Guido's "Persönlich" reads as "Guido" for his mum) and
|
||||||
|
# flagged read_only unless the share grants write. Group calendars are
|
||||||
|
# excluded — they keep their own name and stay writable for members.
|
||||||
|
shares_map = {
|
||||||
|
s.calendar_id: s.permission
|
||||||
|
for s in db.query(models.CalendarShare).filter(
|
||||||
|
models.CalendarShare.user_id == current_user.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
group_cal_ids = {
|
||||||
|
r[0] for r in db.query(models.GroupCalendar.calendar_id).filter(
|
||||||
|
models.GroupCalendar.calendar_id.in_(readable_ids)
|
||||||
|
)
|
||||||
|
} if readable_ids else set()
|
||||||
# Cache each owner's private-event visibility (one lookup per owner, not per event).
|
# Cache each owner's private-event visibility (one lookup per owner, not per event).
|
||||||
vis_cache: dict = {}
|
vis_cache: dict = {}
|
||||||
|
|
||||||
@@ -361,6 +376,13 @@ def get_events(
|
|||||||
return vis_cache[uid]
|
return vis_cache[uid]
|
||||||
|
|
||||||
for local_cal in local_calendars:
|
for local_cal in local_calendars:
|
||||||
|
# Decoration for a personal calendar shared with (not owned by) me.
|
||||||
|
is_shared_personal = (
|
||||||
|
local_cal.user_id != current_user.id
|
||||||
|
and local_cal.id not in group_cal_ids
|
||||||
|
)
|
||||||
|
shared_owner_name = name_cache.get(local_cal.user_id) if is_shared_personal else None
|
||||||
|
shared_read_only = is_shared_personal and shares_map.get(local_cal.id) != "read_write"
|
||||||
local_events = (
|
local_events = (
|
||||||
db.query(models.LocalEvent)
|
db.query(models.LocalEvent)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -391,6 +413,10 @@ def get_events(
|
|||||||
else:
|
else:
|
||||||
built = [build_local_event_dict(ev, local_cal, rrule=None, creator=creator)]
|
built = [build_local_event_dict(ev, local_cal, rrule=None, creator=creator)]
|
||||||
for b in built:
|
for b in built:
|
||||||
|
if shared_owner_name:
|
||||||
|
b["calendar_name"] = shared_owner_name
|
||||||
|
if shared_read_only:
|
||||||
|
b["read_only"] = True
|
||||||
b = apply_event_privacy(
|
b = apply_event_privacy(
|
||||||
b, owner_id=owner_id, is_private=is_priv,
|
b, owner_id=owner_id, is_private=is_priv,
|
||||||
requester_id=current_user.id, visibility=visibility,
|
requester_id=current_user.id, visibility=visibility,
|
||||||
|
|||||||
@@ -317,17 +317,12 @@ def _first_name(name: Optional[str]) -> str:
|
|||||||
|
|
||||||
def _decorate_title(title: str, *, is_group: bool, creator: Optional[dict],
|
def _decorate_title(title: str, *, is_group: bool, creator: Optional[dict],
|
||||||
owner: Optional[dict], me_id: int) -> str:
|
owner: Optional[dict], me_id: int) -> str:
|
||||||
"""Server-side display title for the combined view so every client (web,
|
"""Server-side display title for the combined view. The former owner/creator
|
||||||
iOS, Android) renders identically: another member's / creator's first name
|
first-name prefix ("Guido: …") was dropped: each member already has a
|
||||||
is prefixed. No icon glyph is embedded — group icons are semantic keys the
|
distinct colour, so the prefix was redundant noise. We still return a
|
||||||
clients render as native vector icons, and group-calendar events are
|
non-empty `display_title` (== raw title) so the clients' legacy fallback —
|
||||||
distinguished by their (group) colour. The raw `title` stays for editing."""
|
which rebuilds a prefix when `display_title` is empty — never kicks in.
|
||||||
if is_group:
|
`display_color` continues to carry the per-person colour."""
|
||||||
if creator and creator.get("id") is not None and creator.get("id") != me_id:
|
|
||||||
return f"{_first_name(creator.get('display_name'))}: {title}"
|
|
||||||
return title
|
|
||||||
if owner and owner.get("id") is not None and owner.get("id") != me_id:
|
|
||||||
return f"{_first_name(owner.get('display_name'))}: {title}"
|
|
||||||
return title
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user