diff --git a/backend/models.py b/backend/models.py index 9aa1075..5ebb39f 100644 --- a/backend/models.py +++ b/backend/models.py @@ -307,13 +307,28 @@ class CalendarShare(Base): calendar_id = Column(Integer, ForeignKey("local_calendars.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"), nullable=False) permission = Column(String(20), default="read") # 'read' | 'read_write' - color = Column(String(16), nullable=True) # recipient's own colour; NULL = owner's created_at = Column(String(50), nullable=True) # ISO 8601 calendar = relationship("LocalCalendar") user = relationship("User") +class CalendarColorPref(Base): + """A user's personal colour for a calendar they don't own — works for any + way a foreign calendar becomes visible (direct share, group calendar, or a + co-member's group-visible calendar). NULL/absent = the owner's colour.""" + + __tablename__ = "calendar_color_prefs" + __table_args__ = ( + UniqueConstraint("calendar_id", "user_id", name="uq_calendar_color_pref"), + ) + + id = Column(Integer, primary_key=True, index=True) + calendar_id = Column(Integer, ForeignKey("local_calendars.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + color = Column(String(16), nullable=False) + + class Group(Base): __tablename__ = "groups" diff --git a/backend/permissions.py b/backend/permissions.py index 6988371..270ba7e 100644 --- a/backend/permissions.py +++ b/backend/permissions.py @@ -163,3 +163,14 @@ def readable_local_calendar_ids(db: Session, user: models.User) -> list[int]: ids.update(c.id for c in co_member_group_visible_calendars(db, user)) return list(ids) + + +def color_prefs_for(db: Session, user_id: int) -> dict[int, str]: + """Map calendar_id -> the user's personal colour for calendars they don't + own (any sharing path). Empty when the user set no overrides.""" + return { + p.calendar_id: p.color + for p in db.query(models.CalendarColorPref).filter( + models.CalendarColorPref.user_id == user_id + ) + } diff --git a/backend/routers/caldav_router.py b/backend/routers/caldav_router.py index bf3e53d..d5d3088 100644 --- a/backend/routers/caldav_router.py +++ b/backend/routers/caldav_router.py @@ -367,6 +367,8 @@ def get_events( models.GroupCalendar.calendar_id.in_(readable_ids) ) } if readable_ids else set() + # Per-user colour overrides for calendars the user doesn't own (any share path). + color_prefs = permissions.color_prefs_for(db, current_user.id) # Cache each owner's private-event visibility (one lookup per owner, not per event). vis_cache: dict = {} @@ -384,7 +386,7 @@ def get_events( share = shares_by_cal.get(local_cal.id) if is_shared_personal else None shared_owner_name = name_cache.get(local_cal.user_id) if is_shared_personal else None shared_read_only = is_shared_personal and (share.permission if share else None) != "read_write" - shared_color = share.color if share else None + shared_color = color_prefs.get(local_cal.id) local_events = ( db.query(models.LocalEvent) .filter( diff --git a/backend/routers/local_router.py b/backend/routers/local_router.py index 471e7f2..9896cb8 100644 --- a/backend/routers/local_router.py +++ b/backend/routers/local_router.py @@ -120,6 +120,9 @@ def list_calendars( ) } + # Per-user colour overrides for calendars the user doesn't own (any sharing path). + color_prefs = permissions.color_prefs_for(db, current_user.id) + # Own calendars own = ( db.query(models.LocalCalendar) @@ -152,7 +155,7 @@ def list_calendars( cal, owned=False, shared_by=(owner.display_name or owner.username) if owner else None, permission=share.permission, - color_override=share.color, + color_override=color_prefs.get(cal.id), ) if cal.id in group_cal_map: d["group"] = True @@ -167,7 +170,8 @@ def list_calendars( if not cal: continue seen_ids.add(cal_id) - d = _cal_dict(cal, owned=False, shared_by=group_name, permission="read_write") + d = _cal_dict(cal, owned=False, shared_by=group_name, permission="read_write", + color_override=color_prefs.get(cal.id)) d["group"] = True result.append(d) @@ -183,6 +187,7 @@ def list_calendars( cal, owned=False, shared_by=(owner.display_name or owner.username) if owner else None, permission="read", + color_override=color_prefs.get(cal.id), request=request, ) d["group_shared"] = True @@ -261,17 +266,30 @@ def set_calendar_color( current_user: models.User = Depends(get_current_user), ): """Set a calendar's colour. The owner changes the calendar's colour for - everyone; a share recipient sets only their OWN per-user colour (stored on - the share) without touching the owner's. Recipients may recolour but never - rename a shared calendar.""" - cal = permissions.accessible_local_calendar(db, current_user, calendar_id) + everyone; anyone else who can see the calendar (direct share, group calendar, + or a co-member's group-visible calendar) sets only their OWN per-user colour. + Recipients may recolour but never rename a shared calendar.""" + cal = db.query(models.LocalCalendar).filter(models.LocalCalendar.id == calendar_id).first() + if cal is None: + raise HTTPException(404, "Calendar not found") if cal.user_id == current_user.id: cal.color = data.color else: - share = permissions._share_for(db, calendar_id, current_user.id) - if share is None: - raise HTTPException(403, "Only the owner or a share recipient can set the colour") - share.color = data.color + if calendar_id not in permissions.readable_local_calendar_ids(db, current_user): + raise HTTPException(403, "You cannot access this calendar") + pref = ( + db.query(models.CalendarColorPref) + .filter( + models.CalendarColorPref.calendar_id == calendar_id, + models.CalendarColorPref.user_id == current_user.id, + ) + .first() + ) + if pref: + pref.color = data.color + else: + db.add(models.CalendarColorPref( + calendar_id=calendar_id, user_id=current_user.id, color=data.color)) db.commit() return {"ok": True, "color": data.color}