Coverage for src/couchers/servicers/events.py: 83%

504 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-07-22 17:19 +0000

1import logging 

2from datetime import timedelta 

3from types import SimpleNamespace 

4 

5import grpc 

6from google.protobuf import empty_pb2 

7from psycopg2.extras import DateTimeTZRange 

8from sqlalchemy.sql import and_, func, or_, select, update 

9 

10from couchers import errors 

11from couchers.db import can_moderate_node, get_parent_node_at_location, session_scope 

12from couchers.jobs.enqueue import queue_job 

13from couchers.models import ( 

14 AttendeeStatus, 

15 Cluster, 

16 ClusterSubscription, 

17 Event, 

18 EventCommunityInviteRequest, 

19 EventOccurrence, 

20 EventOccurrenceAttendee, 

21 EventOrganizer, 

22 EventSubscription, 

23 Node, 

24 Thread, 

25 Upload, 

26 User, 

27) 

28from couchers.notifications.notify import notify 

29from couchers.servicers.api import user_model_to_pb 

30from couchers.servicers.blocking import are_blocked 

31from couchers.servicers.threads import thread_to_pb 

32from couchers.sql import couchers_select as select 

33from couchers.tasks import send_event_community_invite_request_email 

34from couchers.utils import ( 

35 Timestamp_from_datetime, 

36 create_coordinate, 

37 dt_from_millis, 

38 millis_from_dt, 

39 now, 

40 to_aware_datetime, 

41) 

42from proto import events_pb2, events_pb2_grpc, notification_data_pb2 

43from proto.internal import jobs_pb2 

44 

45logger = logging.getLogger(__name__) 

46 

47attendancestate2sql = { 

48 events_pb2.AttendanceState.ATTENDANCE_STATE_NOT_GOING: None, 

49 events_pb2.AttendanceState.ATTENDANCE_STATE_GOING: AttendeeStatus.going, 

50 events_pb2.AttendanceState.ATTENDANCE_STATE_MAYBE: AttendeeStatus.maybe, 

51} 

52 

53attendancestate2api = { 

54 None: events_pb2.AttendanceState.ATTENDANCE_STATE_NOT_GOING, 

55 AttendeeStatus.going: events_pb2.AttendanceState.ATTENDANCE_STATE_GOING, 

56 AttendeeStatus.maybe: events_pb2.AttendanceState.ATTENDANCE_STATE_MAYBE, 

57} 

58 

59MAX_PAGINATION_LENGTH = 25 

60 

61 

62def _is_event_owner(event: Event, user_id): 

63 """ 

64 Checks whether the user can act as an owner of the event 

65 """ 

66 if event.owner_user: 

67 return event.owner_user_id == user_id 

68 # otherwise owned by a cluster 

69 return event.owner_cluster.admins.where(User.id == user_id).one_or_none() is not None 

70 

71 

72def _can_moderate_event(session, event: Event, user_id): 

73 # if the event is owned by a cluster, then any moderator of that cluster can moderate this event 

74 if event.owner_cluster is not None and can_moderate_node(session, user_id, event.owner_cluster.parent_node_id): 

75 return True 

76 

77 # finally check if the user can moderate the parent node of the cluster 

78 return can_moderate_node(session, user_id, event.parent_node_id) 

79 

80 

81def _can_edit_event(session, event, user_id): 

82 return _is_event_owner(event, user_id) or _can_moderate_event(session, event, user_id) 

83 

84 

85def event_to_pb(session, occurrence: EventOccurrence, context): 

86 event = occurrence.event 

87 

88 next_occurrence = ( 

89 event.occurrences.where(EventOccurrence.end_time >= now()).order_by(EventOccurrence.end_time.asc()).first() 

90 ) 

91 

92 owner_community_id = None 

93 owner_group_id = None 

94 if event.owner_cluster: 

95 if event.owner_cluster.is_official_cluster: 

96 owner_community_id = event.owner_cluster.parent_node_id 

97 else: 

98 owner_group_id = event.owner_cluster.id 

99 

100 attendance = occurrence.attendances.where(EventOccurrenceAttendee.user_id == context.user_id).one_or_none() 

101 attendance_state = attendance.attendee_status if attendance else None 

102 

103 can_moderate = _can_moderate_event(session, event, context.user_id) 

104 can_edit = _can_edit_event(session, event, context.user_id) 

105 

106 going_count = session.execute( 

107 select(func.count()) 

108 .select_from(EventOccurrenceAttendee) 

109 .where_users_column_visible(context, EventOccurrenceAttendee.user_id) 

110 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id) 

111 .where(EventOccurrenceAttendee.attendee_status == AttendeeStatus.going) 

112 ).scalar_one() 

113 maybe_count = session.execute( 

114 select(func.count()) 

115 .select_from(EventOccurrenceAttendee) 

116 .where_users_column_visible(context, EventOccurrenceAttendee.user_id) 

117 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id) 

118 .where(EventOccurrenceAttendee.attendee_status == AttendeeStatus.maybe) 

119 ).scalar_one() 

120 

121 organizer_count = session.execute( 

122 select(func.count()) 

123 .select_from(EventOrganizer) 

124 .where_users_column_visible(context, EventOrganizer.user_id) 

125 .where(EventOrganizer.event_id == event.id) 

126 ).scalar_one() 

127 subscriber_count = session.execute( 

128 select(func.count()) 

129 .select_from(EventSubscription) 

130 .where_users_column_visible(context, EventSubscription.user_id) 

131 .where(EventSubscription.event_id == event.id) 

132 ).scalar_one() 

133 

134 return events_pb2.Event( 

135 event_id=occurrence.id, 

136 is_next=False if not next_occurrence else occurrence.id == next_occurrence.id, 

137 is_cancelled=occurrence.is_cancelled, 

138 is_deleted=occurrence.is_deleted, 

139 title=event.title, 

140 slug=event.slug, 

141 content=occurrence.content, 

142 photo_url=occurrence.photo.full_url if occurrence.photo else None, 

143 online_information=( 

144 events_pb2.OnlineEventInformation( 

145 link=occurrence.link, 

146 ) 

147 if occurrence.link 

148 else None 

149 ), 

150 offline_information=( 

151 events_pb2.OfflineEventInformation( 

152 lat=occurrence.coordinates[0], 

153 lng=occurrence.coordinates[1], 

154 address=occurrence.address, 

155 ) 

156 if occurrence.geom 

157 else None 

158 ), 

159 created=Timestamp_from_datetime(occurrence.created), 

160 last_edited=Timestamp_from_datetime(occurrence.last_edited), 

161 creator_user_id=occurrence.creator_user_id, 

162 start_time=Timestamp_from_datetime(occurrence.start_time), 

163 end_time=Timestamp_from_datetime(occurrence.end_time), 

164 timezone=occurrence.timezone, 

165 start_time_display=str(occurrence.start_time), 

166 end_time_display=str(occurrence.end_time), 

167 attendance_state=attendancestate2api[attendance_state], 

168 organizer=event.organizers.where(EventOrganizer.user_id == context.user_id).one_or_none() is not None, 

169 subscriber=event.subscribers.where(EventSubscription.user_id == context.user_id).one_or_none() is not None, 

170 going_count=going_count, 

171 maybe_count=maybe_count, 

172 organizer_count=organizer_count, 

173 subscriber_count=subscriber_count, 

174 owner_user_id=event.owner_user_id, 

175 owner_community_id=owner_community_id, 

176 owner_group_id=owner_group_id, 

177 thread=thread_to_pb(event.thread_id), 

178 can_edit=can_edit, 

179 can_moderate=can_moderate, 

180 ) 

181 

182 

183def _get_event_and_occurrence_query(occurrence_id, include_deleted: bool): 

184 query = ( 

185 select(Event, EventOccurrence) 

186 .where(EventOccurrence.id == occurrence_id) 

187 .where(EventOccurrence.event_id == Event.id) 

188 ) 

189 

190 if not include_deleted: 

191 query = query.where(~EventOccurrence.is_deleted) 

192 

193 return query 

194 

195 

196def _get_event_and_occurrence_one( 

197 session, occurrence_id, include_deleted: bool = False 

198) -> tuple[Event, EventOccurrence]: 

199 return session.execute(_get_event_and_occurrence_query(occurrence_id, include_deleted)).one() 

200 

201 

202def _get_event_and_occurrence_one_or_none( 

203 session, occurrence_id, include_deleted: bool = False 

204) -> tuple[Event, EventOccurrence] | None: 

205 return session.execute(_get_event_and_occurrence_query(occurrence_id, include_deleted)).one_or_none() 

206 

207 

208def _check_occurrence_time_validity(start_time, end_time, context): 

209 if start_time < now(): 

210 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.EVENT_IN_PAST) 

211 if end_time < start_time: 

212 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.EVENT_ENDS_BEFORE_STARTS) 

213 if end_time - start_time > timedelta(days=7): 

214 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.EVENT_TOO_LONG) 

215 if start_time - now() > timedelta(days=365): 

216 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.EVENT_TOO_FAR_IN_FUTURE) 

217 

218 

219def get_users_to_notify_for_new_event(session, occurrence): 

220 """ 

221 Returns the users to notify, as well as the community id that is being notified (None if based on geo search) 

222 """ 

223 cluster = occurrence.event.parent_node.official_cluster 

224 if cluster.parent_node_id == 1: 

225 logger.info("The Global Community is too big for email notifications.") 

226 return [], occurrence.event.parent_node_id 

227 elif occurrence.creator_user in cluster.admins or cluster.is_leaf: 

228 return list(cluster.members.where(User.is_visible)), occurrence.event.parent_node_id 

229 else: 

230 max_radius = 20000 # m 

231 users = ( 

232 session.execute( 

233 select(User) 

234 .join(ClusterSubscription, ClusterSubscription.user_id == User.id) 

235 .where(User.is_visible) 

236 .where(ClusterSubscription.cluster_id == cluster.id) 

237 .where(func.ST_DWithin(User.geom, occurrence.geom, max_radius / 111111)) 

238 ) 

239 .scalars() 

240 .all() 

241 ) 

242 return users, None 

243 

244 

245def generate_event_create_notifications(payload: jobs_pb2.GenerateEventCreateNotificationsPayload): 

246 """ 

247 Background job to generated/fan out event notifications 

248 """ 

249 from couchers.servicers.communities import community_to_pb 

250 

251 logger.info(f"Fanning out notifications for event occurrence id = {payload.occurrence_id}") 

252 

253 with session_scope() as session: 

254 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id) 

255 creator = occurrence.creator_user 

256 

257 users, node_id = get_users_to_notify_for_new_event(session, occurrence) 

258 

259 inviting_user = session.execute(select(User).where(User.id == payload.inviting_user_id)).scalar_one_or_none() 

260 

261 if not inviting_user: 

262 logger.error(f"Inviting user {payload.inviting_user_id} is gone while trying to send event notification?") 

263 return 

264 

265 for user in users: 

266 if are_blocked(session, user.id, creator.id): 

267 continue 

268 context = SimpleNamespace(user_id=user.id) 

269 notify( 

270 user_id=user.id, 

271 topic_action="event:create_approved" if payload.approved else "event:create_any", 

272 key=payload.occurrence_id, 

273 data=notification_data_pb2.EventCreate( 

274 event=event_to_pb(session, occurrence, context), 

275 inviting_user=user_model_to_pb(inviting_user, session, context), 

276 nearby=True if node_id is None else None, 

277 in_community=community_to_pb(event.parent_node, context) if node_id is not None else None, 

278 ), 

279 ) 

280 

281 

282def generate_event_update_notifications(payload: jobs_pb2.GenerateEventUpdateNotificationsPayload): 

283 with session_scope() as session: 

284 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id) 

285 

286 updating_user = session.execute(select(User).where(User.id == payload.updating_user_id)).scalar_one_or_none() 

287 

288 subscribed_user_ids = [user.id for user in event.subscribers] 

289 attending_user_ids = [user.user_id for user in occurrence.attendances] 

290 

291 for user_id in set(subscribed_user_ids + attending_user_ids): 

292 logger.info(user_id) 

293 if are_blocked(session, user_id, updating_user.id): 

294 continue 

295 context = SimpleNamespace(user_id=user_id) 

296 notify( 

297 user_id=user_id, 

298 topic_action="event:update", 

299 key=payload.occurrence_id, 

300 data=notification_data_pb2.EventUpdate( 

301 event=event_to_pb(session, occurrence, context), 

302 updating_user=user_model_to_pb(updating_user, session, context), 

303 updated_items=payload.updated_items, 

304 ), 

305 ) 

306 

307 

308def generate_event_cancel_notifications(payload: jobs_pb2.GenerateEventCancelNotificationsPayload): 

309 with session_scope() as session: 

310 event, occurrence = _get_event_and_occurrence_one(session, occurrence_id=payload.occurrence_id) 

311 

312 cancelling_user = session.execute( 

313 select(User).where(User.id == payload.cancelling_user_id) 

314 ).scalar_one_or_none() 

315 

316 subscribed_user_ids = [user.id for user in event.subscribers] 

317 attending_user_ids = [user.user_id for user in occurrence.attendances] 

318 

319 for user_id in set(subscribed_user_ids + attending_user_ids): 

320 logger.info(user_id) 

321 if are_blocked(session, user_id, cancelling_user.id): 

322 continue 

323 context = SimpleNamespace(user_id=user_id) 

324 notify( 

325 user_id=user_id, 

326 topic_action="event:cancel", 

327 key=payload.occurrence_id, 

328 data=notification_data_pb2.EventCancel( 

329 event=event_to_pb(session, occurrence, context), 

330 cancelling_user=user_model_to_pb(cancelling_user, session, context), 

331 ), 

332 ) 

333 

334 

335def generate_event_delete_notifications(payload: jobs_pb2.GenerateEventDeleteNotificationsPayload): 

336 with session_scope() as session: 

337 event, occurrence = _get_event_and_occurrence_one( 

338 session, occurrence_id=payload.occurrence_id, include_deleted=True 

339 ) 

340 

341 subscribed_user_ids = [user.id for user in event.subscribers] 

342 attending_user_ids = [user.user_id for user in occurrence.attendances] 

343 

344 for user_id in set(subscribed_user_ids + attending_user_ids): 

345 logger.info(user_id) 

346 context = SimpleNamespace(user_id=user_id) 

347 notify( 

348 user_id=user_id, 

349 topic_action="event:delete", 

350 key=payload.occurrence_id, 

351 data=notification_data_pb2.EventDelete( 

352 event=event_to_pb(session, occurrence, context), 

353 ), 

354 ) 

355 

356 

357class Events(events_pb2_grpc.EventsServicer): 

358 def CreateEvent(self, request, context): 

359 if not request.title: 

360 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_TITLE) 

361 if not request.content: 

362 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_CONTENT) 

363 if request.HasField("online_information"): 

364 online = True 

365 geom = None 

366 address = None 

367 if not request.online_information.link: 

368 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.ONLINE_EVENT_REQUIRES_LINK) 

369 link = request.online_information.link 

370 elif request.HasField("offline_information"): 

371 online = False 

372 if not ( 

373 request.offline_information.address 

374 and request.offline_information.lat 

375 and request.offline_information.lng 

376 ): 

377 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_ADDRESS_OR_LOCATION) 

378 if request.offline_information.lat == 0 and request.offline_information.lng == 0: 

379 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) 

380 geom = create_coordinate(request.offline_information.lat, request.offline_information.lng) 

381 address = request.offline_information.address 

382 link = None 

383 else: 

384 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_ADDRESS_LOCATION_OR_LINK) 

385 

386 start_time = to_aware_datetime(request.start_time) 

387 end_time = to_aware_datetime(request.end_time) 

388 

389 _check_occurrence_time_validity(start_time, end_time, context) 

390 

391 with session_scope() as session: 

392 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

393 if request.parent_community_id: 

394 parent_node = session.execute( 

395 select(Node).where(Node.id == request.parent_community_id) 

396 ).scalar_one_or_none() 

397 else: 

398 if online: 

399 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.ONLINE_EVENT_MISSING_PARENT_COMMUNITY) 

400 # parent community computed from geom 

401 parent_node = get_parent_node_at_location(session, geom) 

402 

403 if not parent_node: 

404 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.COMMUNITY_NOT_FOUND) 

405 

406 if ( 

407 request.photo_key 

408 and not session.execute(select(Upload).where(Upload.key == request.photo_key)).scalar_one_or_none() 

409 ): 

410 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.PHOTO_NOT_FOUND) 

411 

412 event = Event( 

413 title=request.title, 

414 parent_node_id=parent_node.id, 

415 owner_user_id=context.user_id, 

416 thread=Thread(), 

417 creator_user_id=context.user_id, 

418 ) 

419 session.add(event) 

420 

421 occurrence = EventOccurrence( 

422 event=event, 

423 content=request.content, 

424 geom=geom, 

425 address=address, 

426 link=link, 

427 photo_key=request.photo_key if request.photo_key != "" else None, 

428 # timezone=timezone, 

429 during=DateTimeTZRange(start_time, end_time), 

430 creator_user_id=context.user_id, 

431 ) 

432 session.add(occurrence) 

433 

434 session.add( 

435 EventOrganizer( 

436 user_id=context.user_id, 

437 event=event, 

438 ) 

439 ) 

440 

441 session.add( 

442 EventSubscription( 

443 user_id=context.user_id, 

444 event=event, 

445 ) 

446 ) 

447 

448 session.add( 

449 EventOccurrenceAttendee( 

450 user_id=context.user_id, 

451 occurrence=occurrence, 

452 attendee_status=AttendeeStatus.going, 

453 ) 

454 ) 

455 

456 session.commit() 

457 

458 if user.has_completed_profile: 

459 queue_job( 

460 "generate_event_create_notifications", 

461 payload=jobs_pb2.GenerateEventCreateNotificationsPayload( 

462 inviting_user_id=user.id, 

463 occurrence_id=occurrence.id, 

464 approved=False, 

465 ), 

466 ) 

467 

468 return event_to_pb(session, occurrence, context) 

469 

470 def ScheduleEvent(self, request, context): 

471 if not request.content: 

472 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_CONTENT) 

473 if request.HasField("online_information"): 

474 geom = None 

475 address = None 

476 link = request.online_information.link 

477 elif request.HasField("offline_information"): 

478 if not ( 

479 request.offline_information.address 

480 and request.offline_information.lat 

481 and request.offline_information.lng 

482 ): 

483 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_ADDRESS_OR_LOCATION) 

484 if request.offline_information.lat == 0 and request.offline_information.lng == 0: 

485 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) 

486 geom = create_coordinate(request.offline_information.lat, request.offline_information.lng) 

487 address = request.offline_information.address 

488 link = None 

489 else: 

490 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.MISSING_EVENT_ADDRESS_LOCATION_OR_LINK) 

491 

492 start_time = to_aware_datetime(request.start_time) 

493 end_time = to_aware_datetime(request.end_time) 

494 

495 _check_occurrence_time_validity(start_time, end_time, context) 

496 

497 with session_scope() as session: 

498 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

499 if not res: 

500 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

501 

502 event, occurrence = res 

503 

504 if not _can_edit_event(session, event, context.user_id): 

505 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_EDIT_PERMISSION_DENIED) 

506 

507 if occurrence.is_cancelled: 

508 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

509 

510 if ( 

511 request.photo_key 

512 and not session.execute(select(Upload).where(Upload.key == request.photo_key)).scalar_one_or_none() 

513 ): 

514 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.PHOTO_NOT_FOUND) 

515 

516 during = DateTimeTZRange(start_time, end_time) 

517 

518 # && is the overlap operator for ranges 

519 if ( 

520 session.execute( 

521 select(EventOccurrence.id) 

522 .where(EventOccurrence.event_id == event.id) 

523 .where(EventOccurrence.during.op("&&")(during)) 

524 ) 

525 .scalars() 

526 .first() 

527 is not None 

528 ): 

529 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_OVERLAP) 

530 

531 occurrence = EventOccurrence( 

532 event=event, 

533 content=request.content, 

534 geom=geom, 

535 address=address, 

536 link=link, 

537 photo_key=request.photo_key if request.photo_key != "" else None, 

538 # timezone=timezone, 

539 during=during, 

540 creator_user_id=context.user_id, 

541 ) 

542 session.add(occurrence) 

543 

544 session.add( 

545 EventOccurrenceAttendee( 

546 user_id=context.user_id, 

547 occurrence=occurrence, 

548 attendee_status=AttendeeStatus.going, 

549 ) 

550 ) 

551 

552 session.flush() 

553 

554 # TODO: notify 

555 

556 return event_to_pb(session, occurrence, context) 

557 

558 def UpdateEvent(self, request, context): 

559 with session_scope() as session: 

560 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

561 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

562 if not res: 

563 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

564 

565 event, occurrence = res 

566 

567 if not _can_edit_event(session, event, context.user_id): 

568 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_EDIT_PERMISSION_DENIED) 

569 

570 # the things that were updated and need to be notified about 

571 notify_updated = [] 

572 

573 if occurrence.is_cancelled: 

574 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

575 

576 occurrence_update = {"last_edited": now()} 

577 

578 if request.HasField("title"): 

579 notify_updated.append("title") 

580 event.title = request.title.value 

581 event.last_edited = now() 

582 

583 if request.HasField("content"): 

584 notify_updated.append("content") 

585 occurrence_update["content"] = request.content.value 

586 

587 if request.HasField("photo_key"): 

588 occurrence_update["photo_key"] = request.photo_key.value 

589 

590 if request.HasField("online_information"): 

591 notify_updated.append("location") 

592 if not request.online_information.link: 

593 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.ONLINE_EVENT_REQUIRES_LINK) 

594 occurrence_update["link"] = request.online_information.link 

595 occurrence_update["geom"] = None 

596 occurrence_update["address"] = None 

597 elif request.HasField("offline_information"): 

598 notify_updated.append("location") 

599 occurrence_update["link"] = None 

600 if request.offline_information.lat == 0 and request.offline_information.lng == 0: 

601 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_COORDINATE) 

602 occurrence_update["geom"] = create_coordinate( 

603 request.offline_information.lat, request.offline_information.lng 

604 ) 

605 occurrence_update["address"] = request.offline_information.address 

606 

607 if request.HasField("start_time") or request.HasField("end_time"): 

608 if request.update_all_future: 

609 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.EVENT_CANT_UPDATE_ALL_TIMES) 

610 if request.HasField("start_time"): 

611 notify_updated.append("start time") 

612 start_time = to_aware_datetime(request.start_time) 

613 else: 

614 start_time = occurrence.start_time 

615 if request.HasField("end_time"): 

616 notify_updated.append("end time") 

617 end_time = to_aware_datetime(request.end_time) 

618 else: 

619 end_time = occurrence.end_time 

620 

621 _check_occurrence_time_validity(start_time, end_time, context) 

622 

623 during = DateTimeTZRange(start_time, end_time) 

624 

625 # && is the overlap operator for ranges 

626 if ( 

627 session.execute( 

628 select(EventOccurrence.id) 

629 .where(EventOccurrence.event_id == event.id) 

630 .where(EventOccurrence.id != occurrence.id) 

631 .where(EventOccurrence.during.op("&&")(during)) 

632 ) 

633 .scalars() 

634 .first() 

635 is not None 

636 ): 

637 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_OVERLAP) 

638 

639 occurrence_update["during"] = during 

640 

641 # TODO 

642 # if request.HasField("timezone"): 

643 # occurrence_update["timezone"] = request.timezone 

644 

645 # allow editing any event which hasn't ended more than 24 hours before now 

646 # when editing all future events, we edit all which have not yet ended 

647 

648 if request.update_all_future: 

649 session.execute( 

650 update(EventOccurrence) 

651 .where(EventOccurrence.end_time >= now() - timedelta(hours=24)) 

652 .where(EventOccurrence.start_time >= occurrence.start_time) 

653 .values(occurrence_update) 

654 .execution_options(synchronize_session=False) 

655 ) 

656 else: 

657 if occurrence.end_time < now() - timedelta(hours=24): 

658 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

659 session.execute( 

660 update(EventOccurrence) 

661 .where(EventOccurrence.end_time >= now() - timedelta(hours=24)) 

662 .where(EventOccurrence.id == occurrence.id) 

663 .values(occurrence_update) 

664 .execution_options(synchronize_session=False) 

665 ) 

666 

667 session.flush() 

668 

669 if notify_updated: 

670 logger.info(f"Fields {','.join(notify_updated)} updated in event {event.id=}, notifying") 

671 

672 queue_job( 

673 "generate_event_update_notifications", 

674 payload=jobs_pb2.GenerateEventUpdateNotificationsPayload( 

675 updating_user_id=user.id, 

676 occurrence_id=occurrence.id, 

677 updated_items=notify_updated, 

678 ), 

679 ) 

680 

681 # since we have synchronize_session=False, we have to refresh the object 

682 session.refresh(occurrence) 

683 

684 return event_to_pb(session, occurrence, context) 

685 

686 def GetEvent(self, request, context): 

687 with session_scope() as session: 

688 occurrence = session.execute( 

689 select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted) 

690 ).scalar_one_or_none() 

691 

692 if not occurrence: 

693 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

694 

695 return event_to_pb(session, occurrence, context) 

696 

697 def CancelEvent(self, request, context): 

698 with session_scope() as session: 

699 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

700 if not res: 

701 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

702 

703 event, occurrence = res 

704 

705 if not _can_edit_event(session, event, context.user_id): 

706 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_EDIT_PERMISSION_DENIED) 

707 

708 if occurrence.end_time < now() - timedelta(hours=24): 

709 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_CANCEL_OLD_EVENT) 

710 

711 occurrence.is_cancelled = True 

712 

713 queue_job( 

714 "generate_event_cancel_notifications", 

715 payload=jobs_pb2.GenerateEventCancelNotificationsPayload( 

716 cancelling_user_id=context.user_id, 

717 occurrence_id=occurrence.id, 

718 ), 

719 ) 

720 

721 return empty_pb2.Empty() 

722 

723 def RequestCommunityInvite(self, request, context): 

724 with session_scope() as session: 

725 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

726 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

727 if not res: 

728 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

729 

730 event, occurrence = res 

731 

732 if not _can_edit_event(session, event, context.user_id): 

733 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_EDIT_PERMISSION_DENIED) 

734 

735 if occurrence.is_cancelled: 

736 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

737 

738 if occurrence.end_time < now() - timedelta(hours=24): 

739 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

740 

741 this_user_reqs = [req for req in occurrence.community_invite_requests if req.user_id == context.user_id] 

742 

743 if len(this_user_reqs) > 0: 

744 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_COMMUNITY_INVITE_ALREADY_REQUESTED) 

745 

746 approved_reqs = [req for req in occurrence.community_invite_requests if req.approved] 

747 

748 if len(approved_reqs) > 0: 

749 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_COMMUNITY_INVITE_ALREADY_APPROVED) 

750 

751 request = EventCommunityInviteRequest( 

752 occurrence_id=request.event_id, 

753 user_id=context.user_id, 

754 ) 

755 session.add(request) 

756 session.flush() 

757 

758 send_event_community_invite_request_email(request) 

759 

760 return empty_pb2.Empty() 

761 

762 def ListEventOccurrences(self, request, context): 

763 with session_scope() as session: 

764 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

765 # the page token is a unix timestamp of where we left off 

766 page_token = dt_from_millis(int(request.page_token)) if request.page_token else now() 

767 occurrence = session.execute( 

768 select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted) 

769 ).scalar_one_or_none() 

770 if not occurrence: 

771 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

772 

773 occurrences = ( 

774 select(EventOccurrence).where(EventOccurrence.event_id == Event.id).where(~EventOccurrence.is_deleted) 

775 ) 

776 

777 if not request.include_cancelled: 

778 occurrences = occurrences.where(~EventOccurrence.is_cancelled) 

779 

780 if not request.past: 

781 occurrences = occurrences.where(EventOccurrence.end_time > page_token - timedelta(seconds=1)).order_by( 

782 EventOccurrence.start_time.asc() 

783 ) 

784 else: 

785 occurrences = occurrences.where(EventOccurrence.end_time < page_token + timedelta(seconds=1)).order_by( 

786 EventOccurrence.start_time.desc() 

787 ) 

788 

789 occurrences = occurrences.limit(page_size + 1) 

790 occurrences = session.execute(occurrences).scalars().all() 

791 

792 return events_pb2.ListEventOccurrencesRes( 

793 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]], 

794 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None, 

795 ) 

796 

797 def ListEventAttendees(self, request, context): 

798 with session_scope() as session: 

799 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

800 next_user_id = int(request.page_token) if request.page_token else 0 

801 occurrence = session.execute( 

802 select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted) 

803 ).scalar_one_or_none() 

804 if not occurrence: 

805 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

806 attendees = ( 

807 session.execute( 

808 select(EventOccurrenceAttendee) 

809 .where_users_column_visible(context, EventOccurrenceAttendee.user_id) 

810 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id) 

811 .where(EventOccurrenceAttendee.user_id >= next_user_id) 

812 .order_by(EventOccurrenceAttendee.user_id) 

813 .limit(page_size + 1) 

814 ) 

815 .scalars() 

816 .all() 

817 ) 

818 return events_pb2.ListEventAttendeesRes( 

819 attendee_user_ids=[attendee.user_id for attendee in attendees[:page_size]], 

820 next_page_token=str(attendees[-1].user_id) if len(attendees) > page_size else None, 

821 ) 

822 

823 def ListEventSubscribers(self, request, context): 

824 with session_scope() as session: 

825 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

826 next_user_id = int(request.page_token) if request.page_token else 0 

827 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

828 if not res: 

829 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

830 event, occurrence = res 

831 subscribers = ( 

832 session.execute( 

833 select(EventSubscription) 

834 .where_users_column_visible(context, EventSubscription.user_id) 

835 .where(EventSubscription.event_id == event.id) 

836 .where(EventSubscription.user_id >= next_user_id) 

837 .order_by(EventSubscription.user_id) 

838 .limit(page_size + 1) 

839 ) 

840 .scalars() 

841 .all() 

842 ) 

843 return events_pb2.ListEventSubscribersRes( 

844 subscriber_user_ids=[subscriber.user_id for subscriber in subscribers[:page_size]], 

845 next_page_token=str(subscribers[-1].user_id) if len(subscribers) > page_size else None, 

846 ) 

847 

848 def ListEventOrganizers(self, request, context): 

849 with session_scope() as session: 

850 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

851 next_user_id = int(request.page_token) if request.page_token else 0 

852 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

853 if not res: 

854 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

855 event, occurrence = res 

856 organizers = ( 

857 session.execute( 

858 select(EventOrganizer) 

859 .where_users_column_visible(context, EventOrganizer.user_id) 

860 .where(EventOrganizer.event_id == event.id) 

861 .where(EventOrganizer.user_id >= next_user_id) 

862 .order_by(EventOrganizer.user_id) 

863 .limit(page_size + 1) 

864 ) 

865 .scalars() 

866 .all() 

867 ) 

868 return events_pb2.ListEventOrganizersRes( 

869 organizer_user_ids=[organizer.user_id for organizer in organizers[:page_size]], 

870 next_page_token=str(organizers[-1].user_id) if len(organizers) > page_size else None, 

871 ) 

872 

873 def TransferEvent(self, request, context): 

874 with session_scope() as session: 

875 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

876 if not res: 

877 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

878 

879 event, occurrence = res 

880 

881 if not _can_edit_event(session, event, context.user_id): 

882 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_TRANSFER_PERMISSION_DENIED) 

883 

884 if occurrence.is_cancelled: 

885 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

886 

887 if occurrence.end_time < now() - timedelta(hours=24): 

888 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

889 

890 if request.WhichOneof("new_owner") == "new_owner_group_id": 

891 cluster = session.execute( 

892 select(Cluster).where(~Cluster.is_official_cluster).where(Cluster.id == request.new_owner_group_id) 

893 ).scalar_one_or_none() 

894 elif request.WhichOneof("new_owner") == "new_owner_community_id": 

895 cluster = session.execute( 

896 select(Cluster) 

897 .where(Cluster.parent_node_id == request.new_owner_community_id) 

898 .where(Cluster.is_official_cluster) 

899 ).scalar_one_or_none() 

900 

901 if not cluster: 

902 context.abort(grpc.StatusCode.NOT_FOUND, errors.GROUP_OR_COMMUNITY_NOT_FOUND) 

903 

904 event.owner_user = None 

905 event.owner_cluster = cluster 

906 

907 session.commit() 

908 return event_to_pb(session, occurrence, context) 

909 

910 def SetEventSubscription(self, request, context): 

911 with session_scope() as session: 

912 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

913 if not res: 

914 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

915 

916 event, occurrence = res 

917 

918 if occurrence.is_cancelled: 

919 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

920 

921 if occurrence.end_time < now() - timedelta(hours=24): 

922 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

923 

924 current_subscription = session.execute( 

925 select(EventSubscription) 

926 .where(EventSubscription.user_id == context.user_id) 

927 .where(EventSubscription.event_id == event.id) 

928 ).scalar_one_or_none() 

929 

930 # if not subscribed, subscribe 

931 if request.subscribe and not current_subscription: 

932 session.add(EventSubscription(user_id=context.user_id, event_id=event.id)) 

933 

934 # if subscribed but unsubbing, remove subscription 

935 if not request.subscribe and current_subscription: 

936 session.delete(current_subscription) 

937 

938 session.flush() 

939 

940 return event_to_pb(session, occurrence, context) 

941 

942 def SetEventAttendance(self, request, context): 

943 with session_scope() as session: 

944 occurrence = session.execute( 

945 select(EventOccurrence).where(EventOccurrence.id == request.event_id).where(~EventOccurrence.is_deleted) 

946 ).scalar_one_or_none() 

947 

948 if not occurrence: 

949 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

950 

951 if occurrence.is_cancelled: 

952 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

953 

954 if occurrence.end_time < now() - timedelta(hours=24): 

955 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

956 

957 current_attendance = session.execute( 

958 select(EventOccurrenceAttendee) 

959 .where(EventOccurrenceAttendee.user_id == context.user_id) 

960 .where(EventOccurrenceAttendee.occurrence_id == occurrence.id) 

961 ).scalar_one_or_none() 

962 

963 if request.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING: 

964 if current_attendance: 

965 session.delete(current_attendance) 

966 # if unset/not going, nothing to do! 

967 else: 

968 if current_attendance: 

969 current_attendance.attendee_status = attendancestate2sql[request.attendance_state] 

970 else: 

971 # create new 

972 attendance = EventOccurrenceAttendee( 

973 user_id=context.user_id, 

974 occurrence_id=occurrence.id, 

975 attendee_status=attendancestate2sql[request.attendance_state], 

976 ) 

977 session.add(attendance) 

978 

979 session.flush() 

980 

981 return event_to_pb(session, occurrence, context) 

982 

983 def ListMyEvents(self, request, context): 

984 with session_scope() as session: 

985 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

986 # the page token is a unix timestamp of where we left off 

987 page_token = dt_from_millis(int(request.page_token)) if request.page_token else now() 

988 

989 occurrences = ( 

990 select(EventOccurrence) 

991 .join(Event, Event.id == EventOccurrence.event_id) 

992 .where(~EventOccurrence.is_deleted) 

993 ) 

994 

995 include_all = not (request.subscribed or request.attending or request.organizing or request.my_communities) 

996 include_subscribed = request.subscribed or include_all 

997 include_organizing = request.organizing or include_all 

998 include_attending = request.attending or include_all 

999 include_my_communities = request.my_communities or include_all 

1000 

1001 where_ = [] 

1002 

1003 if include_subscribed: 

1004 occurrences = occurrences.outerjoin( 

1005 EventSubscription, 

1006 and_(EventSubscription.event_id == Event.id, EventSubscription.user_id == context.user_id), 

1007 ) 

1008 where_.append(EventSubscription.user_id != None) 

1009 if include_organizing: 

1010 occurrences = occurrences.outerjoin( 

1011 EventOrganizer, and_(EventOrganizer.event_id == Event.id, EventOrganizer.user_id == context.user_id) 

1012 ) 

1013 where_.append(EventOrganizer.user_id != None) 

1014 if include_attending: 

1015 occurrences = occurrences.outerjoin( 

1016 EventOccurrenceAttendee, 

1017 and_( 

1018 EventOccurrenceAttendee.occurrence_id == EventOccurrence.id, 

1019 EventOccurrenceAttendee.user_id == context.user_id, 

1020 ), 

1021 ) 

1022 where_.append(EventOccurrenceAttendee.user_id != None) 

1023 if include_my_communities: 

1024 my_communities = ( 

1025 session.execute( 

1026 select(Node.id) 

1027 .join(Cluster, Cluster.parent_node_id == Node.id) 

1028 .join(ClusterSubscription, ClusterSubscription.cluster_id == Cluster.id) 

1029 .where(ClusterSubscription.user_id == context.user_id) 

1030 .where(Cluster.is_official_cluster) 

1031 .order_by(Node.id) 

1032 .limit(100000) 

1033 ) 

1034 .scalars() 

1035 .all() 

1036 ) 

1037 where_.append(Event.parent_node_id.in_(my_communities)) 

1038 

1039 occurrences = occurrences.where(or_(*where_)) 

1040 

1041 if not request.include_cancelled: 

1042 occurrences = occurrences.where(~EventOccurrence.is_cancelled) 

1043 

1044 if not request.past: 

1045 occurrences = occurrences.where(EventOccurrence.end_time > page_token - timedelta(seconds=1)).order_by( 

1046 EventOccurrence.start_time.asc() 

1047 ) 

1048 else: 

1049 occurrences = occurrences.where(EventOccurrence.end_time < page_token + timedelta(seconds=1)).order_by( 

1050 EventOccurrence.start_time.desc() 

1051 ) 

1052 

1053 occurrences = occurrences.limit(page_size + 1) 

1054 occurrences = session.execute(occurrences).scalars().all() 

1055 

1056 return events_pb2.ListMyEventsRes( 

1057 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]], 

1058 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None, 

1059 ) 

1060 

1061 def ListAllEvents(self, request, context): 

1062 with session_scope() as session: 

1063 page_size = min(MAX_PAGINATION_LENGTH, request.page_size or MAX_PAGINATION_LENGTH) 

1064 # the page token is a unix timestamp of where we left off 

1065 page_token = dt_from_millis(int(request.page_token)) if request.page_token else now() 

1066 

1067 occurrences = ( 

1068 select(EventOccurrence) 

1069 .join(Event, Event.id == EventOccurrence.event_id) 

1070 .where(~EventOccurrence.is_deleted) 

1071 ) 

1072 

1073 if not request.past: 

1074 occurrences = occurrences.where(EventOccurrence.end_time > page_token - timedelta(seconds=1)).order_by( 

1075 EventOccurrence.start_time.asc() 

1076 ) 

1077 else: 

1078 occurrences = occurrences.where(EventOccurrence.end_time < page_token + timedelta(seconds=1)).order_by( 

1079 EventOccurrence.start_time.desc() 

1080 ) 

1081 

1082 occurrences = occurrences.limit(page_size + 1) 

1083 occurrences = session.execute(occurrences).scalars().all() 

1084 

1085 return events_pb2.ListAllEventsRes( 

1086 events=[event_to_pb(session, occurrence, context) for occurrence in occurrences[:page_size]], 

1087 next_page_token=str(millis_from_dt(occurrences[-1].end_time)) if len(occurrences) > page_size else None, 

1088 ) 

1089 

1090 def InviteEventOrganizer(self, request, context): 

1091 with session_scope() as session: 

1092 user = session.execute(select(User).where(User.id == context.user_id)).scalar_one() 

1093 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

1094 if not res: 

1095 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

1096 

1097 event, occurrence = res 

1098 

1099 if not _can_edit_event(session, event, context.user_id): 

1100 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_EDIT_PERMISSION_DENIED) 

1101 

1102 if occurrence.is_cancelled: 

1103 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

1104 

1105 if occurrence.end_time < now() - timedelta(hours=24): 

1106 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

1107 

1108 if not session.execute( 

1109 select(User).where_users_visible(context).where(User.id == request.user_id) 

1110 ).scalar_one_or_none(): 

1111 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

1112 

1113 session.add( 

1114 EventOrganizer( 

1115 user_id=request.user_id, 

1116 event=event, 

1117 ) 

1118 ) 

1119 session.flush() 

1120 

1121 other_user_context = SimpleNamespace(user_id=request.user_id) 

1122 

1123 notify( 

1124 user_id=request.user_id, 

1125 topic_action="event:invite_organizer", 

1126 key=event.id, 

1127 data=notification_data_pb2.EventInviteOrganizer( 

1128 event=event_to_pb(session, occurrence, other_user_context), 

1129 inviting_user=user_model_to_pb(user, session, other_user_context), 

1130 ), 

1131 ) 

1132 

1133 return empty_pb2.Empty() 

1134 

1135 def RemoveEventOrganizer(self, request, context): 

1136 with session_scope() as session: 

1137 res = _get_event_and_occurrence_one_or_none(session, occurrence_id=request.event_id) 

1138 if not res: 

1139 context.abort(grpc.StatusCode.NOT_FOUND, errors.EVENT_NOT_FOUND) 

1140 

1141 event, occurrence = res 

1142 

1143 if occurrence.is_cancelled: 

1144 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.EVENT_CANT_UPDATE_CANCELLED_EVENT) 

1145 

1146 if occurrence.end_time < now() - timedelta(hours=24): 

1147 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_UPDATE_OLD_EVENT) 

1148 

1149 if event.owner_user_id == context.user_id: 

1150 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_CANT_REMOVE_OWNER_AS_ORGANIZER) 

1151 

1152 current = session.execute( 

1153 select(EventOrganizer) 

1154 .where(EventOrganizer.user_id == context.user_id) 

1155 .where(EventOrganizer.event_id == event.id) 

1156 ).scalar_one_or_none() 

1157 

1158 if not current: 

1159 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.EVENT_NOT_AN_ORGANIZER) 

1160 

1161 session.delete(current) 

1162 

1163 return empty_pb2.Empty()