Coverage for app/backend/src/couchers/servicers/events.py: 84%
525 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-23 04:10 +0000
1import logging
2from datetime import datetime, timedelta
3from typing import Any, cast
5import grpc
6from google.protobuf import empty_pb2
7from psycopg.types.range import TimestamptzRange
8from sqlalchemy import Select, select
9from sqlalchemy.orm import Session
10from sqlalchemy.sql import and_, func, or_, update
12from couchers.context import CouchersContext, make_notification_user_context
13from couchers.db import can_moderate_node, get_parent_node_at_location, session_scope
14from couchers.event_log import log_event
15from couchers.helpers.completed_profile import has_completed_profile
16from couchers.jobs.enqueue import queue_job
17from couchers.models import (
18 AttendeeStatus,
19 Cluster,
20 ClusterSubscription,
21 Event,
22 EventCommunityInviteRequest,
23 EventOccurrence,
24 EventOccurrenceAttendee,
25 EventOrganizer,
26 EventSubscription,
27 ModerationObjectType,
28 Node,
29 NodeType,
30 Thread,
31 Upload,
32 User,
33)
34from couchers.models.notifications import NotificationTopicAction
35from couchers.moderation.utils import create_moderation
36from couchers.notifications.notify import notify
37from couchers.proto import events_pb2, events_pb2_grpc, notification_data_pb2
38from couchers.proto.internal import jobs_pb2
39from couchers.servicers.api import user_model_to_pb
40from couchers.servicers.blocking import is_not_visible
41from couchers.servicers.threads import thread_to_pb
42from couchers.sql import users_visible, where_moderated_content_visible, where_users_column_visible
43from couchers.tasks import send_event_community_invite_request_email
44from couchers.utils import (
45 Timestamp_from_datetime,
46 create_coordinate,
47 dt_from_millis,
48 millis_from_dt,
49 not_none,
50 now,
51 to_aware_datetime,
52)
54logger = logging.getLogger(__name__)
56attendancestate2sql = {
57 events_pb2.AttendanceState.ATTENDANCE_STATE_NOT_GOING: None,
58 events_pb2.AttendanceState.ATTENDANCE_STATE_GOING: AttendeeStatus.going,
59}
61attendancestate2api = {
62 None: events_pb2.AttendanceState.ATTENDANCE_STATE_NOT_GOING,
63 AttendeeStatus.going: events_pb2.AttendanceState.ATTENDANCE_STATE_GOING,
64}
66MAX_PAGINATION_LENGTH = 25
69def _is_event_owner(event: Event, user_id: int) -> bool:
70 """
71 Checks whether the user can act as an owner of the event
72 """
73 if event.owner_user:
74 return event.owner_user_id == user_id
75 # otherwise owned by a cluster
76 return not_none(event.owner_cluster).admins.where(User.id == user_id).one_or_none() is not None
79def _is_event_organizer(event: Event, user_id: int) -> bool:
80 """
81 Checks whether the user is as an organizer of the event
82 """
83 return event.organizers.where(EventOrganizer.user_id == user_id).one_or_none() is not None
86def _can_moderate_event(session: Session, event: Event, user_id: int) -> bool:
87 # if the event is owned by a cluster, then any moderator of that cluster can moderate this event
88 if event.owner_cluster is not None and can_moderate_node(session, user_id, event.owner_cluster.parent_node_id):
89 return True
91 # finally check if the user can moderate the parent node of the cluster
92 return can_moderate_node(session, user_id, event.parent_node_id)
95def _can_edit_event(session: Session, event: Event, user_id: int) -> bool:
96 return (
97 _is_event_owner(event, user_id)
98 or _is_event_organizer(event, user_id)
99 or _can_moderate_event(session, event, user_id)
100 )
103def event_to_pb(session: Session, occurrence: EventOccurrence, context: CouchersContext) -> events_pb2.Event:
104 event = occurrence.event
106 next_occurrence = (
107 event.occurrences.where(EventOccurrence.end_time >= now())
108 .order_by(EventOccurrence.end_time.asc())
109 .limit(1)
110 .one_or_none()
111 )
113 owner_community_id = None
114 owner_group_id = None
115 if event.owner_cluster:
116 if event.owner_cluster.is_official_cluster:
117 owner_community_id = event.owner_cluster.parent_node_id
118 else:
119 owner_group_id = event.owner_cluster.id
121 attendance = occurrence.attendances.where(EventOccurrenceAttendee.user_id == context.user_id).one_or_none()
122 attendance_state = attendance.attendee_status if attendance else None
124 can_moderate = _can_moderate_event(session, event, context.user_id)
125 can_edit = _can_edit_event(session, event, context.user_id)
127 going_count = session.execute(
128 where_users_column_visible(
129 select(func.count())
130 .select_from(EventOccurrenceAttendee)
131 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id)
132 .where(EventOccurrenceAttendee.attendee_status == AttendeeStatus.going),
133 context,
134 EventOccurrenceAttendee.user_id,
135 )
136 ).scalar_one()
137 organizer_count = session.execute(
138 where_users_column_visible(
139 select(func.count()).select_from(EventOrganizer).where(EventOrganizer.event_id == event.id),
140 context,
141 EventOrganizer.user_id,
142 )
143 ).scalar_one()
144 subscriber_count = session.execute(
145 where_users_column_visible(
146 select(func.count()).select_from(EventSubscription).where(EventSubscription.event_id == event.id),
147 context,
148 EventSubscription.user_id,
149 )
150 ).scalar_one()
152 return events_pb2.Event(
153 event_id=occurrence.id,
154 is_next=False if not next_occurrence else occurrence.id == next_occurrence.id,
155 is_cancelled=occurrence.is_cancelled,
156 is_deleted=occurrence.is_deleted,
157 title=event.title,
158 slug=event.slug,
159 content=occurrence.content,
160 photo_url=occurrence.photo.full_url if occurrence.photo else None,
161 photo_key=occurrence.photo_key or "",
162 location=events_pb2.EventLocation(
163 lat=occurrence.coordinates[0], lng=occurrence.coordinates[1], address=occurrence.address
164 ),
165 created=Timestamp_from_datetime(occurrence.created),
166 last_edited=Timestamp_from_datetime(occurrence.last_edited),
167 creator_user_id=occurrence.creator_user_id,
168 start_time=Timestamp_from_datetime(occurrence.start_time),
169 end_time=Timestamp_from_datetime(occurrence.end_time),
170 timezone=occurrence.timezone,
171 attendance_state=attendancestate2api[attendance_state],
172 organizer=event.organizers.where(EventOrganizer.user_id == context.user_id).one_or_none() is not None,
173 subscriber=event.subscribers.where(EventSubscription.user_id == context.user_id).one_or_none() is not None,
174 going_count=going_count,
175 organizer_count=organizer_count,
176 subscriber_count=subscriber_count,
177 owner_user_id=event.owner_user_id,
178 owner_community_id=owner_community_id,
179 owner_group_id=owner_group_id,
180 thread=thread_to_pb(session, context, event.thread_id),
181 can_edit=can_edit,
182 can_moderate=can_moderate,
183 )
186def _get_event_and_occurrence_query(
187 occurrence_id: int,
188 include_deleted: bool,
189 context: CouchersContext | None = None,
190) -> Select[tuple[Event, EventOccurrence]]:
191 query = (
192 select(Event, EventOccurrence)
193 .where(EventOccurrence.id == occurrence_id)
194 .where(EventOccurrence.event_id == Event.id)
195 )
197 if not include_deleted: 197 ↛ 200line 197 didn't jump to line 200 because the condition on line 197 was always true
198 query = query.where(~EventOccurrence.is_deleted)
200 if context is not None:
201 query = where_moderated_content_visible(query, context, EventOccurrence, is_list_operation=False)
203 return query
206def _get_event_and_occurrence_one(
207 session: Session, occurrence_id: int, include_deleted: bool = False
208) -> tuple[Event, EventOccurrence]:
209 """For background jobs only - no visibility filtering."""
210 result = session.execute(_get_event_and_occurrence_query(occurrence_id, include_deleted)).one()
211 return result._tuple()
214def _get_event_and_occurrence_one_or_none(
215 session: Session, occurrence_id: int, context: CouchersContext, include_deleted: bool = False
216) -> tuple[Event, EventOccurrence] | None:
217 result = session.execute(
218 _get_event_and_occurrence_query(occurrence_id, include_deleted, context=context)
219 ).one_or_none()
220 return result._tuple() if result else None
223def _check_occurrence_time_validity(start_time: datetime, end_time: datetime, context: CouchersContext) -> None:
224 if start_time < now():
225 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "event_in_past")
226 if end_time < start_time:
227 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "event_ends_before_starts")
228 if end_time - start_time > timedelta(days=7):
229 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "event_too_long")
230 if start_time - now() > timedelta(days=365):
231 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "event_too_far_in_future")
234def get_users_to_notify_for_new_event(session: Session, occurrence: EventOccurrence) -> tuple[list[User], int | None]:
235 """
236 Returns the users to notify, as well as the community id that is being notified (None if based on geo search)
237 """
238 cluster = occurrence.event.parent_node.official_cluster
239 if occurrence.event.parent_node.node_type.value <= NodeType.region.value:
240 logger.info("Global, macroregion, and region communities are too big for email notifications.")
241 return [], occurrence.event.parent_node_id
242 elif occurrence.creator_user in cluster.admins or cluster.is_leaf: 242 ↛ 245line 242 didn't jump to line 245 because the condition on line 242 was always true
243 return list(cluster.members.where(User.is_visible)), occurrence.event.parent_node_id
244 else:
245 max_radius = 20000 # m
246 users = (
247 session.execute(
248 select(User)
249 .join(ClusterSubscription, ClusterSubscription.user_id == User.id)
250 .where(User.is_visible)
251 .where(ClusterSubscription.cluster_id == cluster.id)
252 .where(func.ST_DWithin(User.geom, occurrence.geom, max_radius / 111111))
253 )
254 .scalars()
255 .all()
256 )
257 return cast(tuple[list[User], int | None], (users, None))
260def generate_event_create_notifications(payload: jobs_pb2.GenerateEventCreateNotificationsPayload) -> None:
261 """
262 Background job to generated/fan out event notifications
263 """
264 # Import here to avoid circular dependency
265 from couchers.servicers.communities import community_to_pb # noqa: PLC0415
267 logger.info(f"Fanning out notifications for event occurrence id = {payload.occurrence_id}")
269 with session_scope() as session:
270 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id)
271 creator = occurrence.creator_user
273 users, node_id = get_users_to_notify_for_new_event(session, occurrence)
275 inviting_user = session.execute(select(User).where(User.id == payload.inviting_user_id)).scalar_one_or_none()
277 if not inviting_user: 277 ↛ 278line 277 didn't jump to line 278 because the condition on line 277 was never true
278 logger.error(f"Inviting user {payload.inviting_user_id} is gone while trying to send event notification?")
279 return
281 for user in users:
282 if is_not_visible(session, user.id, creator.id): 282 ↛ 283line 282 didn't jump to line 283 because the condition on line 282 was never true
283 continue
284 context = make_notification_user_context(user_id=user.id)
285 topic_action = (
286 NotificationTopicAction.event__create_approved
287 if payload.approved
288 else NotificationTopicAction.event__create_any
289 )
290 notify(
291 session,
292 user_id=user.id,
293 topic_action=topic_action,
294 key=str(payload.occurrence_id),
295 data=notification_data_pb2.EventCreate(
296 event=event_to_pb(session, occurrence, context),
297 inviting_user=user_model_to_pb(inviting_user, session, context),
298 nearby=True if node_id is None else None,
299 in_community=community_to_pb(session, event.parent_node, context) if node_id is not None else None,
300 ),
301 moderation_state_id=occurrence.moderation_state_id,
302 )
305def generate_event_update_notifications(payload: jobs_pb2.GenerateEventUpdateNotificationsPayload) -> None:
306 with session_scope() as session:
307 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id)
309 updating_user = session.execute(select(User).where(User.id == payload.updating_user_id)).scalar_one()
311 subscribed_user_ids = [user.id for user in event.subscribers]
312 attending_user_ids = [user.user_id for user in occurrence.attendances]
314 for user_id in set(subscribed_user_ids + attending_user_ids):
315 if is_not_visible(session, user_id, updating_user.id): 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true
316 continue
317 context = make_notification_user_context(user_id=user_id)
318 notify(
319 session,
320 user_id=user_id,
321 topic_action=NotificationTopicAction.event__update,
322 key=str(payload.occurrence_id),
323 data=notification_data_pb2.EventUpdate(
324 event=event_to_pb(session, occurrence, context),
325 updating_user=user_model_to_pb(updating_user, session, context),
326 # TODO(#9117): Remove update_str_items once known unused.
327 updated_str_items=payload.updated_str_items,
328 updated_enum_items=(
329 notification_data_pb2.EventUpdateItem.ValueType(value) for value in payload.updated_enum_items
330 ),
331 ),
332 moderation_state_id=occurrence.moderation_state_id,
333 )
336def generate_event_cancel_notifications(payload: jobs_pb2.GenerateEventCancelNotificationsPayload) -> None:
337 with session_scope() as session:
338 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id)
340 cancelling_user = session.execute(select(User).where(User.id == payload.cancelling_user_id)).scalar_one()
342 subscribed_user_ids = [user.id for user in event.subscribers]
343 attending_user_ids = [user.user_id for user in occurrence.attendances]
345 for user_id in set(subscribed_user_ids + attending_user_ids):
346 if is_not_visible(session, user_id, cancelling_user.id): 346 ↛ 347line 346 didn't jump to line 347 because the condition on line 346 was never true
347 continue
348 context = make_notification_user_context(user_id=user_id)
349 notify(
350 session,
351 user_id=user_id,
352 topic_action=NotificationTopicAction.event__cancel,
353 key=str(payload.occurrence_id),
354 data=notification_data_pb2.EventCancel(
355 event=event_to_pb(session, occurrence, context),
356 cancelling_user=user_model_to_pb(cancelling_user, session, context),
357 ),
358 moderation_state_id=occurrence.moderation_state_id,
359 )
362def generate_event_delete_notifications(payload: jobs_pb2.GenerateEventDeleteNotificationsPayload) -> None:
363 with session_scope() as session:
364 event, occurrence = _get_event_and_occurrence_one(
365 session, occurrence_id=payload.occurrence_id, include_deleted=True
366 )
368 subscribed_user_ids = [user.id for user in event.subscribers]
369 attending_user_ids = [user.user_id for user in occurrence.attendances]
371 for user_id in set(subscribed_user_ids + attending_user_ids):
372 context = make_notification_user_context(user_id=user_id)
373 notify(
374 session,
375 user_id=user_id,
376 topic_action=NotificationTopicAction.event__delete,
377 key=str(payload.occurrence_id),
378 data=notification_data_pb2.EventDelete(
379 event=event_to_pb(session, occurrence, context),
380 ),
381 moderation_state_id=occurrence.moderation_state_id,
382 )
385class Events(events_pb2_grpc.EventsServicer):
386 def CreateEvent(
387 self, request: events_pb2.CreateEventReq, context: CouchersContext, session: Session
388 ) -> events_pb2.Event:
389 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
390 if not has_completed_profile(session, user):
391 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "incomplete_profile_create_event")
392 if not request.title:
393 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_event_title")
394 if not request.content:
395 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_event_content")
397 # As protobuf parses a missing value as 0.0, this is not a permitted event coordinate value
398 if not (
399 request.HasField("location") and request.location.address and request.location.lat and request.location.lng
400 ):
401 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_event_address_or_location")
402 if request.location.lat == 0 and request.location.lng == 0: 402 ↛ 403line 402 didn't jump to line 403 because the condition on line 402 was never true
403 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
404 geom = create_coordinate(request.location.lat, request.location.lng)
405 address = request.location.address
407 start_time = to_aware_datetime(request.start_time)
408 end_time = to_aware_datetime(request.end_time)
410 _check_occurrence_time_validity(start_time, end_time, context)
412 if request.parent_community_id:
413 parent_node = session.execute(
414 select(Node).where(Node.id == request.parent_community_id)
415 ).scalar_one_or_none()
417 if not parent_node or not parent_node.official_cluster.small_community_features_enabled: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "events_not_enabled")
419 else:
420 # parent community computed from geom
421 parent_node = get_parent_node_at_location(session, not_none(geom))
423 if not parent_node: 423 ↛ 424line 423 didn't jump to line 424 because the condition on line 423 was never true
424 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "community_not_found")
426 if (
427 request.photo_key
428 and not session.execute(select(Upload).where(Upload.key == request.photo_key)).scalar_one_or_none()
429 ):
430 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "photo_not_found")
432 thread = Thread()
433 session.add(thread)
434 session.flush()
436 event = Event(
437 title=request.title,
438 parent_node_id=parent_node.id,
439 owner_user_id=context.user_id,
440 thread_id=thread.id,
441 creator_user_id=context.user_id,
442 )
443 session.add(event)
444 session.flush()
446 occurrence: EventOccurrence | None = None
448 def create_occurrence(moderation_state_id: int) -> int:
449 nonlocal occurrence
450 occurrence = EventOccurrence(
451 event_id=event.id,
452 content=request.content,
453 geom=geom,
454 address=address,
455 photo_key=request.photo_key if request.photo_key != "" else None,
456 # timezone=timezone,
457 during=TimestamptzRange(start_time, end_time),
458 creator_user_id=context.user_id,
459 moderation_state_id=moderation_state_id,
460 )
461 session.add(occurrence)
462 session.flush()
463 return occurrence.id
465 create_moderation(
466 session=session,
467 object_type=ModerationObjectType.event_occurrence,
468 object_id=create_occurrence,
469 creator_user_id=context.user_id,
470 )
472 assert occurrence is not None
474 session.add(
475 EventOrganizer(
476 user_id=context.user_id,
477 event_id=event.id,
478 )
479 )
481 session.add(
482 EventSubscription(
483 user_id=context.user_id,
484 event_id=event.id,
485 )
486 )
488 session.add(
489 EventOccurrenceAttendee(
490 user_id=context.user_id,
491 occurrence_id=occurrence.id,
492 attendee_status=AttendeeStatus.going,
493 )
494 )
496 session.commit()
498 log_event(
499 context,
500 session,
501 "event.created",
502 {
503 "event_id": event.id,
504 "occurrence_id": occurrence.id,
505 "parent_community_id": parent_node.id,
506 "parent_community_name": parent_node.official_cluster.name,
507 },
508 )
510 if has_completed_profile(session, user): 510 ↛ 521line 510 didn't jump to line 521 because the condition on line 510 was always true
511 queue_job(
512 session,
513 job=generate_event_create_notifications,
514 payload=jobs_pb2.GenerateEventCreateNotificationsPayload(
515 inviting_user_id=user.id,
516 occurrence_id=occurrence.id,
517 approved=False,
518 ),
519 )
521 return event_to_pb(session, occurrence, context)
523 def ScheduleEvent(
524 self, request: events_pb2.ScheduleEventReq, context: CouchersContext, session: Session
525 ) -> events_pb2.Event:
526 if not request.content: 526 ↛ 527line 526 didn't jump to line 527 because the condition on line 526 was never true
527 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_event_content")
528 if not ( 528 ↛ 531line 528 didn't jump to line 531 because the condition on line 528 was never true
529 request.HasField("location") and request.location.address and request.location.lat and request.location.lng
530 ):
531 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "missing_event_address_or_location")
532 if request.location.lat == 0 and request.location.lng == 0: 532 ↛ 533line 532 didn't jump to line 533 because the condition on line 532 was never true
533 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
534 geom = create_coordinate(request.location.lat, request.location.lng)
535 address = request.location.address
537 start_time = to_aware_datetime(request.start_time)
538 end_time = to_aware_datetime(request.end_time)
540 _check_occurrence_time_validity(start_time, end_time, context)
542 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
543 if not res: 543 ↛ 544line 543 didn't jump to line 544 because the condition on line 543 was never true
544 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
546 event, occurrence = res
548 if not _can_edit_event(session, event, context.user_id): 548 ↛ 549line 548 didn't jump to line 549 because the condition on line 548 was never true
549 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_edit_permission_denied")
551 if occurrence.is_cancelled: 551 ↛ 552line 551 didn't jump to line 552 because the condition on line 551 was never true
552 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
554 if ( 554 ↛ 558line 554 didn't jump to line 558 because the condition on line 554 was never true
555 request.photo_key
556 and not session.execute(select(Upload).where(Upload.key == request.photo_key)).scalar_one_or_none()
557 ):
558 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "photo_not_found")
560 during = TimestamptzRange(start_time, end_time)
562 # && is the overlap operator for ranges
563 if (
564 session.execute(
565 select(EventOccurrence.id)
566 .where(EventOccurrence.event_id == event.id)
567 .where(EventOccurrence.during.op("&&")(during))
568 .limit(1)
569 )
570 .scalars()
571 .one_or_none()
572 is not None
573 ):
574 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_overlap")
576 new_occurrence: EventOccurrence | None = None
578 def create_occurrence(moderation_state_id: int) -> int:
579 nonlocal new_occurrence
580 new_occurrence = EventOccurrence(
581 event_id=event.id,
582 content=request.content,
583 geom=geom,
584 address=address,
585 photo_key=request.photo_key if request.photo_key != "" else None,
586 # timezone=timezone,
587 during=during,
588 creator_user_id=context.user_id,
589 moderation_state_id=moderation_state_id,
590 )
591 session.add(new_occurrence)
592 session.flush()
593 return new_occurrence.id
595 create_moderation(
596 session=session,
597 object_type=ModerationObjectType.event_occurrence,
598 object_id=create_occurrence,
599 creator_user_id=context.user_id,
600 )
602 assert new_occurrence is not None
604 session.add(
605 EventOccurrenceAttendee(
606 user_id=context.user_id,
607 occurrence_id=new_occurrence.id,
608 attendee_status=AttendeeStatus.going,
609 )
610 )
612 session.flush()
614 # TODO: notify
616 return event_to_pb(session, new_occurrence, context)
618 def UpdateEvent(
619 self, request: events_pb2.UpdateEventReq, context: CouchersContext, session: Session
620 ) -> events_pb2.Event:
621 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
622 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
623 if not res: 623 ↛ 624line 623 didn't jump to line 624 because the condition on line 623 was never true
624 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
626 event, occurrence = res
628 if not _can_edit_event(session, event, context.user_id): 628 ↛ 629line 628 didn't jump to line 629 because the condition on line 628 was never true
629 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_edit_permission_denied")
631 # the things that were updated and need to be notified about
632 notify_updated: list[notification_data_pb2.EventUpdateItem.ValueType] = []
634 if occurrence.is_cancelled:
635 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
637 occurrence_update: dict[str, Any] = {"last_edited": now()}
639 if request.HasField("title"):
640 notify_updated.append(notification_data_pb2.EventUpdateItem.EVENT_UPDATE_ITEM_TITLE)
641 event.title = request.title.value
643 if request.HasField("content"):
644 notify_updated.append(notification_data_pb2.EventUpdateItem.EVENT_UPDATE_ITEM_CONTENT)
645 occurrence_update["content"] = request.content.value
647 if request.HasField("photo_key"): 647 ↛ 648line 647 didn't jump to line 648 because the condition on line 647 was never true
648 occurrence_update["photo_key"] = request.photo_key.value
650 if request.HasField("location"):
651 notify_updated.append(notification_data_pb2.EventUpdateItem.EVENT_UPDATE_ITEM_LOCATION)
652 if request.location.lat == 0 and request.location.lng == 0: 652 ↛ 653line 652 didn't jump to line 653 because the condition on line 652 was never true
653 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "invalid_coordinate")
654 occurrence_update["geom"] = create_coordinate(request.location.lat, request.location.lng)
655 occurrence_update["address"] = request.location.address
657 if request.HasField("start_time") or request.HasField("end_time"):
658 if request.update_all_future: 658 ↛ 659line 658 didn't jump to line 659 because the condition on line 658 was never true
659 context.abort_with_error_code(grpc.StatusCode.INVALID_ARGUMENT, "event_cant_update_all_times")
660 if request.HasField("start_time"): 660 ↛ 664line 660 didn't jump to line 664 because the condition on line 660 was always true
661 notify_updated.append(notification_data_pb2.EventUpdateItem.EVENT_UPDATE_ITEM_START_TIME)
662 start_time = to_aware_datetime(request.start_time)
663 else:
664 start_time = occurrence.start_time
665 if request.HasField("end_time"):
666 notify_updated.append(notification_data_pb2.EventUpdateItem.EVENT_UPDATE_ITEM_END_TIME)
667 end_time = to_aware_datetime(request.end_time)
668 else:
669 end_time = occurrence.end_time
671 _check_occurrence_time_validity(start_time, end_time, context)
673 during = TimestamptzRange(start_time, end_time)
675 # && is the overlap operator for ranges
676 if (
677 session.execute(
678 select(EventOccurrence.id)
679 .where(EventOccurrence.event_id == event.id)
680 .where(EventOccurrence.id != occurrence.id)
681 .where(EventOccurrence.during.op("&&")(during))
682 .limit(1)
683 )
684 .scalars()
685 .one_or_none()
686 is not None
687 ):
688 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_overlap")
690 occurrence_update["during"] = during
692 # TODO
693 # if request.HasField("timezone"):
694 # occurrence_update["timezone"] = request.timezone
696 # allow editing any event which hasn't ended more than 24 hours before now
697 # when editing all future events, we edit all which have not yet ended
699 cutoff_time = now() - timedelta(hours=24)
700 if request.update_all_future:
701 session.execute(
702 update(EventOccurrence)
703 .where(EventOccurrence.end_time >= cutoff_time)
704 .where(EventOccurrence.start_time >= occurrence.start_time)
705 .values(occurrence_update)
706 .execution_options(synchronize_session=False)
707 )
708 else:
709 if occurrence.end_time < cutoff_time: 709 ↛ 710line 709 didn't jump to line 710 because the condition on line 709 was never true
710 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
711 session.execute(
712 update(EventOccurrence)
713 .where(EventOccurrence.end_time >= cutoff_time)
714 .where(EventOccurrence.id == occurrence.id)
715 .values(occurrence_update)
716 .execution_options(synchronize_session=False)
717 )
719 session.flush()
721 if notify_updated:
722 items_str = ",".join(notification_data_pb2.EventUpdateItem.Name(item) for item in notify_updated)
723 if request.should_notify:
724 logger.info(f"Items {items_str} updated in event {event.id=}, notifying")
726 queue_job(
727 session,
728 job=generate_event_update_notifications,
729 payload=jobs_pb2.GenerateEventUpdateNotificationsPayload(
730 updating_user_id=user.id,
731 occurrence_id=occurrence.id,
732 updated_enum_items=notify_updated,
733 ),
734 )
735 else:
736 logger.info(f"Items {items_str} updated in event {event.id=}, but skipping notifications")
738 # since we have synchronize_session=False, we have to refresh the object
739 session.refresh(occurrence)
741 return event_to_pb(session, occurrence, context)
743 def GetEvent(self, request: events_pb2.GetEventReq, context: CouchersContext, session: Session) -> events_pb2.Event:
744 query = select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted)
745 query = where_moderated_content_visible(query, context, EventOccurrence, is_list_operation=False)
746 occurrence = session.execute(query).scalar_one_or_none()
748 if not occurrence:
749 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
751 return event_to_pb(session, occurrence, context)
753 def CancelEvent(
754 self, request: events_pb2.CancelEventReq, context: CouchersContext, session: Session
755 ) -> empty_pb2.Empty:
756 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
757 if not res: 757 ↛ 758line 757 didn't jump to line 758 because the condition on line 757 was never true
758 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
760 event, occurrence = res
762 if not _can_edit_event(session, event, context.user_id): 762 ↛ 763line 762 didn't jump to line 763 because the condition on line 762 was never true
763 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_edit_permission_denied")
765 if occurrence.end_time < now() - timedelta(hours=24): 765 ↛ 766line 765 didn't jump to line 766 because the condition on line 765 was never true
766 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_cancel_old_event")
768 occurrence.is_cancelled = True
770 log_event(context, session, "event.cancelled", {"event_id": event.id, "occurrence_id": occurrence.id})
772 queue_job(
773 session,
774 job=generate_event_cancel_notifications,
775 payload=jobs_pb2.GenerateEventCancelNotificationsPayload(
776 cancelling_user_id=context.user_id,
777 occurrence_id=occurrence.id,
778 ),
779 )
781 return empty_pb2.Empty()
783 def RequestCommunityInvite(
784 self, request: events_pb2.RequestCommunityInviteReq, context: CouchersContext, session: Session
785 ) -> empty_pb2.Empty:
786 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
787 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
788 if not res: 788 ↛ 789line 788 didn't jump to line 789 because the condition on line 788 was never true
789 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
791 event, occurrence = res
793 if not _can_edit_event(session, event, context.user_id):
794 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_edit_permission_denied")
796 if occurrence.is_cancelled: 796 ↛ 797line 796 didn't jump to line 797 because the condition on line 796 was never true
797 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
799 if occurrence.end_time < now() - timedelta(hours=24): 799 ↛ 800line 799 didn't jump to line 800 because the condition on line 799 was never true
800 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
802 this_user_reqs = [req for req in occurrence.community_invite_requests if req.user_id == context.user_id]
804 if len(this_user_reqs) > 0:
805 context.abort_with_error_code(
806 grpc.StatusCode.FAILED_PRECONDITION, "event_community_invite_already_requested"
807 )
809 approved_reqs = [req for req in occurrence.community_invite_requests if req.approved]
811 if len(approved_reqs) > 0:
812 context.abort_with_error_code(
813 grpc.StatusCode.FAILED_PRECONDITION, "event_community_invite_already_approved"
814 )
816 req = EventCommunityInviteRequest(
817 occurrence_id=request.event_id,
818 user_id=context.user_id,
819 )
820 session.add(req)
821 session.flush()
823 send_event_community_invite_request_email(session, req)
825 return empty_pb2.Empty()
827 def ListEventOccurrences(
828 self, request: events_pb2.ListEventOccurrencesReq, context: CouchersContext, session: Session
829 ) -> events_pb2.ListEventOccurrencesRes:
830 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
831 # the page token is a unix timestamp of where we left off
832 page_token = dt_from_millis(int(request.page_token)) if request.page_token else now()
833 initial_query = (
834 select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted)
835 )
836 initial_query = where_moderated_content_visible(
837 initial_query, context, EventOccurrence, is_list_operation=False
838 )
839 occurrence = session.execute(initial_query).scalar_one_or_none()
840 if not occurrence: 840 ↛ 841line 840 didn't jump to line 841 because the condition on line 840 was never true
841 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
843 query = (
844 select(EventOccurrence)
845 .where(EventOccurrence.event_id == occurrence.event_id)
846 .where(~EventOccurrence.is_deleted)
847 )
848 query = where_moderated_content_visible(query, context, EventOccurrence, is_list_operation=True)
850 if not request.include_cancelled:
851 query = query.where(~EventOccurrence.is_cancelled)
853 if not request.past: 853 ↛ 857line 853 didn't jump to line 857 because the condition on line 853 was always true
854 cutoff = page_token - timedelta(seconds=1)
855 query = query.where(EventOccurrence.end_time > cutoff).order_by(EventOccurrence.start_time.asc())
856 else:
857 cutoff = page_token + timedelta(seconds=1)
858 query = query.where(EventOccurrence.end_time < cutoff).order_by(EventOccurrence.start_time.desc())
860 query = query.limit(page_size + 1)
861 occurrences = session.execute(query).scalars().all()
863 return events_pb2.ListEventOccurrencesRes(
864 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]],
865 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None,
866 )
868 def ListEventAttendees(
869 self, request: events_pb2.ListEventAttendeesReq, context: CouchersContext, session: Session
870 ) -> events_pb2.ListEventAttendeesRes:
871 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
872 next_user_id = int(request.page_token) if request.page_token else 0
873 occurrence = session.execute(
874 where_moderated_content_visible(
875 select(EventOccurrence)
876 .where(EventOccurrence.id == request.event_id)
877 .where(~EventOccurrence.is_deleted),
878 context,
879 EventOccurrence,
880 is_list_operation=False,
881 )
882 ).scalar_one_or_none()
883 if not occurrence: 883 ↛ 884line 883 didn't jump to line 884 because the condition on line 883 was never true
884 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
885 attendees = (
886 session.execute(
887 where_users_column_visible(
888 select(EventOccurrenceAttendee)
889 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id)
890 .where(EventOccurrenceAttendee.user_id >= next_user_id)
891 .order_by(EventOccurrenceAttendee.user_id)
892 .limit(page_size + 1),
893 context,
894 EventOccurrenceAttendee.user_id,
895 )
896 )
897 .scalars()
898 .all()
899 )
900 return events_pb2.ListEventAttendeesRes(
901 attendee_user_ids=[attendee.user_id for attendee in attendees[:page_size]],
902 next_page_token=str(attendees[-1].user_id) if len(attendees) > page_size else None,
903 )
905 def ListEventSubscribers(
906 self, request: events_pb2.ListEventSubscribersReq, context: CouchersContext, session: Session
907 ) -> events_pb2.ListEventSubscribersRes:
908 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
909 next_user_id = int(request.page_token) if request.page_token else 0
910 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
911 if not res: 911 ↛ 912line 911 didn't jump to line 912 because the condition on line 911 was never true
912 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
913 event, occurrence = res
914 subscribers = (
915 session.execute(
916 where_users_column_visible(
917 select(EventSubscription)
918 .where(EventSubscription.event_id == event.id)
919 .where(EventSubscription.user_id >= next_user_id)
920 .order_by(EventSubscription.user_id)
921 .limit(page_size + 1),
922 context,
923 EventSubscription.user_id,
924 )
925 )
926 .scalars()
927 .all()
928 )
929 return events_pb2.ListEventSubscribersRes(
930 subscriber_user_ids=[subscriber.user_id for subscriber in subscribers[:page_size]],
931 next_page_token=str(subscribers[-1].user_id) if len(subscribers) > page_size else None,
932 )
934 def ListEventOrganizers(
935 self, request: events_pb2.ListEventOrganizersReq, context: CouchersContext, session: Session
936 ) -> events_pb2.ListEventOrganizersRes:
937 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
938 next_user_id = int(request.page_token) if request.page_token else 0
939 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
940 if not res: 940 ↛ 941line 940 didn't jump to line 941 because the condition on line 940 was never true
941 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
942 event, occurrence = res
943 organizers = (
944 session.execute(
945 where_users_column_visible(
946 select(EventOrganizer)
947 .where(EventOrganizer.event_id == event.id)
948 .where(EventOrganizer.user_id >= next_user_id)
949 .order_by(EventOrganizer.user_id)
950 .limit(page_size + 1),
951 context,
952 EventOrganizer.user_id,
953 )
954 )
955 .scalars()
956 .all()
957 )
958 return events_pb2.ListEventOrganizersRes(
959 organizer_user_ids=[organizer.user_id for organizer in organizers[:page_size]],
960 next_page_token=str(organizers[-1].user_id) if len(organizers) > page_size else None,
961 )
963 def TransferEvent(
964 self, request: events_pb2.TransferEventReq, context: CouchersContext, session: Session
965 ) -> events_pb2.Event:
966 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
967 if not res: 967 ↛ 968line 967 didn't jump to line 968 because the condition on line 967 was never true
968 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
970 event, occurrence = res
972 if not _can_edit_event(session, event, context.user_id):
973 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_transfer_permission_denied")
975 if occurrence.is_cancelled:
976 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
978 if occurrence.end_time < now() - timedelta(hours=24): 978 ↛ 979line 978 didn't jump to line 979 because the condition on line 978 was never true
979 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
981 if request.WhichOneof("new_owner") == "new_owner_group_id":
982 cluster = session.execute(
983 select(Cluster).where(~Cluster.is_official_cluster).where(Cluster.id == request.new_owner_group_id)
984 ).scalar_one_or_none()
985 elif request.WhichOneof("new_owner") == "new_owner_community_id": 985 ↛ 992line 985 didn't jump to line 992 because the condition on line 985 was always true
986 cluster = session.execute(
987 select(Cluster)
988 .where(Cluster.parent_node_id == request.new_owner_community_id)
989 .where(Cluster.is_official_cluster)
990 ).scalar_one_or_none()
992 if not cluster: 992 ↛ 993line 992 didn't jump to line 993 because the condition on line 992 was never true
993 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "group_or_community_not_found")
995 event.owner_user = None
996 event.owner_cluster = cluster
998 session.commit()
999 return event_to_pb(session, occurrence, context)
1001 def SetEventSubscription(
1002 self, request: events_pb2.SetEventSubscriptionReq, context: CouchersContext, session: Session
1003 ) -> events_pb2.Event:
1004 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
1005 if not res: 1005 ↛ 1006line 1005 didn't jump to line 1006 because the condition on line 1005 was never true
1006 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
1008 event, occurrence = res
1010 if occurrence.is_cancelled:
1011 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
1013 if occurrence.end_time < now() - timedelta(hours=24): 1013 ↛ 1014line 1013 didn't jump to line 1014 because the condition on line 1013 was never true
1014 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
1016 current_subscription = session.execute(
1017 select(EventSubscription)
1018 .where(EventSubscription.user_id == context.user_id)
1019 .where(EventSubscription.event_id == event.id)
1020 ).scalar_one_or_none()
1022 # if not subscribed, subscribe
1023 if request.subscribe and not current_subscription:
1024 session.add(EventSubscription(user_id=context.user_id, event_id=event.id))
1026 # if subscribed but unsubbing, remove subscription
1027 if not request.subscribe and current_subscription:
1028 session.delete(current_subscription)
1030 session.flush()
1032 log_event(
1033 context,
1034 session,
1035 "event.subscription_set",
1036 {"event_id": event.id, "occurrence_id": occurrence.id, "subscribed": request.subscribe},
1037 )
1039 return event_to_pb(session, occurrence, context)
1041 def SetEventAttendance(
1042 self, request: events_pb2.SetEventAttendanceReq, context: CouchersContext, session: Session
1043 ) -> events_pb2.Event:
1044 occurrence = session.execute(
1045 where_moderated_content_visible(
1046 select(EventOccurrence)
1047 .where(EventOccurrence.id == request.event_id)
1048 .where(~EventOccurrence.is_deleted),
1049 context,
1050 EventOccurrence,
1051 is_list_operation=False,
1052 )
1053 ).scalar_one_or_none()
1055 if not occurrence: 1055 ↛ 1056line 1055 didn't jump to line 1056 because the condition on line 1055 was never true
1056 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
1058 if occurrence.is_cancelled:
1059 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
1061 if occurrence.end_time < now() - timedelta(hours=24): 1061 ↛ 1062line 1061 didn't jump to line 1062 because the condition on line 1061 was never true
1062 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
1064 current_attendance = session.execute(
1065 select(EventOccurrenceAttendee)
1066 .where(EventOccurrenceAttendee.user_id == context.user_id)
1067 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id)
1068 ).scalar_one_or_none()
1070 if request.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING:
1071 if current_attendance: 1071 ↛ 1086line 1071 didn't jump to line 1086 because the condition on line 1071 was always true
1072 session.delete(current_attendance)
1073 # if unset/not going, nothing to do!
1074 else:
1075 if current_attendance: 1075 ↛ 1076line 1075 didn't jump to line 1076 because the condition on line 1075 was never true
1076 current_attendance.attendee_status = attendancestate2sql[request.attendance_state] # type: ignore[assignment]
1077 else:
1078 # create new
1079 attendance = EventOccurrenceAttendee(
1080 user_id=context.user_id,
1081 occurrence_id=occurrence.id,
1082 attendee_status=not_none(attendancestate2sql[request.attendance_state]),
1083 )
1084 session.add(attendance)
1086 session.flush()
1088 log_event(
1089 context,
1090 session,
1091 "event.attendance_set",
1092 {"occurrence_id": occurrence.id, "attendance_state": request.attendance_state},
1093 )
1095 return event_to_pb(session, occurrence, context)
1097 def ListMyEvents(
1098 self, request: events_pb2.ListMyEventsReq, context: CouchersContext, session: Session
1099 ) -> events_pb2.ListMyEventsRes:
1100 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
1101 # the page token is a unix timestamp of where we left off
1102 page_token = (
1103 dt_from_millis(int(request.page_token)) if request.page_token and not request.page_number else now()
1104 )
1105 # the page number is the page number we are on
1106 page_number = request.page_number or 1
1107 # Calculate the offset for pagination
1108 offset = (page_number - 1) * page_size
1109 query = (
1110 select(EventOccurrence).join(Event, Event.id == EventOccurrence.event_id).where(~EventOccurrence.is_deleted)
1111 )
1112 query = where_moderated_content_visible(query, context, EventOccurrence, is_list_operation=True)
1114 include_all = not (request.subscribed or request.attending or request.organizing or request.my_communities)
1115 include_subscribed = request.subscribed or include_all
1116 include_organizing = request.organizing or include_all
1117 include_attending = request.attending or include_all
1118 include_my_communities = request.my_communities or include_all
1120 if include_attending and request.exclude_attending:
1121 context.abort_with_error_code(
1122 grpc.StatusCode.INVALID_ARGUMENT, "cannot_combine_attending_and_exclude_attending"
1123 )
1125 where_ = []
1127 if include_subscribed:
1128 query = query.outerjoin(
1129 EventSubscription,
1130 and_(EventSubscription.event_id == Event.id, EventSubscription.user_id == context.user_id),
1131 )
1132 where_.append(EventSubscription.user_id != None)
1133 if include_organizing:
1134 query = query.outerjoin(
1135 EventOrganizer, and_(EventOrganizer.event_id == Event.id, EventOrganizer.user_id == context.user_id)
1136 )
1137 where_.append(EventOrganizer.user_id != None)
1138 if include_attending or request.exclude_attending:
1139 query = query.outerjoin(
1140 EventOccurrenceAttendee,
1141 and_(
1142 EventOccurrenceAttendee.occurrence_id == EventOccurrence.id,
1143 EventOccurrenceAttendee.user_id == context.user_id,
1144 ),
1145 )
1146 if include_attending:
1147 where_.append(EventOccurrenceAttendee.user_id != None)
1148 elif request.exclude_attending: 1148 ↛ 1155line 1148 didn't jump to line 1155 because the condition on line 1148 was always true
1149 if not include_organizing: 1149 ↛ 1154line 1149 didn't jump to line 1154 because the condition on line 1149 was always true
1150 query = query.outerjoin(
1151 EventOrganizer,
1152 and_(EventOrganizer.event_id == Event.id, EventOrganizer.user_id == context.user_id),
1153 )
1154 query = query.where(EventOccurrenceAttendee.user_id == None, EventOrganizer.user_id == None)
1155 if include_my_communities:
1156 my_communities = (
1157 session.execute(
1158 select(Node.id)
1159 .join(Cluster, Cluster.parent_node_id == Node.id)
1160 .join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id)
1161 .where(ClusterSubscription.user_id == context.user_id)
1162 .where(Cluster.is_official_cluster)
1163 .order_by(Node.id)
1164 .limit(100000)
1165 )
1166 .scalars()
1167 .all()
1168 )
1169 where_.append(Event.parent_node_id.in_(my_communities))
1171 query = query.where(or_(*where_))
1173 if request.my_communities_exclude_global:
1174 query = query.join(Node, Node.id == Event.parent_node_id).where(Node.node_type > NodeType.region)
1176 if not request.include_cancelled:
1177 query = query.where(~EventOccurrence.is_cancelled)
1179 if not request.past: 1179 ↛ 1183line 1179 didn't jump to line 1183 because the condition on line 1179 was always true
1180 cutoff = page_token - timedelta(seconds=1)
1181 query = query.where(EventOccurrence.end_time > cutoff).order_by(EventOccurrence.start_time.asc())
1182 else:
1183 cutoff = page_token + timedelta(seconds=1)
1184 query = query.where(EventOccurrence.end_time < cutoff).order_by(EventOccurrence.start_time.desc())
1185 # Count the total number of items for pagination
1186 total_items = session.execute(select(func.count()).select_from(query.subquery())).scalar()
1187 # Apply pagination by page number
1188 query = query.offset(offset).limit(page_size) if request.page_number else query.limit(page_size + 1)
1189 occurrences = session.execute(query).scalars().all()
1191 return events_pb2.ListMyEventsRes(
1192 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]],
1193 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None,
1194 total_items=total_items,
1195 )
1197 def ListAllEvents(
1198 self, request: events_pb2.ListAllEventsReq, context: CouchersContext, session: Session
1199 ) -> events_pb2.ListAllEventsRes:
1200 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH)
1201 # the page token is a unix timestamp of where we left off
1202 page_token = dt_from_millis(int(request.page_token)) if request.page_token else now()
1204 query = select(EventOccurrence).where(~EventOccurrence.is_deleted)
1205 query = where_moderated_content_visible(query, context, EventOccurrence, is_list_operation=True)
1207 if not request.include_cancelled: 1207 ↛ 1210line 1207 didn't jump to line 1210 because the condition on line 1207 was always true
1208 query = query.where(~EventOccurrence.is_cancelled)
1210 if not request.past:
1211 cutoff = page_token - timedelta(seconds=1)
1212 query = query.where(EventOccurrence.end_time > cutoff).order_by(EventOccurrence.start_time.asc())
1213 else:
1214 cutoff = page_token + timedelta(seconds=1)
1215 query = query.where(EventOccurrence.end_time < cutoff).order_by(EventOccurrence.start_time.desc())
1217 query = query.limit(page_size + 1)
1218 occurrences = session.execute(query).scalars().all()
1220 return events_pb2.ListAllEventsRes(
1221 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]],
1222 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None,
1223 )
1225 def InviteEventOrganizer(
1226 self, request: events_pb2.InviteEventOrganizerReq, context: CouchersContext, session: Session
1227 ) -> empty_pb2.Empty:
1228 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one()
1229 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
1230 if not res: 1230 ↛ 1231line 1230 didn't jump to line 1231 because the condition on line 1230 was never true
1231 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
1233 event, occurrence = res
1235 if not _can_edit_event(session, event, context.user_id):
1236 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_edit_permission_denied")
1238 if occurrence.is_cancelled:
1239 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
1241 if occurrence.end_time < now() - timedelta(hours=24): 1241 ↛ 1242line 1241 didn't jump to line 1242 because the condition on line 1241 was never true
1242 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
1244 if not session.execute( 1244 ↛ 1247line 1244 didn't jump to line 1247 because the condition on line 1244 was never true
1245 select(User).where(users_visible(context)).where(User.id == request.user_id)
1246 ).scalar_one_or_none():
1247 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "user_not_found")
1249 session.add(
1250 EventOrganizer(
1251 user_id=request.user_id,
1252 event_id=event.id,
1253 )
1254 )
1255 session.flush()
1257 other_user_context = make_notification_user_context(user_id=request.user_id)
1259 notify(
1260 session,
1261 user_id=request.user_id,
1262 topic_action=NotificationTopicAction.event__invite_organizer,
1263 key=str(event.id),
1264 data=notification_data_pb2.EventInviteOrganizer(
1265 event=event_to_pb(session, occurrence, other_user_context),
1266 inviting_user=user_model_to_pb(user, session, other_user_context),
1267 ),
1268 )
1270 return empty_pb2.Empty()
1272 def RemoveEventOrganizer(
1273 self, request: events_pb2.RemoveEventOrganizerReq, context: CouchersContext, session: Session
1274 ) -> empty_pb2.Empty:
1275 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id, context=context)
1276 if not res: 1276 ↛ 1277line 1276 didn't jump to line 1277 because the condition on line 1276 was never true
1277 context.abort_with_error_code(grpc.StatusCode.NOT_FOUND, "event_not_found")
1279 event, occurrence = res
1281 if occurrence.is_cancelled: 1281 ↛ 1282line 1281 didn't jump to line 1282 because the condition on line 1281 was never true
1282 context.abort_with_error_code(grpc.StatusCode.PERMISSION_DENIED, "event_cant_update_cancelled_event")
1284 if occurrence.end_time < now() - timedelta(hours=24): 1284 ↛ 1285line 1284 didn't jump to line 1285 because the condition on line 1284 was never true
1285 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_update_old_event")
1287 # Determine which user to remove
1288 user_id_to_remove = request.user_id.value if request.HasField("user_id") else context.user_id
1290 # Check if the target user is the event owner (only after permission check)
1291 if event.owner_user_id == user_id_to_remove:
1292 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_cant_remove_owner_as_organizer")
1294 # Check permissions: either an organizer removing an organizer OR you're the event owner
1295 if not _can_edit_event(session, event, context.user_id):
1296 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_edit_permission_denied")
1298 # Find the organizer to remove
1299 organizer_to_remove = session.execute(
1300 select(EventOrganizer)
1301 .where(EventOrganizer.user_id == user_id_to_remove)
1302 .where(EventOrganizer.event_id == event.id)
1303 ).scalar_one_or_none()
1305 if not organizer_to_remove: 1305 ↛ 1306line 1305 didn't jump to line 1306 because the condition on line 1305 was never true
1306 context.abort_with_error_code(grpc.StatusCode.FAILED_PRECONDITION, "event_not_an_organizer")
1308 session.delete(organizer_to_remove)
1310 return empty_pb2.Empty()