From 9b635d8f3df5da002048e117d95088781271bd93 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Fri, 24 Jul 2026 07:01:16 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20calendar=20attendee=20RSVP=20correctness?= =?UTF-8?q?=20=E2=80=94=20server-derived=20status=20and=20hide=20declined?= =?UTF-8?q?=20invites=20(#27007)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: let only the attendee set their own calendar RSVP status set_attendees took each attendee's status from the caller-supplied value, so an event organiser could set another user's RSVP (for example to 'accepted') on create or update. RSVP is meant to be self-service: the /events/{id}/rsvp endpoint already scopes status changes to the calling user. Derive attendee status server-side instead of from the request. An existing attendee keeps the status they set via RSVP and a newly added attendee starts 'pending'; any caller-supplied status is ignored. Event edits no longer reset attendees' existing responses. Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com> * fix: hide declined calendar invites from the attendee view `get_events_by_range` surfaced every event where the user is an attendee regardless of their RSVP status, so declining an invite left it in the calendar with no way to remove it. Exclude `declined` attendee rows from the attendee branch, so a decline now removes the event from the user's own view while pending, accepted and tentative invitations still surface. Co-Authored-By: legobattman <302282032+legobattman@users.noreply.github.com> --------- Co-authored-by: legobattman <302282032+legobattman@users.noreply.github.com> --- backend/open_webui/models/calendar.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/backend/open_webui/models/calendar.py b/backend/open_webui/models/calendar.py index 2067ccfab5..b9be370f31 100644 --- a/backend/open_webui/models/calendar.py +++ b/backend/open_webui/models/calendar.py @@ -500,9 +500,12 @@ class CalendarEventTable: # Filter to requested calendars only accessible_cal_ids = [c for c in accessible_cal_ids if c in calendar_ids] - # Also get event IDs where user is an attendee + # Also get event IDs where the user is an attendee, excluding invites they declined attendee_event_ids_result = await db.execute( - select(CalendarEventAttendee.event_id).filter(CalendarEventAttendee.user_id == user_id) + select(CalendarEventAttendee.event_id).filter( + CalendarEventAttendee.user_id == user_id, + CalendarEventAttendee.status != 'declined', + ) ) attendee_event_ids = [r[0] for r in attendee_event_ids_result.all()] @@ -764,22 +767,34 @@ class CalendarEventAttendeeTable: async def set_attendees( self, event_id: str, attendees: list[dict], db: Optional[AsyncSession] = None ) -> list[CalendarEventAttendeeModel]: - """Replace all attendees for an event. + """Replace all attendees for an event ({user_id, meta?} per dict). - Each dict in attendees: {user_id: str, status?: str, meta?: dict} + RSVP status is the attendee's alone to set (via update_rsvp): an existing + attendee keeps their status, a newly added one starts 'pending'. A + caller-supplied status is ignored so an organiser cannot set it for others. """ async with get_async_db_context(db) as db: + existing_status = { + row.user_id: row.status + for row in ( + await db.execute( + select(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id) + ) + ).scalars() + } + # Remove existing await db.execute(delete(CalendarEventAttendee).filter(CalendarEventAttendee.event_id == event_id)) now = int(time.time_ns()) models = [] for att in attendees: + user_id = att['user_id'] row = CalendarEventAttendee( id=str(uuid4()), event_id=event_id, - user_id=att['user_id'], - status=att.get('status', 'pending'), + user_id=user_id, + status=existing_status.get(user_id, 'pending'), meta=att.get('meta'), created_at=now, updated_at=now,