Coverage for app / backend / src / tests / test_events.py: 99%
1538 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 09:44 +0000
1from datetime import timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2, wrappers_pb2
6from psycopg.types.range import TimestamptzRange
7from sqlalchemy import select
8from sqlalchemy.sql.expression import update
10from couchers.db import session_scope
11from couchers.jobs.handlers import send_event_reminders
12from couchers.models import (
13 BackgroundJob,
14 BackgroundJobState,
15 EventOccurrence,
16 ModerationState,
17 ModerationVisibility,
18 Notification,
19 NotificationDelivery,
20 NotificationTopicAction,
21 Upload,
22)
23from couchers.proto import editor_pb2, events_pb2, threads_pb2
24from couchers.tasks import enforce_community_memberships
25from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime
26from tests.fixtures.db import generate_user
27from tests.fixtures.misc import Moderator, PushCollector, email_fields, mock_notification_email, process_jobs
28from tests.fixtures.sessions import events_session, real_editor_session, threads_session
29from tests.test_communities import create_community, create_group
32@pytest.fixture(autouse=True)
33def _(testconfig):
34 pass
37def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
38 # test cases:
39 # can create event
40 # cannot create event with missing details
41 # can create online event
42 # can create in person event
43 # can't create event that starts in the past
44 # can create in different timezones
46 # event creator
47 user1, token1 = generate_user()
48 # community moderator
49 user2, token2 = generate_user()
50 # third party
51 user3, token3 = generate_user()
53 with session_scope() as session:
54 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
56 time_before = now()
57 start_time = now() + timedelta(hours=2)
58 end_time = start_time + timedelta(hours=3)
60 with events_session(token1) as api:
61 # in person event
62 res = api.CreateEvent(
63 events_pb2.CreateEventReq(
64 title="Dummy Title",
65 content="Dummy content.",
66 photo_key=None,
67 offline_information=events_pb2.OfflineEventInformation(
68 address="Near Null Island",
69 lat=0.1,
70 lng=0.2,
71 ),
72 start_time=Timestamp_from_datetime(start_time),
73 end_time=Timestamp_from_datetime(end_time),
74 timezone="UTC",
75 )
76 )
78 assert res.is_next
79 assert res.title == "Dummy Title"
80 assert res.slug == "dummy-title"
81 assert res.content == "Dummy content."
82 assert not res.photo_url
83 assert res.WhichOneof("mode") == "offline_information"
84 assert res.offline_information.lat == 0.1
85 assert res.offline_information.lng == 0.2
86 assert res.offline_information.address == "Near Null Island"
87 assert time_before <= to_aware_datetime(res.created) <= now()
88 assert time_before <= to_aware_datetime(res.last_edited) <= now()
89 assert res.creator_user_id == user1.id
90 assert to_aware_datetime(res.start_time) == start_time
91 assert to_aware_datetime(res.end_time) == end_time
92 # assert res.timezone == "UTC"
93 assert res.start_time_display
94 assert res.end_time_display
95 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
96 assert res.organizer
97 assert res.subscriber
98 assert res.going_count == 1
99 assert res.maybe_count == 0
100 assert res.organizer_count == 1
101 assert res.subscriber_count == 1
102 assert res.owner_user_id == user1.id
103 assert not res.owner_community_id
104 assert not res.owner_group_id
105 assert res.thread.thread_id
106 assert res.can_edit
107 assert not res.can_moderate
109 event_id = res.event_id
111 # Approve the event so other users can see it
112 moderator.approve_event_occurrence(event_id)
114 with events_session(token2) as api:
115 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
117 assert res.is_next
118 assert res.title == "Dummy Title"
119 assert res.slug == "dummy-title"
120 assert res.content == "Dummy content."
121 assert not res.photo_url
122 assert res.WhichOneof("mode") == "offline_information"
123 assert res.offline_information.lat == 0.1
124 assert res.offline_information.lng == 0.2
125 assert res.offline_information.address == "Near Null Island"
126 assert time_before <= to_aware_datetime(res.created) <= now()
127 assert time_before <= to_aware_datetime(res.last_edited) <= now()
128 assert res.creator_user_id == user1.id
129 assert to_aware_datetime(res.start_time) == start_time
130 assert to_aware_datetime(res.end_time) == end_time
131 # assert res.timezone == "UTC"
132 assert res.start_time_display
133 assert res.end_time_display
134 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
135 assert not res.organizer
136 assert not res.subscriber
137 assert res.going_count == 1
138 assert res.maybe_count == 0
139 assert res.organizer_count == 1
140 assert res.subscriber_count == 1
141 assert res.owner_user_id == user1.id
142 assert not res.owner_community_id
143 assert not res.owner_group_id
144 assert res.thread.thread_id
145 assert res.can_edit
146 assert res.can_moderate
148 with events_session(token3) as api:
149 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
151 assert res.is_next
152 assert res.title == "Dummy Title"
153 assert res.slug == "dummy-title"
154 assert res.content == "Dummy content."
155 assert not res.photo_url
156 assert res.WhichOneof("mode") == "offline_information"
157 assert res.offline_information.lat == 0.1
158 assert res.offline_information.lng == 0.2
159 assert res.offline_information.address == "Near Null Island"
160 assert time_before <= to_aware_datetime(res.created) <= now()
161 assert time_before <= to_aware_datetime(res.last_edited) <= now()
162 assert res.creator_user_id == user1.id
163 assert to_aware_datetime(res.start_time) == start_time
164 assert to_aware_datetime(res.end_time) == end_time
165 # assert res.timezone == "UTC"
166 assert res.start_time_display
167 assert res.end_time_display
168 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
169 assert not res.organizer
170 assert not res.subscriber
171 assert res.going_count == 1
172 assert res.maybe_count == 0
173 assert res.organizer_count == 1
174 assert res.subscriber_count == 1
175 assert res.owner_user_id == user1.id
176 assert not res.owner_community_id
177 assert not res.owner_group_id
178 assert res.thread.thread_id
179 assert not res.can_edit
180 assert not res.can_moderate
182 with events_session(token1) as api:
183 # online only event
184 res = api.CreateEvent(
185 events_pb2.CreateEventReq(
186 title="Dummy Title",
187 content="Dummy content.",
188 photo_key=None,
189 online_information=events_pb2.OnlineEventInformation(
190 link="https://couchers.org/meet/",
191 ),
192 parent_community_id=c_id,
193 start_time=Timestamp_from_datetime(start_time),
194 end_time=Timestamp_from_datetime(end_time),
195 timezone="UTC",
196 )
197 )
199 assert res.is_next
200 assert res.title == "Dummy Title"
201 assert res.slug == "dummy-title"
202 assert res.content == "Dummy content."
203 assert not res.photo_url
204 assert res.WhichOneof("mode") == "online_information"
205 assert res.online_information.link == "https://couchers.org/meet/"
206 assert time_before <= to_aware_datetime(res.created) <= now()
207 assert time_before <= to_aware_datetime(res.last_edited) <= now()
208 assert res.creator_user_id == user1.id
209 assert to_aware_datetime(res.start_time) == start_time
210 assert to_aware_datetime(res.end_time) == end_time
211 # assert res.timezone == "UTC"
212 assert res.start_time_display
213 assert res.end_time_display
214 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
215 assert res.organizer
216 assert res.subscriber
217 assert res.going_count == 1
218 assert res.maybe_count == 0
219 assert res.organizer_count == 1
220 assert res.subscriber_count == 1
221 assert res.owner_user_id == user1.id
222 assert not res.owner_community_id
223 assert not res.owner_group_id
224 assert res.thread.thread_id
225 assert res.can_edit
226 assert not res.can_moderate
228 event_id = res.event_id
230 # Approve the online event so other users can see it
231 moderator.approve_event_occurrence(event_id)
233 with events_session(token2) as api:
234 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
236 assert res.is_next
237 assert res.title == "Dummy Title"
238 assert res.slug == "dummy-title"
239 assert res.content == "Dummy content."
240 assert not res.photo_url
241 assert res.WhichOneof("mode") == "online_information"
242 assert res.online_information.link == "https://couchers.org/meet/"
243 assert time_before <= to_aware_datetime(res.created) <= now()
244 assert time_before <= to_aware_datetime(res.last_edited) <= now()
245 assert res.creator_user_id == user1.id
246 assert to_aware_datetime(res.start_time) == start_time
247 assert to_aware_datetime(res.end_time) == end_time
248 # assert res.timezone == "UTC"
249 assert res.start_time_display
250 assert res.end_time_display
251 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
252 assert not res.organizer
253 assert not res.subscriber
254 assert res.going_count == 1
255 assert res.maybe_count == 0
256 assert res.organizer_count == 1
257 assert res.subscriber_count == 1
258 assert res.owner_user_id == user1.id
259 assert not res.owner_community_id
260 assert not res.owner_group_id
261 assert res.thread.thread_id
262 assert res.can_edit
263 assert res.can_moderate
265 with events_session(token3) as api:
266 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
268 assert res.is_next
269 assert res.title == "Dummy Title"
270 assert res.slug == "dummy-title"
271 assert res.content == "Dummy content."
272 assert not res.photo_url
273 assert res.WhichOneof("mode") == "online_information"
274 assert res.online_information.link == "https://couchers.org/meet/"
275 assert time_before <= to_aware_datetime(res.created) <= now()
276 assert time_before <= to_aware_datetime(res.last_edited) <= now()
277 assert res.creator_user_id == user1.id
278 assert to_aware_datetime(res.start_time) == start_time
279 assert to_aware_datetime(res.end_time) == end_time
280 # assert res.timezone == "UTC"
281 assert res.start_time_display
282 assert res.end_time_display
283 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
284 assert not res.organizer
285 assert not res.subscriber
286 assert res.going_count == 1
287 assert res.maybe_count == 0
288 assert res.organizer_count == 1
289 assert res.subscriber_count == 1
290 assert res.owner_user_id == user1.id
291 assert not res.owner_community_id
292 assert not res.owner_group_id
293 assert res.thread.thread_id
294 assert not res.can_edit
295 assert not res.can_moderate
297 with events_session(token1) as api:
298 with pytest.raises(grpc.RpcError) as e:
299 api.CreateEvent(
300 events_pb2.CreateEventReq(
301 title="Dummy Title",
302 content="Dummy content.",
303 photo_key=None,
304 online_information=events_pb2.OnlineEventInformation(
305 link="https://couchers.org/meet/",
306 ),
307 start_time=Timestamp_from_datetime(start_time),
308 end_time=Timestamp_from_datetime(end_time),
309 timezone="UTC",
310 )
311 )
312 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
313 assert e.value.details() == "The online event is missing a parent community."
315 with pytest.raises(grpc.RpcError) as e:
316 api.CreateEvent(
317 events_pb2.CreateEventReq(
318 # title="Dummy Title",
319 content="Dummy content.",
320 photo_key=None,
321 offline_information=events_pb2.OfflineEventInformation(
322 address="Near Null Island",
323 lat=0.1,
324 lng=0.1,
325 ),
326 start_time=Timestamp_from_datetime(start_time),
327 end_time=Timestamp_from_datetime(end_time),
328 timezone="UTC",
329 )
330 )
331 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
332 assert e.value.details() == "Missing event title."
334 with pytest.raises(grpc.RpcError) as e:
335 api.CreateEvent(
336 events_pb2.CreateEventReq(
337 title="Dummy Title",
338 # content="Dummy content.",
339 photo_key=None,
340 offline_information=events_pb2.OfflineEventInformation(
341 address="Near Null Island",
342 lat=0.1,
343 lng=0.1,
344 ),
345 start_time=Timestamp_from_datetime(start_time),
346 end_time=Timestamp_from_datetime(end_time),
347 timezone="UTC",
348 )
349 )
350 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
351 assert e.value.details() == "Missing event content."
353 with pytest.raises(grpc.RpcError) as e:
354 api.CreateEvent(
355 events_pb2.CreateEventReq(
356 title="Dummy Title",
357 content="Dummy content.",
358 photo_key="nonexistent",
359 offline_information=events_pb2.OfflineEventInformation(
360 address="Near Null Island",
361 lat=0.1,
362 lng=0.1,
363 ),
364 start_time=Timestamp_from_datetime(start_time),
365 end_time=Timestamp_from_datetime(end_time),
366 timezone="UTC",
367 )
368 )
369 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
370 assert e.value.details() == "Photo not found."
372 with pytest.raises(grpc.RpcError) as e:
373 api.CreateEvent(
374 events_pb2.CreateEventReq(
375 title="Dummy Title",
376 content="Dummy content.",
377 photo_key=None,
378 offline_information=events_pb2.OfflineEventInformation(
379 address="Near Null Island",
380 ),
381 start_time=Timestamp_from_datetime(start_time),
382 end_time=Timestamp_from_datetime(end_time),
383 timezone="UTC",
384 )
385 )
386 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
387 assert e.value.details() == "Missing event address or location."
389 with pytest.raises(grpc.RpcError) as e:
390 api.CreateEvent(
391 events_pb2.CreateEventReq(
392 title="Dummy Title",
393 content="Dummy content.",
394 photo_key=None,
395 offline_information=events_pb2.OfflineEventInformation(
396 lat=0.1,
397 lng=0.1,
398 ),
399 start_time=Timestamp_from_datetime(start_time),
400 end_time=Timestamp_from_datetime(end_time),
401 timezone="UTC",
402 )
403 )
404 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
405 assert e.value.details() == "Missing event address or location."
407 with pytest.raises(grpc.RpcError) as e:
408 api.CreateEvent(
409 events_pb2.CreateEventReq(
410 title="Dummy Title",
411 content="Dummy content.",
412 photo_key=None,
413 online_information=events_pb2.OnlineEventInformation(),
414 start_time=Timestamp_from_datetime(start_time),
415 end_time=Timestamp_from_datetime(end_time),
416 timezone="UTC",
417 )
418 )
419 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
420 assert e.value.details() == "An online-only event requires a link."
422 with pytest.raises(grpc.RpcError) as e:
423 api.CreateEvent(
424 events_pb2.CreateEventReq(
425 title="Dummy Title",
426 content="Dummy content.",
427 parent_community_id=c_id,
428 online_information=events_pb2.OnlineEventInformation(
429 link="https://couchers.org/meet/",
430 ),
431 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)),
432 end_time=Timestamp_from_datetime(end_time),
433 timezone="UTC",
434 )
435 )
436 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
437 assert e.value.details() == "The event must be in the future."
439 with pytest.raises(grpc.RpcError) as e:
440 api.CreateEvent(
441 events_pb2.CreateEventReq(
442 title="Dummy Title",
443 content="Dummy content.",
444 parent_community_id=c_id,
445 online_information=events_pb2.OnlineEventInformation(
446 link="https://couchers.org/meet/",
447 ),
448 start_time=Timestamp_from_datetime(end_time),
449 end_time=Timestamp_from_datetime(start_time),
450 timezone="UTC",
451 )
452 )
453 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
454 assert e.value.details() == "The event must end after it starts."
456 with pytest.raises(grpc.RpcError) as e:
457 api.CreateEvent(
458 events_pb2.CreateEventReq(
459 title="Dummy Title",
460 content="Dummy content.",
461 parent_community_id=c_id,
462 online_information=events_pb2.OnlineEventInformation(
463 link="https://couchers.org/meet/",
464 ),
465 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)),
466 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)),
467 timezone="UTC",
468 )
469 )
470 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
471 assert e.value.details() == "The event needs to start within the next year."
473 with pytest.raises(grpc.RpcError) as e:
474 api.CreateEvent(
475 events_pb2.CreateEventReq(
476 title="Dummy Title",
477 content="Dummy content.",
478 parent_community_id=c_id,
479 online_information=events_pb2.OnlineEventInformation(
480 link="https://couchers.org/meet/",
481 ),
482 start_time=Timestamp_from_datetime(start_time),
483 end_time=Timestamp_from_datetime(now() + timedelta(days=100)),
484 timezone="UTC",
485 )
486 )
487 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
488 assert e.value.details() == "Events cannot last longer than 7 days."
491def test_CreateEvent_incomplete_profile(db):
492 user1, token1 = generate_user(complete_profile=False)
493 user2, token2 = generate_user()
495 with session_scope() as session:
496 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
498 start_time = now() + timedelta(hours=2)
499 end_time = start_time + timedelta(hours=3)
501 with events_session(token1) as api:
502 with pytest.raises(grpc.RpcError) as e:
503 api.CreateEvent(
504 events_pb2.CreateEventReq(
505 title="Dummy Title",
506 content="Dummy content.",
507 photo_key=None,
508 offline_information=events_pb2.OfflineEventInformation(
509 address="Near Null Island",
510 lat=0.1,
511 lng=0.2,
512 ),
513 start_time=Timestamp_from_datetime(start_time),
514 end_time=Timestamp_from_datetime(end_time),
515 timezone="UTC",
516 )
517 )
518 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
519 assert e.value.details() == "You have to complete your profile before you can create an event."
522def test_ScheduleEvent(db):
523 # test cases:
524 # can schedule a new event occurrence
526 user, token = generate_user()
528 with session_scope() as session:
529 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
531 time_before = now()
532 start_time = now() + timedelta(hours=2)
533 end_time = start_time + timedelta(hours=3)
535 with events_session(token) as api:
536 res = api.CreateEvent(
537 events_pb2.CreateEventReq(
538 title="Dummy Title",
539 content="Dummy content.",
540 parent_community_id=c_id,
541 online_information=events_pb2.OnlineEventInformation(
542 link="https://couchers.org/meet/",
543 ),
544 start_time=Timestamp_from_datetime(start_time),
545 end_time=Timestamp_from_datetime(end_time),
546 timezone="UTC",
547 )
548 )
550 new_start_time = now() + timedelta(hours=6)
551 new_end_time = new_start_time + timedelta(hours=2)
553 res = api.ScheduleEvent(
554 events_pb2.ScheduleEventReq(
555 event_id=res.event_id,
556 content="New event occurrence",
557 offline_information=events_pb2.OfflineEventInformation(
558 address="A bit further but still near Null Island",
559 lat=0.3,
560 lng=0.2,
561 ),
562 start_time=Timestamp_from_datetime(new_start_time),
563 end_time=Timestamp_from_datetime(new_end_time),
564 timezone="UTC",
565 )
566 )
568 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
570 assert not res.is_next
571 assert res.title == "Dummy Title"
572 assert res.slug == "dummy-title"
573 assert res.content == "New event occurrence"
574 assert not res.photo_url
575 assert res.WhichOneof("mode") == "offline_information"
576 assert res.offline_information.lat == 0.3
577 assert res.offline_information.lng == 0.2
578 assert res.offline_information.address == "A bit further but still near Null Island"
579 assert time_before <= to_aware_datetime(res.created) <= now()
580 assert time_before <= to_aware_datetime(res.last_edited) <= now()
581 assert res.creator_user_id == user.id
582 assert to_aware_datetime(res.start_time) == new_start_time
583 assert to_aware_datetime(res.end_time) == new_end_time
584 # assert res.timezone == "UTC"
585 assert res.start_time_display
586 assert res.end_time_display
587 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
588 assert res.organizer
589 assert res.subscriber
590 assert res.going_count == 1
591 assert res.maybe_count == 0
592 assert res.organizer_count == 1
593 assert res.subscriber_count == 1
594 assert res.owner_user_id == user.id
595 assert not res.owner_community_id
596 assert not res.owner_group_id
597 assert res.thread.thread_id
598 assert res.can_edit
599 assert res.can_moderate
602def test_cannot_overlap_occurrences_schedule(db):
603 user, token = generate_user()
605 with session_scope() as session:
606 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
608 start = now()
610 with events_session(token) as api:
611 res = api.CreateEvent(
612 events_pb2.CreateEventReq(
613 title="Dummy Title",
614 content="Dummy content.",
615 parent_community_id=c_id,
616 online_information=events_pb2.OnlineEventInformation(
617 link="https://couchers.org/meet/",
618 ),
619 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
620 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
621 timezone="UTC",
622 )
623 )
625 with pytest.raises(grpc.RpcError) as e:
626 api.ScheduleEvent(
627 events_pb2.ScheduleEventReq(
628 event_id=res.event_id,
629 content="New event occurrence",
630 offline_information=events_pb2.OfflineEventInformation(
631 address="A bit further but still near Null Island",
632 lat=0.3,
633 lng=0.2,
634 ),
635 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
636 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
637 timezone="UTC",
638 )
639 )
640 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
641 assert e.value.details() == "An event cannot have overlapping occurrences."
644def test_cannot_overlap_occurrences_update(db):
645 user, token = generate_user()
647 with session_scope() as session:
648 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
650 start = now()
652 with events_session(token) as api:
653 res = api.CreateEvent(
654 events_pb2.CreateEventReq(
655 title="Dummy Title",
656 content="Dummy content.",
657 parent_community_id=c_id,
658 online_information=events_pb2.OnlineEventInformation(
659 link="https://couchers.org/meet/",
660 ),
661 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
662 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
663 timezone="UTC",
664 )
665 )
667 event_id = api.ScheduleEvent(
668 events_pb2.ScheduleEventReq(
669 event_id=res.event_id,
670 content="New event occurrence",
671 offline_information=events_pb2.OfflineEventInformation(
672 address="A bit further but still near Null Island",
673 lat=0.3,
674 lng=0.2,
675 ),
676 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
677 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
678 timezone="UTC",
679 )
680 ).event_id
682 # can overlap with this current existing occurrence
683 api.UpdateEvent(
684 events_pb2.UpdateEventReq(
685 event_id=event_id,
686 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
687 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
688 )
689 )
691 with pytest.raises(grpc.RpcError) as e:
692 api.UpdateEvent(
693 events_pb2.UpdateEventReq(
694 event_id=event_id,
695 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
696 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
697 )
698 )
699 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
700 assert e.value.details() == "An event cannot have overlapping occurrences."
703def test_UpdateEvent_single(db, moderator: Moderator):
704 # test cases:
705 # owner can update
706 # community owner can update
707 # can't mess up online/in person dichotomy
708 # notifies attendees
710 # event creator
711 user1, token1 = generate_user()
712 # community moderator
713 user2, token2 = generate_user()
714 # third parties
715 user3, token3 = generate_user()
716 user4, token4 = generate_user()
717 user5, token5 = generate_user()
718 user6, token6 = generate_user()
720 with session_scope() as session:
721 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
723 time_before = now()
724 start_time = now() + timedelta(hours=2)
725 end_time = start_time + timedelta(hours=3)
727 with events_session(token1) as api:
728 res = api.CreateEvent(
729 events_pb2.CreateEventReq(
730 title="Dummy Title",
731 content="Dummy content.",
732 offline_information=events_pb2.OfflineEventInformation(
733 address="Near Null Island",
734 lat=0.1,
735 lng=0.2,
736 ),
737 start_time=Timestamp_from_datetime(start_time),
738 end_time=Timestamp_from_datetime(end_time),
739 timezone="UTC",
740 )
741 )
743 event_id = res.event_id
745 moderator.approve_event_occurrence(event_id)
747 with events_session(token4) as api:
748 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
750 with events_session(token5) as api:
751 api.SetEventAttendance(
752 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
753 )
755 with events_session(token6) as api:
756 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
757 api.SetEventAttendance(
758 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
759 )
761 time_before_update = now()
763 with events_session(token1) as api:
764 res = api.UpdateEvent(
765 events_pb2.UpdateEventReq(
766 event_id=event_id,
767 )
768 )
770 with events_session(token1) as api:
771 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
773 assert res.is_next
774 assert res.title == "Dummy Title"
775 assert res.slug == "dummy-title"
776 assert res.content == "Dummy content."
777 assert not res.photo_url
778 assert res.WhichOneof("mode") == "offline_information"
779 assert res.offline_information.lat == 0.1
780 assert res.offline_information.lng == 0.2
781 assert res.offline_information.address == "Near Null Island"
782 assert time_before <= to_aware_datetime(res.created) <= time_before_update
783 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
784 assert res.creator_user_id == user1.id
785 assert to_aware_datetime(res.start_time) == start_time
786 assert to_aware_datetime(res.end_time) == end_time
787 # assert res.timezone == "UTC"
788 assert res.start_time_display
789 assert res.end_time_display
790 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
791 assert res.organizer
792 assert res.subscriber
793 assert res.going_count == 2
794 assert res.maybe_count == 1
795 assert res.organizer_count == 1
796 assert res.subscriber_count == 3
797 assert res.owner_user_id == user1.id
798 assert not res.owner_community_id
799 assert not res.owner_group_id
800 assert res.thread.thread_id
801 assert res.can_edit
802 assert not res.can_moderate
804 with events_session(token2) as api:
805 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
807 assert res.is_next
808 assert res.title == "Dummy Title"
809 assert res.slug == "dummy-title"
810 assert res.content == "Dummy content."
811 assert not res.photo_url
812 assert res.WhichOneof("mode") == "offline_information"
813 assert res.offline_information.lat == 0.1
814 assert res.offline_information.lng == 0.2
815 assert res.offline_information.address == "Near Null Island"
816 assert time_before <= to_aware_datetime(res.created) <= time_before_update
817 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
818 assert res.creator_user_id == user1.id
819 assert to_aware_datetime(res.start_time) == start_time
820 assert to_aware_datetime(res.end_time) == end_time
821 # assert res.timezone == "UTC"
822 assert res.start_time_display
823 assert res.end_time_display
824 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
825 assert not res.organizer
826 assert not res.subscriber
827 assert res.going_count == 2
828 assert res.maybe_count == 1
829 assert res.organizer_count == 1
830 assert res.subscriber_count == 3
831 assert res.owner_user_id == user1.id
832 assert not res.owner_community_id
833 assert not res.owner_group_id
834 assert res.thread.thread_id
835 assert res.can_edit
836 assert res.can_moderate
838 with events_session(token3) as api:
839 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
841 assert res.is_next
842 assert res.title == "Dummy Title"
843 assert res.slug == "dummy-title"
844 assert res.content == "Dummy content."
845 assert not res.photo_url
846 assert res.WhichOneof("mode") == "offline_information"
847 assert res.offline_information.lat == 0.1
848 assert res.offline_information.lng == 0.2
849 assert res.offline_information.address == "Near Null Island"
850 assert time_before <= to_aware_datetime(res.created) <= time_before_update
851 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
852 assert res.creator_user_id == user1.id
853 assert to_aware_datetime(res.start_time) == start_time
854 assert to_aware_datetime(res.end_time) == end_time
855 # assert res.timezone == "UTC"
856 assert res.start_time_display
857 assert res.end_time_display
858 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
859 assert not res.organizer
860 assert not res.subscriber
861 assert res.going_count == 2
862 assert res.maybe_count == 1
863 assert res.organizer_count == 1
864 assert res.subscriber_count == 3
865 assert res.owner_user_id == user1.id
866 assert not res.owner_community_id
867 assert not res.owner_group_id
868 assert res.thread.thread_id
869 assert not res.can_edit
870 assert not res.can_moderate
872 with events_session(token1) as api:
873 res = api.UpdateEvent(
874 events_pb2.UpdateEventReq(
875 event_id=event_id,
876 title=wrappers_pb2.StringValue(value="Dummy Title"),
877 content=wrappers_pb2.StringValue(value="Dummy content."),
878 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
879 start_time=Timestamp_from_datetime(start_time),
880 end_time=Timestamp_from_datetime(end_time),
881 timezone=wrappers_pb2.StringValue(value="UTC"),
882 )
883 )
885 assert res.is_next
886 assert res.title == "Dummy Title"
887 assert res.slug == "dummy-title"
888 assert res.content == "Dummy content."
889 assert not res.photo_url
890 assert res.WhichOneof("mode") == "online_information"
891 assert res.online_information.link == "https://couchers.org/meet/"
892 assert time_before <= to_aware_datetime(res.created) <= time_before_update
893 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
894 assert res.creator_user_id == user1.id
895 assert to_aware_datetime(res.start_time) == start_time
896 assert to_aware_datetime(res.end_time) == end_time
897 # assert res.timezone == "UTC"
898 assert res.start_time_display
899 assert res.end_time_display
900 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
901 assert res.organizer
902 assert res.subscriber
903 assert res.going_count == 2
904 assert res.maybe_count == 1
905 assert res.organizer_count == 1
906 assert res.subscriber_count == 3
907 assert res.owner_user_id == user1.id
908 assert not res.owner_community_id
909 assert not res.owner_group_id
910 assert res.thread.thread_id
911 assert res.can_edit
912 assert not res.can_moderate
914 event_id = res.event_id
916 with events_session(token2) as api:
917 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
919 assert res.is_next
920 assert res.title == "Dummy Title"
921 assert res.slug == "dummy-title"
922 assert res.content == "Dummy content."
923 assert not res.photo_url
924 assert res.WhichOneof("mode") == "online_information"
925 assert res.online_information.link == "https://couchers.org/meet/"
926 assert time_before <= to_aware_datetime(res.created) <= time_before_update
927 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
928 assert res.creator_user_id == user1.id
929 assert to_aware_datetime(res.start_time) == start_time
930 assert to_aware_datetime(res.end_time) == end_time
931 # assert res.timezone == "UTC"
932 assert res.start_time_display
933 assert res.end_time_display
934 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
935 assert not res.organizer
936 assert not res.subscriber
937 assert res.going_count == 2
938 assert res.maybe_count == 1
939 assert res.organizer_count == 1
940 assert res.subscriber_count == 3
941 assert res.owner_user_id == user1.id
942 assert not res.owner_community_id
943 assert not res.owner_group_id
944 assert res.thread.thread_id
945 assert res.can_edit
946 assert res.can_moderate
948 with events_session(token3) as api:
949 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
951 assert res.is_next
952 assert res.title == "Dummy Title"
953 assert res.slug == "dummy-title"
954 assert res.content == "Dummy content."
955 assert not res.photo_url
956 assert res.WhichOneof("mode") == "online_information"
957 assert res.online_information.link == "https://couchers.org/meet/"
958 assert time_before <= to_aware_datetime(res.created) <= time_before_update
959 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
960 assert res.creator_user_id == user1.id
961 assert to_aware_datetime(res.start_time) == start_time
962 assert to_aware_datetime(res.end_time) == end_time
963 # assert res.timezone == "UTC"
964 assert res.start_time_display
965 assert res.end_time_display
966 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
967 assert not res.organizer
968 assert not res.subscriber
969 assert res.going_count == 2
970 assert res.maybe_count == 1
971 assert res.organizer_count == 1
972 assert res.subscriber_count == 3
973 assert res.owner_user_id == user1.id
974 assert not res.owner_community_id
975 assert not res.owner_group_id
976 assert res.thread.thread_id
977 assert not res.can_edit
978 assert not res.can_moderate
980 with events_session(token1) as api:
981 res = api.UpdateEvent(
982 events_pb2.UpdateEventReq(
983 event_id=event_id,
984 offline_information=events_pb2.OfflineEventInformation(
985 address="Near Null Island",
986 lat=0.1,
987 lng=0.2,
988 ),
989 )
990 )
992 with events_session(token3) as api:
993 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
995 assert res.WhichOneof("mode") == "offline_information"
996 assert res.offline_information.address == "Near Null Island"
997 assert res.offline_information.lat == 0.1
998 assert res.offline_information.lng == 0.2
1001def test_UpdateEvent_all(db, moderator: Moderator):
1002 # event creator
1003 user1, token1 = generate_user()
1004 # community moderator
1005 user2, token2 = generate_user()
1006 # third parties
1007 user3, token3 = generate_user()
1008 user4, token4 = generate_user()
1009 user5, token5 = generate_user()
1010 user6, token6 = generate_user()
1012 with session_scope() as session:
1013 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1015 time_before = now()
1016 start_time = now() + timedelta(hours=1)
1017 end_time = start_time + timedelta(hours=1.5)
1019 event_ids = []
1021 with events_session(token1) as api:
1022 res = api.CreateEvent(
1023 events_pb2.CreateEventReq(
1024 title="Dummy Title",
1025 content="0th occurrence",
1026 offline_information=events_pb2.OfflineEventInformation(
1027 address="Near Null Island",
1028 lat=0.1,
1029 lng=0.2,
1030 ),
1031 start_time=Timestamp_from_datetime(start_time),
1032 end_time=Timestamp_from_datetime(end_time),
1033 timezone="UTC",
1034 )
1035 )
1037 event_id = res.event_id
1038 event_ids.append(event_id)
1040 moderator.approve_event_occurrence(event_id)
1042 with events_session(token4) as api:
1043 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1045 with events_session(token5) as api:
1046 api.SetEventAttendance(
1047 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1048 )
1050 with events_session(token6) as api:
1051 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1052 api.SetEventAttendance(
1053 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1054 )
1056 with events_session(token1) as api:
1057 for i in range(5):
1058 res = api.ScheduleEvent(
1059 events_pb2.ScheduleEventReq(
1060 event_id=event_ids[-1],
1061 content=f"{i + 1}th occurrence",
1062 online_information=events_pb2.OnlineEventInformation(
1063 link="https://couchers.org/meet/",
1064 ),
1065 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)),
1066 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)),
1067 timezone="UTC",
1068 )
1069 )
1071 event_ids.append(res.event_id)
1073 # Approve all scheduled occurrences
1074 for eid in event_ids[1:]:
1075 moderator.approve_event_occurrence(eid)
1077 updated_event_id = event_ids[3]
1079 time_before_update = now()
1081 with events_session(token1) as api:
1082 res = api.UpdateEvent(
1083 events_pb2.UpdateEventReq(
1084 event_id=updated_event_id,
1085 title=wrappers_pb2.StringValue(value="New Title"),
1086 content=wrappers_pb2.StringValue(value="New content."),
1087 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
1088 update_all_future=True,
1089 )
1090 )
1092 time_after_update = now()
1094 with events_session(token2) as api:
1095 for i in range(3):
1096 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1097 assert res.content == f"{i}th occurrence"
1098 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
1100 for i in range(3, 6):
1101 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1102 assert res.content == "New content."
1103 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
1106def test_GetEvent(db, moderator: Moderator):
1107 # event creator
1108 user1, token1 = generate_user()
1109 # community moderator
1110 user2, token2 = generate_user()
1111 # third parties
1112 user3, token3 = generate_user()
1113 user4, token4 = generate_user()
1114 user5, token5 = generate_user()
1115 user6, token6 = generate_user()
1117 with session_scope() as session:
1118 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1120 time_before = now()
1121 start_time = now() + timedelta(hours=2)
1122 end_time = start_time + timedelta(hours=3)
1124 with events_session(token1) as api:
1125 # in person event
1126 res = api.CreateEvent(
1127 events_pb2.CreateEventReq(
1128 title="Dummy Title",
1129 content="Dummy content.",
1130 offline_information=events_pb2.OfflineEventInformation(
1131 address="Near Null Island",
1132 lat=0.1,
1133 lng=0.2,
1134 ),
1135 start_time=Timestamp_from_datetime(start_time),
1136 end_time=Timestamp_from_datetime(end_time),
1137 timezone="UTC",
1138 )
1139 )
1141 event_id = res.event_id
1143 moderator.approve_event_occurrence(event_id)
1145 with events_session(token4) as api:
1146 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1148 with events_session(token5) as api:
1149 api.SetEventAttendance(
1150 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1151 )
1153 with events_session(token6) as api:
1154 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1155 api.SetEventAttendance(
1156 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1157 )
1159 with events_session(token1) as api:
1160 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1162 assert res.is_next
1163 assert res.title == "Dummy Title"
1164 assert res.slug == "dummy-title"
1165 assert res.content == "Dummy content."
1166 assert not res.photo_url
1167 assert res.WhichOneof("mode") == "offline_information"
1168 assert res.offline_information.lat == 0.1
1169 assert res.offline_information.lng == 0.2
1170 assert res.offline_information.address == "Near Null Island"
1171 assert time_before <= to_aware_datetime(res.created) <= now()
1172 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1173 assert res.creator_user_id == user1.id
1174 assert to_aware_datetime(res.start_time) == start_time
1175 assert to_aware_datetime(res.end_time) == end_time
1176 # assert res.timezone == "UTC"
1177 assert res.start_time_display
1178 assert res.end_time_display
1179 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1180 assert res.organizer
1181 assert res.subscriber
1182 assert res.going_count == 2
1183 assert res.maybe_count == 1
1184 assert res.organizer_count == 1
1185 assert res.subscriber_count == 3
1186 assert res.owner_user_id == user1.id
1187 assert not res.owner_community_id
1188 assert not res.owner_group_id
1189 assert res.thread.thread_id
1190 assert res.can_edit
1191 assert not res.can_moderate
1193 with events_session(token2) as api:
1194 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1196 assert res.is_next
1197 assert res.title == "Dummy Title"
1198 assert res.slug == "dummy-title"
1199 assert res.content == "Dummy content."
1200 assert not res.photo_url
1201 assert res.WhichOneof("mode") == "offline_information"
1202 assert res.offline_information.lat == 0.1
1203 assert res.offline_information.lng == 0.2
1204 assert res.offline_information.address == "Near Null Island"
1205 assert time_before <= to_aware_datetime(res.created) <= now()
1206 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1207 assert res.creator_user_id == user1.id
1208 assert to_aware_datetime(res.start_time) == start_time
1209 assert to_aware_datetime(res.end_time) == end_time
1210 # assert res.timezone == "UTC"
1211 assert res.start_time_display
1212 assert res.end_time_display
1213 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1214 assert not res.organizer
1215 assert not res.subscriber
1216 assert res.going_count == 2
1217 assert res.maybe_count == 1
1218 assert res.organizer_count == 1
1219 assert res.subscriber_count == 3
1220 assert res.owner_user_id == user1.id
1221 assert not res.owner_community_id
1222 assert not res.owner_group_id
1223 assert res.thread.thread_id
1224 assert res.can_edit
1225 assert res.can_moderate
1227 with events_session(token3) as api:
1228 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1230 assert res.is_next
1231 assert res.title == "Dummy Title"
1232 assert res.slug == "dummy-title"
1233 assert res.content == "Dummy content."
1234 assert not res.photo_url
1235 assert res.WhichOneof("mode") == "offline_information"
1236 assert res.offline_information.lat == 0.1
1237 assert res.offline_information.lng == 0.2
1238 assert res.offline_information.address == "Near Null Island"
1239 assert time_before <= to_aware_datetime(res.created) <= now()
1240 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1241 assert res.creator_user_id == user1.id
1242 assert to_aware_datetime(res.start_time) == start_time
1243 assert to_aware_datetime(res.end_time) == end_time
1244 # assert res.timezone == "UTC"
1245 assert res.start_time_display
1246 assert res.end_time_display
1247 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1248 assert not res.organizer
1249 assert not res.subscriber
1250 assert res.going_count == 2
1251 assert res.maybe_count == 1
1252 assert res.organizer_count == 1
1253 assert res.subscriber_count == 3
1254 assert res.owner_user_id == user1.id
1255 assert not res.owner_community_id
1256 assert not res.owner_group_id
1257 assert res.thread.thread_id
1258 assert not res.can_edit
1259 assert not res.can_moderate
1262def test_CancelEvent(db, moderator: Moderator):
1263 # event creator
1264 user1, token1 = generate_user()
1265 # community moderator
1266 user2, token2 = generate_user()
1267 # third parties
1268 user3, token3 = generate_user()
1269 user4, token4 = generate_user()
1270 user5, token5 = generate_user()
1271 user6, token6 = generate_user()
1273 with session_scope() as session:
1274 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1276 start_time = now() + timedelta(hours=2)
1277 end_time = start_time + timedelta(hours=3)
1279 with events_session(token1) as api:
1280 res = api.CreateEvent(
1281 events_pb2.CreateEventReq(
1282 title="Dummy Title",
1283 content="Dummy content.",
1284 offline_information=events_pb2.OfflineEventInformation(
1285 address="Near Null Island",
1286 lat=0.1,
1287 lng=0.2,
1288 ),
1289 start_time=Timestamp_from_datetime(start_time),
1290 end_time=Timestamp_from_datetime(end_time),
1291 timezone="UTC",
1292 )
1293 )
1295 event_id = res.event_id
1297 moderator.approve_event_occurrence(event_id)
1299 with events_session(token4) as api:
1300 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1302 with events_session(token5) as api:
1303 api.SetEventAttendance(
1304 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1305 )
1307 with events_session(token6) as api:
1308 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1309 api.SetEventAttendance(
1310 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1311 )
1313 with events_session(token1) as api:
1314 res = api.CancelEvent(
1315 events_pb2.CancelEventReq(
1316 event_id=event_id,
1317 )
1318 )
1320 with events_session(token1) as api:
1321 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1322 assert res.is_cancelled
1324 with events_session(token1) as api:
1325 with pytest.raises(grpc.RpcError) as e:
1326 api.UpdateEvent(
1327 events_pb2.UpdateEventReq(
1328 event_id=event_id,
1329 title=wrappers_pb2.StringValue(value="New Title"),
1330 )
1331 )
1332 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1333 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1335 with pytest.raises(grpc.RpcError) as e:
1336 api.InviteEventOrganizer(
1337 events_pb2.InviteEventOrganizerReq(
1338 event_id=event_id,
1339 user_id=user3.id,
1340 )
1341 )
1342 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1343 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1345 with pytest.raises(grpc.RpcError) as e:
1346 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1347 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1348 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1350 with events_session(token3) as api:
1351 with pytest.raises(grpc.RpcError) as e:
1352 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1353 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1354 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1356 with pytest.raises(grpc.RpcError) as e:
1357 api.SetEventAttendance(
1358 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1359 )
1360 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1361 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1363 with events_session(token1) as api:
1364 for include_cancelled in [True, False]:
1365 res = api.ListEventOccurrences(
1366 events_pb2.ListEventOccurrencesReq(
1367 event_id=event_id,
1368 include_cancelled=include_cancelled,
1369 )
1370 )
1371 if include_cancelled:
1372 assert len(res.events) > 0
1373 else:
1374 assert len(res.events) == 0
1376 res = api.ListMyEvents(
1377 events_pb2.ListMyEventsReq(
1378 include_cancelled=include_cancelled,
1379 )
1380 )
1381 if include_cancelled:
1382 assert len(res.events) > 0
1383 else:
1384 assert len(res.events) == 0
1387def test_ListEventAttendees(db, moderator: Moderator):
1388 # event creator
1389 user1, token1 = generate_user()
1390 # others
1391 user2, token2 = generate_user()
1392 user3, token3 = generate_user()
1393 user4, token4 = generate_user()
1394 user5, token5 = generate_user()
1395 user6, token6 = generate_user()
1397 with session_scope() as session:
1398 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1400 with events_session(token1) as api:
1401 event_id = api.CreateEvent(
1402 events_pb2.CreateEventReq(
1403 title="Dummy Title",
1404 content="Dummy content.",
1405 offline_information=events_pb2.OfflineEventInformation(
1406 address="Near Null Island",
1407 lat=0.1,
1408 lng=0.2,
1409 ),
1410 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1411 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1412 timezone="UTC",
1413 )
1414 ).event_id
1416 moderator.approve_event_occurrence(event_id)
1418 for token in [token2, token3, token4, token5]:
1419 with events_session(token) as api:
1420 api.SetEventAttendance(
1421 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1422 )
1424 with events_session(token6) as api:
1425 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1427 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1428 assert res.attendee_user_ids == [user1.id, user2.id]
1430 res = api.ListEventAttendees(
1431 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1432 )
1433 assert res.attendee_user_ids == [user3.id, user4.id]
1435 res = api.ListEventAttendees(
1436 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1437 )
1438 assert res.attendee_user_ids == [user5.id]
1439 assert not res.next_page_token
1442def test_ListEventSubscribers(db, moderator: Moderator):
1443 # event creator
1444 user1, token1 = generate_user()
1445 # others
1446 user2, token2 = generate_user()
1447 user3, token3 = generate_user()
1448 user4, token4 = generate_user()
1449 user5, token5 = generate_user()
1450 user6, token6 = generate_user()
1452 with session_scope() as session:
1453 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1455 with events_session(token1) as api:
1456 event_id = api.CreateEvent(
1457 events_pb2.CreateEventReq(
1458 title="Dummy Title",
1459 content="Dummy content.",
1460 offline_information=events_pb2.OfflineEventInformation(
1461 address="Near Null Island",
1462 lat=0.1,
1463 lng=0.2,
1464 ),
1465 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1466 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1467 timezone="UTC",
1468 )
1469 ).event_id
1471 moderator.approve_event_occurrence(event_id)
1473 for token in [token2, token3, token4, token5]:
1474 with events_session(token) as api:
1475 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1477 with events_session(token6) as api:
1478 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1480 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1481 assert res.subscriber_user_ids == [user1.id, user2.id]
1483 res = api.ListEventSubscribers(
1484 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1485 )
1486 assert res.subscriber_user_ids == [user3.id, user4.id]
1488 res = api.ListEventSubscribers(
1489 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1490 )
1491 assert res.subscriber_user_ids == [user5.id]
1492 assert not res.next_page_token
1495def test_ListEventOrganizers(db, moderator: Moderator):
1496 # event creator
1497 user1, token1 = generate_user()
1498 # others
1499 user2, token2 = generate_user()
1500 user3, token3 = generate_user()
1501 user4, token4 = generate_user()
1502 user5, token5 = generate_user()
1503 user6, token6 = generate_user()
1505 with session_scope() as session:
1506 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1508 with events_session(token1) as api:
1509 event_id = api.CreateEvent(
1510 events_pb2.CreateEventReq(
1511 title="Dummy Title",
1512 content="Dummy content.",
1513 offline_information=events_pb2.OfflineEventInformation(
1514 address="Near Null Island",
1515 lat=0.1,
1516 lng=0.2,
1517 ),
1518 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1519 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1520 timezone="UTC",
1521 )
1522 ).event_id
1524 moderator.approve_event_occurrence(event_id)
1526 with events_session(token1) as api:
1527 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1528 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1530 with events_session(token6) as api:
1531 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1533 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1534 assert res.organizer_user_ids == [user1.id, user2.id]
1536 res = api.ListEventOrganizers(
1537 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1538 )
1539 assert res.organizer_user_ids == [user3.id, user4.id]
1541 res = api.ListEventOrganizers(
1542 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1543 )
1544 assert res.organizer_user_ids == [user5.id]
1545 assert not res.next_page_token
1548def test_TransferEvent(db):
1549 user1, token1 = generate_user()
1550 user2, token2 = generate_user()
1551 user3, token3 = generate_user()
1552 user4, token4 = generate_user()
1554 with session_scope() as session:
1555 c = create_community(session, 0, 2, "Community", [user3], [], None)
1556 h = create_group(session, "Group", [user4], [], c)
1557 c_id = c.id
1558 h_id = h.id
1560 with events_session(token1) as api:
1561 event_id = api.CreateEvent(
1562 events_pb2.CreateEventReq(
1563 title="Dummy Title",
1564 content="Dummy content.",
1565 offline_information=events_pb2.OfflineEventInformation(
1566 address="Near Null Island",
1567 lat=0.1,
1568 lng=0.2,
1569 ),
1570 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1571 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1572 timezone="UTC",
1573 )
1574 ).event_id
1576 api.TransferEvent(
1577 events_pb2.TransferEventReq(
1578 event_id=event_id,
1579 new_owner_community_id=c_id,
1580 )
1581 )
1583 # remove ourselves as organizer, otherwise we can still edit it
1584 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1586 with pytest.raises(grpc.RpcError) as e:
1587 api.TransferEvent(
1588 events_pb2.TransferEventReq(
1589 event_id=event_id,
1590 new_owner_group_id=h_id,
1591 )
1592 )
1593 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1594 assert e.value.details() == "You're not allowed to transfer that event."
1596 event_id = api.CreateEvent(
1597 events_pb2.CreateEventReq(
1598 title="Dummy Title",
1599 content="Dummy content.",
1600 offline_information=events_pb2.OfflineEventInformation(
1601 address="Near Null Island",
1602 lat=0.1,
1603 lng=0.2,
1604 ),
1605 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1606 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1607 timezone="UTC",
1608 )
1609 ).event_id
1611 api.TransferEvent(
1612 events_pb2.TransferEventReq(
1613 event_id=event_id,
1614 new_owner_group_id=h_id,
1615 )
1616 )
1618 # remove ourselves as organizer, otherwise we can still edit it
1619 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1621 with pytest.raises(grpc.RpcError) as e:
1622 api.TransferEvent(
1623 events_pb2.TransferEventReq(
1624 event_id=event_id,
1625 new_owner_community_id=c_id,
1626 )
1627 )
1628 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1629 assert e.value.details() == "You're not allowed to transfer that event."
1632def test_SetEventSubscription(db, moderator: Moderator):
1633 user1, token1 = generate_user()
1634 user2, token2 = generate_user()
1636 with session_scope() as session:
1637 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1639 with events_session(token1) as api:
1640 event_id = api.CreateEvent(
1641 events_pb2.CreateEventReq(
1642 title="Dummy Title",
1643 content="Dummy content.",
1644 offline_information=events_pb2.OfflineEventInformation(
1645 address="Near Null Island",
1646 lat=0.1,
1647 lng=0.2,
1648 ),
1649 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1650 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1651 timezone="UTC",
1652 )
1653 ).event_id
1655 moderator.approve_event_occurrence(event_id)
1657 with events_session(token2) as api:
1658 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1659 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1660 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1661 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1662 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1665def test_SetEventAttendance(db, moderator: Moderator):
1666 user1, token1 = generate_user()
1667 user2, token2 = generate_user()
1669 with session_scope() as session:
1670 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1672 with events_session(token1) as api:
1673 event_id = api.CreateEvent(
1674 events_pb2.CreateEventReq(
1675 title="Dummy Title",
1676 content="Dummy content.",
1677 offline_information=events_pb2.OfflineEventInformation(
1678 address="Near Null Island",
1679 lat=0.1,
1680 lng=0.2,
1681 ),
1682 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1683 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1684 timezone="UTC",
1685 )
1686 ).event_id
1688 moderator.approve_event_occurrence(event_id)
1690 with events_session(token2) as api:
1691 assert (
1692 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1693 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1694 )
1695 api.SetEventAttendance(
1696 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1697 )
1698 assert (
1699 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1700 == events_pb2.ATTENDANCE_STATE_GOING
1701 )
1702 api.SetEventAttendance(
1703 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1704 )
1705 assert (
1706 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1707 == events_pb2.ATTENDANCE_STATE_MAYBE
1708 )
1709 api.SetEventAttendance(
1710 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1711 )
1712 assert (
1713 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1714 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1715 )
1718def test_InviteEventOrganizer(db, moderator: Moderator):
1719 user1, token1 = generate_user()
1720 user2, token2 = generate_user()
1722 with session_scope() as session:
1723 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1725 with events_session(token1) as api:
1726 event_id = api.CreateEvent(
1727 events_pb2.CreateEventReq(
1728 title="Dummy Title",
1729 content="Dummy content.",
1730 offline_information=events_pb2.OfflineEventInformation(
1731 address="Near Null Island",
1732 lat=0.1,
1733 lng=0.2,
1734 ),
1735 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1736 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1737 timezone="UTC",
1738 )
1739 ).event_id
1741 moderator.approve_event_occurrence(event_id)
1743 with events_session(token2) as api:
1744 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1746 with pytest.raises(grpc.RpcError) as e:
1747 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1748 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1749 assert e.value.details() == "You're not allowed to edit that event."
1751 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1753 with events_session(token1) as api:
1754 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1756 with events_session(token2) as api:
1757 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1760def test_ListEventOccurrences(db):
1761 user1, token1 = generate_user()
1762 user2, token2 = generate_user()
1763 user3, token3 = generate_user()
1765 with session_scope() as session:
1766 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1768 start = now()
1770 event_ids = []
1772 with events_session(token1) as api:
1773 res = api.CreateEvent(
1774 events_pb2.CreateEventReq(
1775 title="First occurrence",
1776 content="Dummy content.",
1777 parent_community_id=c_id,
1778 online_information=events_pb2.OnlineEventInformation(
1779 link="https://couchers.org/meet/",
1780 ),
1781 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
1782 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
1783 timezone="UTC",
1784 )
1785 )
1787 event_ids.append(res.event_id)
1789 for i in range(5):
1790 res = api.ScheduleEvent(
1791 events_pb2.ScheduleEventReq(
1792 event_id=event_ids[-1],
1793 content=f"{i}th occurrence",
1794 online_information=events_pb2.OnlineEventInformation(
1795 link="https://couchers.org/meet/",
1796 ),
1797 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
1798 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
1799 timezone="UTC",
1800 )
1801 )
1803 event_ids.append(res.event_id)
1805 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1806 assert [event.event_id for event in res.events] == event_ids[:2]
1808 res = api.ListEventOccurrences(
1809 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1810 )
1811 assert [event.event_id for event in res.events] == event_ids[2:4]
1813 res = api.ListEventOccurrences(
1814 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1815 )
1816 assert [event.event_id for event in res.events] == event_ids[4:6]
1817 assert not res.next_page_token
1820def test_ListMyEvents(db, moderator: Moderator):
1821 user1, token1 = generate_user()
1822 user2, token2 = generate_user()
1823 user3, token3 = generate_user()
1824 user4, token4 = generate_user()
1825 user5, token5 = generate_user()
1827 with session_scope() as session:
1828 # Create global (world) -> macroregion -> region -> subregion hierarchy
1829 # my_communities_exclude_global filters out world, macroregion, and region level communities
1830 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1831 c_id = global_community.id
1832 macroregion_community = create_community(
1833 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1834 )
1835 region_community = create_community(
1836 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1837 )
1838 subregion_community = create_community(
1839 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1840 )
1841 c2_id = subregion_community.id
1843 start = now()
1845 def new_event(hours_from_now: int, community_id: int, online: bool = True) -> events_pb2.CreateEventReq:
1846 if online:
1847 return events_pb2.CreateEventReq(
1848 title="Dummy Online Title",
1849 content="Dummy content.",
1850 online_information=events_pb2.OnlineEventInformation(
1851 link="https://couchers.org/meet/",
1852 ),
1853 parent_community_id=community_id,
1854 timezone="UTC",
1855 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1856 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1857 )
1858 else:
1859 return events_pb2.CreateEventReq(
1860 title="Dummy Offline Title",
1861 content="Dummy content.",
1862 offline_information=events_pb2.OfflineEventInformation(
1863 address="Near Null Island",
1864 lat=0.1,
1865 lng=0.2,
1866 ),
1867 parent_community_id=community_id,
1868 timezone="UTC",
1869 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1870 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1871 )
1873 with events_session(token1) as api:
1874 e2 = api.CreateEvent(new_event(2, c_id, True)).event_id
1876 moderator.approve_event_occurrence(e2)
1878 with events_session(token2) as api:
1879 e1 = api.CreateEvent(new_event(1, c_id, False)).event_id
1881 moderator.approve_event_occurrence(e1)
1883 with events_session(token1) as api:
1884 e3 = api.CreateEvent(new_event(3, c_id, False)).event_id
1886 moderator.approve_event_occurrence(e3)
1888 with events_session(token2) as api:
1889 e5 = api.CreateEvent(new_event(5, c_id, True)).event_id
1891 moderator.approve_event_occurrence(e5)
1893 with events_session(token3) as api:
1894 e4 = api.CreateEvent(new_event(4, c_id, True)).event_id
1896 moderator.approve_event_occurrence(e4)
1898 with events_session(token4) as api:
1899 e6 = api.CreateEvent(new_event(6, c2_id, True)).event_id
1901 moderator.approve_event_occurrence(e6)
1903 with events_session(token1) as api:
1904 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1906 with events_session(token1) as api:
1907 api.SetEventAttendance(
1908 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1909 )
1910 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1912 with events_session(token2) as api:
1913 api.SetEventAttendance(
1914 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1915 )
1917 with events_session(token3) as api:
1918 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1920 with events_session(token1) as api:
1921 # test pagination with token first
1922 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1923 assert [event.event_id for event in res.events] == [e1, e2]
1924 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1925 assert [event.event_id for event in res.events] == [e3, e4]
1926 assert not res.next_page_token
1928 res = api.ListMyEvents(
1929 events_pb2.ListMyEventsReq(
1930 subscribed=True,
1931 attending=True,
1932 organizing=True,
1933 )
1934 )
1935 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1937 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1938 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1940 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1941 assert [event.event_id for event in res.events] == [e2, e3, e4]
1943 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1944 assert [event.event_id for event in res.events] == [e1, e2, e3]
1946 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1947 assert [event.event_id for event in res.events] == [e2, e3]
1949 with events_session(token1) as api:
1950 # Test pagination with page_number and verify total_items
1951 res = api.ListMyEvents(
1952 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1953 )
1954 assert [event.event_id for event in res.events] == [e1, e2]
1955 assert res.total_items == 4
1957 res = api.ListMyEvents(
1958 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1959 )
1960 assert [event.event_id for event in res.events] == [e3, e4]
1961 assert res.total_items == 4
1963 # Verify no more pages
1964 res = api.ListMyEvents(
1965 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1966 )
1967 assert not res.events
1968 assert res.total_items == 4
1970 with events_session(token2) as api:
1971 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1972 assert [event.event_id for event in res.events] == [e1, e3, e5]
1974 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1975 assert [event.event_id for event in res.events] == [e1, e5]
1977 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1978 assert [event.event_id for event in res.events] == [e1, e3, e5]
1980 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1981 assert [event.event_id for event in res.events] == [e1, e5]
1983 with events_session(token3) as api:
1984 # user3 is member of both global (c_id) and child (c2_id) communities
1985 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1986 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1988 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1989 assert [event.event_id for event in res.events] == [e2, e4]
1991 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1992 assert [event.event_id for event in res.events] == [e4]
1994 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1995 assert [event.event_id for event in res.events] == [e3, e4]
1997 # my_communities returns events from both communities user3 is a member of
1998 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1999 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
2001 # my_communities_exclude_global filters out events from global community (node_id=1)
2002 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
2003 assert [event.event_id for event in res.events] == [e6]
2005 # my_communities_exclude_global works independently of my_communities flag
2006 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
2007 assert [event.event_id for event in res.events] == [e6]
2009 # my_communities_exclude_global filters organizing results too
2010 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
2011 assert [event.event_id for event in res.events] == []
2013 # my_communities_exclude_global filters subscribed results too
2014 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
2015 assert [event.event_id for event in res.events] == []
2017 with events_session(token5) as api:
2018 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2019 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
2022def test_RemoveEventOrganizer(db, moderator: Moderator):
2023 user1, token1 = generate_user()
2024 user2, token2 = generate_user()
2026 with session_scope() as session:
2027 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2029 with events_session(token1) as api:
2030 event_id = api.CreateEvent(
2031 events_pb2.CreateEventReq(
2032 title="Dummy Title",
2033 content="Dummy content.",
2034 offline_information=events_pb2.OfflineEventInformation(
2035 address="Near Null Island",
2036 lat=0.1,
2037 lng=0.2,
2038 ),
2039 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2040 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2041 timezone="UTC",
2042 )
2043 ).event_id
2045 moderator.approve_event_occurrence(event_id)
2047 with events_session(token2) as api:
2048 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2050 with pytest.raises(grpc.RpcError) as e:
2051 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2052 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2053 assert e.value.details() == "You're not allowed to edit that event."
2055 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2057 with events_session(token1) as api:
2058 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2060 with pytest.raises(grpc.RpcError) as e:
2061 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2062 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2063 assert e.value.details() == "You cannot remove the event owner as an organizer."
2065 with events_session(token2) as api:
2066 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2067 assert res.organizer
2068 assert res.organizer_count == 2
2069 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2070 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2072 with pytest.raises(grpc.RpcError) as e:
2073 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2074 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2075 assert e.value.details() == "You're not allowed to edit that event."
2077 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2078 assert not res.organizer
2079 assert res.organizer_count == 1
2081 # Test that event owner can remove co-organizers
2082 with events_session(token1) as api:
2083 # Add user2 back as organizer
2084 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2086 # Verify user2 is now an organizer
2087 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2088 assert res.organizer_count == 2
2090 # Event owner can remove co-organizer
2091 api.RemoveEventOrganizer(
2092 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2093 )
2095 # Verify user2 is no longer an organizer
2096 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2097 assert res.organizer_count == 1
2099 # Test that non-organizers cannot remove other organizers
2100 with events_session(token2) as api:
2101 # User2 cannot invite themselves as organizer (not the owner)
2102 with pytest.raises(grpc.RpcError) as e:
2103 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2104 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2105 assert e.value.details() == "You're not allowed to edit that event."
2107 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2108 with events_session(token1) as api:
2109 # Add user2 back as organizer
2110 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2113def test_ListEventAttendees_regression(db):
2114 # see issue #1617:
2115 #
2116 # 1. Create an event
2117 # 2. Transfer the event to a community (although this step probably not necessarily, only needed for it to show up in UI/`ListEvents` from `communities.proto`
2118 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2119 # 4. Change the current user's attendance state to "going" again
2120 #
2121 # **Expected behaviour**
2122 # `ListEventAttendees` should return the current user's ID
2123 #
2124 # **Actual/current behaviour**
2125 # `ListEventAttendees` returns another user's ID. This ID seems to be determined from the row's auto increment ID in `event_occurrence_attendees` in the database
2127 user1, token1 = generate_user()
2128 user2, token2 = generate_user()
2129 user3, token3 = generate_user()
2130 user4, token4 = generate_user()
2131 user5, token5 = generate_user()
2133 with session_scope() as session:
2134 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2136 start_time = now() + timedelta(hours=2)
2137 end_time = start_time + timedelta(hours=3)
2139 with events_session(token1) as api:
2140 res = api.CreateEvent(
2141 events_pb2.CreateEventReq(
2142 title="Dummy Title",
2143 content="Dummy content.",
2144 online_information=events_pb2.OnlineEventInformation(
2145 link="https://couchers.org",
2146 ),
2147 parent_community_id=c_id,
2148 start_time=Timestamp_from_datetime(start_time),
2149 end_time=Timestamp_from_datetime(end_time),
2150 timezone="UTC",
2151 )
2152 )
2154 res = api.TransferEvent(
2155 events_pb2.TransferEventReq(
2156 event_id=res.event_id,
2157 new_owner_community_id=c_id,
2158 )
2159 )
2161 event_id = res.event_id
2163 api.SetEventAttendance(
2164 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2165 )
2166 api.SetEventAttendance(
2167 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2168 )
2170 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2171 assert len(res.attendee_user_ids) == 1
2172 assert res.attendee_user_ids[0] == user1.id
2175def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2176 user1, token1 = generate_user()
2177 user2, token2 = generate_user()
2178 user3, token3 = generate_user()
2179 user4, token4 = generate_user()
2181 with session_scope() as session:
2182 c = create_community(session, 0, 2, "Community", [user3], [], None)
2183 h = create_group(session, "Group", [user4], [], c)
2184 c_id = c.id
2185 h_id = h.id
2186 user4_id = user4.id
2188 with events_session(token1) as api:
2189 event = api.CreateEvent(
2190 events_pb2.CreateEventReq(
2191 title="Dummy Title",
2192 content="Dummy content.",
2193 offline_information=events_pb2.OfflineEventInformation(
2194 address="Near Null Island",
2195 lat=0.1,
2196 lng=0.2,
2197 ),
2198 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2199 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2200 timezone="UTC",
2201 )
2202 )
2204 moderator.approve_event_occurrence(event.event_id)
2206 with threads_session(token2) as api:
2207 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2209 with events_session(token3) as api:
2210 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2211 assert res.thread.num_responses == 1
2213 with threads_session(token3) as api:
2214 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2215 assert len(ret.replies) == 1
2216 assert not ret.next_page_token
2217 assert ret.replies[0].thread_id == reply_id
2218 assert ret.replies[0].content == "hi"
2219 assert ret.replies[0].author_user_id == user2.id
2220 assert ret.replies[0].num_replies == 0
2222 api.PostReply(threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment"))
2224 process_jobs()
2226 assert push_collector.pop_for_user(user1.id, last=True).content.title == f"{user2.name} • Dummy Title"
2227 assert push_collector.pop_for_user(user2.id, last=True).content.title == f"{user3.name} • Dummy Title"
2228 assert push_collector.count_for_user(user4_id) == 0
2231def test_can_overlap_other_events_schedule_regression(db):
2232 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2233 user, token = generate_user()
2235 with session_scope() as session:
2236 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2238 start = now()
2240 with events_session(token) as api:
2241 # create another event, should be able to overlap with this one
2242 api.CreateEvent(
2243 events_pb2.CreateEventReq(
2244 title="Dummy Title",
2245 content="Dummy content.",
2246 parent_community_id=c_id,
2247 online_information=events_pb2.OnlineEventInformation(
2248 link="https://couchers.org/meet/",
2249 ),
2250 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2251 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2252 timezone="UTC",
2253 )
2254 )
2256 # this event
2257 res = api.CreateEvent(
2258 events_pb2.CreateEventReq(
2259 title="Dummy Title",
2260 content="Dummy content.",
2261 parent_community_id=c_id,
2262 online_information=events_pb2.OnlineEventInformation(
2263 link="https://couchers.org/meet/",
2264 ),
2265 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2266 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2267 timezone="UTC",
2268 )
2269 )
2271 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2272 api.ScheduleEvent(
2273 events_pb2.ScheduleEventReq(
2274 event_id=res.event_id,
2275 content="New event occurrence",
2276 offline_information=events_pb2.OfflineEventInformation(
2277 address="A bit further but still near Null Island",
2278 lat=0.3,
2279 lng=0.2,
2280 ),
2281 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2282 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2283 timezone="UTC",
2284 )
2285 )
2288def test_can_overlap_other_events_update_regression(db):
2289 user, token = generate_user()
2291 with session_scope() as session:
2292 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2294 start = now()
2296 with events_session(token) as api:
2297 # create another event, should be able to overlap with this one
2298 api.CreateEvent(
2299 events_pb2.CreateEventReq(
2300 title="Dummy Title",
2301 content="Dummy content.",
2302 parent_community_id=c_id,
2303 online_information=events_pb2.OnlineEventInformation(
2304 link="https://couchers.org/meet/",
2305 ),
2306 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2307 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2308 timezone="UTC",
2309 )
2310 )
2312 res = api.CreateEvent(
2313 events_pb2.CreateEventReq(
2314 title="Dummy Title",
2315 content="Dummy content.",
2316 parent_community_id=c_id,
2317 online_information=events_pb2.OnlineEventInformation(
2318 link="https://couchers.org/meet/",
2319 ),
2320 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2321 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2322 timezone="UTC",
2323 )
2324 )
2326 event_id = api.ScheduleEvent(
2327 events_pb2.ScheduleEventReq(
2328 event_id=res.event_id,
2329 content="New event occurrence",
2330 offline_information=events_pb2.OfflineEventInformation(
2331 address="A bit further but still near Null Island",
2332 lat=0.3,
2333 lng=0.2,
2334 ),
2335 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2336 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2337 timezone="UTC",
2338 )
2339 ).event_id
2341 # can overlap with this current existing occurrence
2342 api.UpdateEvent(
2343 events_pb2.UpdateEventReq(
2344 event_id=event_id,
2345 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2346 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2347 )
2348 )
2350 api.UpdateEvent(
2351 events_pb2.UpdateEventReq(
2352 event_id=event_id,
2353 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2354 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2355 )
2356 )
2359def test_list_past_events_regression(db):
2360 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2361 user, token = generate_user()
2363 with session_scope() as session:
2364 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2366 start = now()
2368 with events_session(token) as api:
2369 api.CreateEvent(
2370 events_pb2.CreateEventReq(
2371 title="Dummy Title",
2372 content="Dummy content.",
2373 parent_community_id=c_id,
2374 online_information=events_pb2.OnlineEventInformation(
2375 link="https://couchers.org/meet/",
2376 ),
2377 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2378 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2379 timezone="UTC",
2380 )
2381 )
2383 with session_scope() as session:
2384 session.execute(
2385 update(EventOccurrence).values(
2386 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2387 )
2388 )
2390 with events_session(token) as api:
2391 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2392 assert len(res.events) == 1
2395def test_community_invite_requests(db, moderator: Moderator):
2396 user1, token1 = generate_user(complete_profile=True)
2397 user2, token2 = generate_user()
2398 user3, token3 = generate_user()
2399 user4, token4 = generate_user()
2400 user5, token5 = generate_user(is_superuser=True)
2402 with session_scope() as session:
2403 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2404 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2405 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2406 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2408 enforce_community_memberships()
2410 with events_session(token1) as api:
2411 res = api.CreateEvent(
2412 events_pb2.CreateEventReq(
2413 title="Dummy Title",
2414 content="Dummy content.",
2415 parent_community_id=c_id,
2416 online_information=events_pb2.OnlineEventInformation(
2417 link="https://couchers.org/meet/",
2418 ),
2419 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2420 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2421 timezone="UTC",
2422 )
2423 )
2424 user_url = f"http://localhost:3000/user/{user1.username}"
2425 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2427 event_id = res.event_id
2429 moderator.approve_event_occurrence(event_id)
2431 with events_session(token1) as api:
2432 with mock_notification_email() as mock:
2433 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2434 assert mock.call_count == 1
2435 e = email_fields(mock)
2436 assert e.recipient == "mods@couchers.org.invalid"
2438 assert user_url in e.plain
2439 assert event_url in e.plain
2441 # can't send another req
2442 with pytest.raises(grpc.RpcError) as err:
2443 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2444 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2445 assert err.value.details() == "You have already requested a community invite for this event."
2447 # another user can send one though
2448 with events_session(token3) as api:
2449 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2451 # but not a non-admin
2452 with events_session(token2) as api:
2453 with pytest.raises(grpc.RpcError) as err:
2454 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2455 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2456 assert err.value.details() == "You're not allowed to edit that event."
2458 with real_editor_session(token5) as editor:
2459 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2460 assert len(res.requests) == 2
2461 assert res.requests[0].user_id == user1.id
2462 assert res.requests[0].approx_users_to_notify == 3
2463 assert res.requests[1].user_id == user3.id
2464 assert res.requests[1].approx_users_to_notify == 3
2466 editor.DecideEventCommunityInviteRequest(
2467 editor_pb2.DecideEventCommunityInviteRequestReq(
2468 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2469 approve=False,
2470 )
2471 )
2473 editor.DecideEventCommunityInviteRequest(
2474 editor_pb2.DecideEventCommunityInviteRequestReq(
2475 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2476 approve=True,
2477 )
2478 )
2480 # not after approve
2481 with events_session(token4) as api:
2482 with pytest.raises(grpc.RpcError) as err:
2483 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2484 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2485 assert err.value.details() == "A community invite has already been sent out for this event."
2488def test_update_event_should_notify_queues_job():
2489 user, token = generate_user()
2490 start = now()
2492 with session_scope() as session:
2493 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2495 # create an event
2496 with events_session(token) as api:
2497 create_res = api.CreateEvent(
2498 events_pb2.CreateEventReq(
2499 title="Dummy Title",
2500 content="Dummy content.",
2501 parent_community_id=c_id,
2502 offline_information=events_pb2.OfflineEventInformation(
2503 address="https://couchers.org/meet/",
2504 lat=1.0,
2505 lng=2.0,
2506 ),
2507 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2508 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2509 timezone="UTC",
2510 )
2511 )
2513 event_id = create_res.event_id
2515 # measure initial background job queue length
2516 with session_scope() as session:
2517 jobs = session.query(BackgroundJob).all()
2518 job_length_before_update = len(jobs)
2520 # update with should_notify=False, expect no change in background job queue
2521 api.UpdateEvent(
2522 events_pb2.UpdateEventReq(
2523 event_id=event_id,
2524 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2525 should_notify=False,
2526 )
2527 )
2529 with session_scope() as session:
2530 jobs = session.query(BackgroundJob).all()
2531 assert len(jobs) == job_length_before_update
2533 # update with should_notify=True, expect one new background job added
2534 api.UpdateEvent(
2535 events_pb2.UpdateEventReq(
2536 event_id=event_id,
2537 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2538 should_notify=True,
2539 )
2540 )
2542 with session_scope() as session:
2543 jobs = session.query(BackgroundJob).all()
2544 assert len(jobs) == job_length_before_update + 1
2547def test_event_photo_key(db):
2548 """Test that events return the photo_key field when a photo is set."""
2549 user, token = generate_user()
2551 start_time = now() + timedelta(hours=2)
2552 end_time = start_time + timedelta(hours=3)
2554 # Create a community and an upload for the event photo
2555 with session_scope() as session:
2556 create_community(session, 0, 2, "Community", [user], [], None)
2557 upload = Upload(
2558 key="test_event_photo_key_123",
2559 filename="test_event_photo_key_123.jpg",
2560 creator_user_id=user.id,
2561 )
2562 session.add(upload)
2564 with events_session(token) as api:
2565 # Create event without photo
2566 res = api.CreateEvent(
2567 events_pb2.CreateEventReq(
2568 title="Event Without Photo",
2569 content="No photo content.",
2570 photo_key=None,
2571 offline_information=events_pb2.OfflineEventInformation(
2572 address="Near Null Island",
2573 lat=0.1,
2574 lng=0.2,
2575 ),
2576 start_time=Timestamp_from_datetime(start_time),
2577 end_time=Timestamp_from_datetime(end_time),
2578 timezone="UTC",
2579 )
2580 )
2582 assert res.photo_key == ""
2583 assert res.photo_url == ""
2585 # Create event with photo
2586 res_with_photo = api.CreateEvent(
2587 events_pb2.CreateEventReq(
2588 title="Event With Photo",
2589 content="Has photo content.",
2590 photo_key="test_event_photo_key_123",
2591 offline_information=events_pb2.OfflineEventInformation(
2592 address="Near Null Island",
2593 lat=0.1,
2594 lng=0.2,
2595 ),
2596 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2597 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2598 timezone="UTC",
2599 )
2600 )
2602 assert res_with_photo.photo_key == "test_event_photo_key_123"
2603 assert "test_event_photo_key_123" in res_with_photo.photo_url
2605 event_id = res_with_photo.event_id
2607 # Verify photo_key is returned when getting the event
2608 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2609 assert get_res.photo_key == "test_event_photo_key_123"
2610 assert "test_event_photo_key_123" in get_res.photo_url
2613def test_event_created_with_shadowed_visibility(db):
2614 """Events start in SHADOWED state when created."""
2615 user, token = generate_user()
2617 with session_scope() as session:
2618 create_community(session, 0, 2, "Community", [user], [], None)
2620 start_time = now() + timedelta(hours=2)
2621 end_time = start_time + timedelta(hours=3)
2623 with events_session(token) as api:
2624 res = api.CreateEvent(
2625 events_pb2.CreateEventReq(
2626 title="Test UMS Event",
2627 content="UMS content.",
2628 offline_information=events_pb2.OfflineEventInformation(
2629 address="Near Null Island",
2630 lat=0.1,
2631 lng=0.2,
2632 ),
2633 start_time=Timestamp_from_datetime(start_time),
2634 end_time=Timestamp_from_datetime(end_time),
2635 timezone="UTC",
2636 )
2637 )
2638 event_id = res.event_id
2640 with session_scope() as session:
2641 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2642 mod_state = session.execute(
2643 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2644 ).scalar_one()
2645 assert mod_state.visibility == ModerationVisibility.shadowed
2648def test_shadowed_event_visible_to_creator_only(db):
2649 """SHADOWED events are visible to the creator but not to other users."""
2650 user1, token1 = generate_user()
2651 user2, token2 = generate_user()
2653 with session_scope() as session:
2654 create_community(session, 0, 2, "Community", [user1], [], None)
2656 start_time = now() + timedelta(hours=2)
2657 end_time = start_time + timedelta(hours=3)
2659 with events_session(token1) as api:
2660 res = api.CreateEvent(
2661 events_pb2.CreateEventReq(
2662 title="Shadowed Event",
2663 content="Content.",
2664 offline_information=events_pb2.OfflineEventInformation(
2665 address="Near Null Island",
2666 lat=0.1,
2667 lng=0.2,
2668 ),
2669 start_time=Timestamp_from_datetime(start_time),
2670 end_time=Timestamp_from_datetime(end_time),
2671 timezone="UTC",
2672 )
2673 )
2674 event_id = res.event_id
2676 # Creator can see it
2677 with events_session(token1) as api:
2678 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2679 assert res.title == "Shadowed Event"
2681 # Other user cannot
2682 with events_session(token2) as api:
2683 with pytest.raises(grpc.RpcError) as e:
2684 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2685 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2688def test_event_visible_after_approval(db, moderator: Moderator):
2689 """Events become visible to all users after moderation approval."""
2690 user1, token1 = generate_user()
2691 user2, token2 = generate_user()
2693 with session_scope() as session:
2694 create_community(session, 0, 2, "Community", [user1], [], None)
2696 start_time = now() + timedelta(hours=2)
2697 end_time = start_time + timedelta(hours=3)
2699 with events_session(token1) as api:
2700 res = api.CreateEvent(
2701 events_pb2.CreateEventReq(
2702 title="Approved Event",
2703 content="Content.",
2704 offline_information=events_pb2.OfflineEventInformation(
2705 address="Near Null Island",
2706 lat=0.1,
2707 lng=0.2,
2708 ),
2709 start_time=Timestamp_from_datetime(start_time),
2710 end_time=Timestamp_from_datetime(end_time),
2711 timezone="UTC",
2712 )
2713 )
2714 event_id = res.event_id
2716 # Other user cannot see it yet
2717 with events_session(token2) as api:
2718 with pytest.raises(grpc.RpcError) as e:
2719 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2720 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2722 # Approve the event
2723 moderator.approve_event_occurrence(event_id)
2725 # Now other user can see it
2726 with events_session(token2) as api:
2727 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2728 assert res.title == "Approved Event"
2731def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2732 """SHADOWED events appear in lists for the creator but not for other users."""
2733 user1, token1 = generate_user()
2734 user2, token2 = generate_user()
2736 with session_scope() as session:
2737 create_community(session, 0, 2, "Community", [user1], [], None)
2739 start_time = now() + timedelta(hours=2)
2740 end_time = start_time + timedelta(hours=3)
2742 with events_session(token1) as api:
2743 res = api.CreateEvent(
2744 events_pb2.CreateEventReq(
2745 title="List Test Event",
2746 content="Content.",
2747 offline_information=events_pb2.OfflineEventInformation(
2748 address="Near Null Island",
2749 lat=0.1,
2750 lng=0.2,
2751 ),
2752 start_time=Timestamp_from_datetime(start_time),
2753 end_time=Timestamp_from_datetime(end_time),
2754 timezone="UTC",
2755 )
2756 )
2757 event_id = res.event_id
2759 # Creator can see their own SHADOWED event in lists
2760 with events_session(token1) as api:
2761 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2762 event_ids = [e.event_id for e in list_res.events]
2763 assert event_id in event_ids
2765 # Other user cannot see the SHADOWED event in lists
2766 with events_session(token2) as api:
2767 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2768 event_ids = [e.event_id for e in list_res.events]
2769 assert event_id not in event_ids
2771 # After approval, other user can see it
2772 moderator.approve_event_occurrence(event_id)
2774 with events_session(token2) as api:
2775 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2776 event_ids = [e.event_id for e in list_res.events]
2777 assert event_id in event_ids
2780def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2781 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2782 user1, token1 = generate_user()
2783 user2, token2 = generate_user()
2785 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2786 with session_scope() as session:
2787 world = create_community(session, 0, 10, "World", [user1], [], None)
2788 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2789 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2790 create_community(session, 0, 2, "Child", [user2], [], region)
2792 start_time = now() + timedelta(hours=2)
2793 end_time = start_time + timedelta(hours=3)
2795 with events_session(token1) as api:
2796 res = api.CreateEvent(
2797 events_pb2.CreateEventReq(
2798 title="Deferred Event",
2799 content="Content.",
2800 offline_information=events_pb2.OfflineEventInformation(
2801 address="Near Null Island",
2802 lat=0.1,
2803 lng=0.2,
2804 ),
2805 start_time=Timestamp_from_datetime(start_time),
2806 end_time=Timestamp_from_datetime(end_time),
2807 timezone="UTC",
2808 )
2809 )
2810 event_id = res.event_id
2812 # Process all jobs — notification should be deferred (event is SHADOWED)
2813 process_jobs()
2815 with session_scope() as session:
2816 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2817 # Notification was created with moderation_state_id for deferral
2818 assert notif.moderation_state_id is not None
2819 # No delivery exists (deferred because event is SHADOWED)
2820 delivery_count = session.execute(
2821 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2822 ).scalar_one_or_none()
2823 assert delivery_count is None
2825 # Approve the event — handle_notification is re-queued for deferred notifications
2826 moderator.approve_event_occurrence(event_id)
2828 # Verify handle_notification job was queued
2829 with session_scope() as session:
2830 pending_jobs = (
2831 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2832 .scalars()
2833 .all()
2834 )
2835 assert any("handle_notification" in j.job_type for j in pending_jobs)
2838def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2839 """Event update notifications should carry the event's moderation_state_id for deferral."""
2840 user1, token1 = generate_user()
2841 user2, token2 = generate_user()
2843 with session_scope() as session:
2844 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2846 start_time = now() + timedelta(hours=2)
2847 end_time = start_time + timedelta(hours=3)
2849 with events_session(token1) as api:
2850 res = api.CreateEvent(
2851 events_pb2.CreateEventReq(
2852 title="Update Test",
2853 content="Content.",
2854 offline_information=events_pb2.OfflineEventInformation(
2855 address="Near Null Island",
2856 lat=0.1,
2857 lng=0.2,
2858 ),
2859 start_time=Timestamp_from_datetime(start_time),
2860 end_time=Timestamp_from_datetime(end_time),
2861 timezone="UTC",
2862 )
2863 )
2864 event_id = res.event_id
2866 moderator.approve_event_occurrence(event_id)
2867 process_jobs()
2868 # Clear any create notifications
2869 while push_collector.count_for_user(user2.id): 2869 ↛ 2870line 2869 didn't jump to line 2870 because the condition on line 2869 was never true
2870 push_collector.pop_for_user(user2.id)
2872 # User2 subscribes to the event
2873 with events_session(token2) as api:
2874 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2876 # User1 updates the event with should_notify=True
2877 with events_session(token1) as api:
2878 api.UpdateEvent(
2879 events_pb2.UpdateEventReq(
2880 event_id=event_id,
2881 title=wrappers_pb2.StringValue(value="Updated Title"),
2882 should_notify=True,
2883 )
2884 )
2886 process_jobs()
2888 # Verify that the update notification for user2 has moderation_state_id set
2889 with session_scope() as session:
2890 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2892 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2893 # Find the update notification (most recent one)
2894 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2895 assert len(update_notifs) == 1
2896 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2899def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2900 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2901 user1, token1 = generate_user()
2902 user2, token2 = generate_user()
2904 with session_scope() as session:
2905 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2907 start_time = now() + timedelta(hours=2)
2908 end_time = start_time + timedelta(hours=3)
2910 with events_session(token1) as api:
2911 res = api.CreateEvent(
2912 events_pb2.CreateEventReq(
2913 title="Cancel Test",
2914 content="Content.",
2915 offline_information=events_pb2.OfflineEventInformation(
2916 address="Near Null Island",
2917 lat=0.1,
2918 lng=0.2,
2919 ),
2920 start_time=Timestamp_from_datetime(start_time),
2921 end_time=Timestamp_from_datetime(end_time),
2922 timezone="UTC",
2923 )
2924 )
2925 event_id = res.event_id
2927 moderator.approve_event_occurrence(event_id)
2928 process_jobs()
2929 while push_collector.count_for_user(user2.id): 2929 ↛ 2930line 2929 didn't jump to line 2930 because the condition on line 2929 was never true
2930 push_collector.pop_for_user(user2.id)
2932 # User2 subscribes
2933 with events_session(token2) as api:
2934 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2936 # User1 cancels the event
2937 with events_session(token1) as api:
2938 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2940 process_jobs()
2942 # Verify that the cancel notification for user2 has moderation_state_id set
2943 with session_scope() as session:
2944 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2946 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2947 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2948 assert len(cancel_notifs) == 1
2949 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2952def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2953 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2954 user1, token1 = generate_user()
2955 user2, token2 = generate_user()
2957 with session_scope() as session:
2958 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2960 # Create event starting 23 hours from now (within 24h reminder window)
2961 start_time = now() + timedelta(hours=23)
2962 end_time = start_time + timedelta(hours=1)
2964 with events_session(token1) as api:
2965 res = api.CreateEvent(
2966 events_pb2.CreateEventReq(
2967 title="Reminder Test",
2968 content="Content.",
2969 offline_information=events_pb2.OfflineEventInformation(
2970 address="Near Null Island",
2971 lat=0.1,
2972 lng=0.2,
2973 ),
2974 start_time=Timestamp_from_datetime(start_time),
2975 end_time=Timestamp_from_datetime(end_time),
2976 timezone="UTC",
2977 )
2978 )
2979 event_id = res.event_id
2981 moderator.approve_event_occurrence(event_id)
2982 process_jobs()
2983 while push_collector.count_for_user(user2.id): 2983 ↛ 2984line 2983 didn't jump to line 2984 because the condition on line 2983 was never true
2984 push_collector.pop_for_user(user2.id)
2986 # User2 marks attendance
2987 with events_session(token2) as api:
2988 api.SetEventAttendance(
2989 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2990 )
2992 # Run the event reminder handler
2993 send_event_reminders(empty_pb2.Empty())
2994 process_jobs()
2996 # Verify that the reminder notification for user2 has moderation_state_id set
2997 with session_scope() as session:
2998 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3000 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3001 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3002 assert len(reminder_notifs) == 1
3003 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3006def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
3007 """Event reminders should not be sent for cancelled events."""
3008 user1, token1 = generate_user()
3009 user2, token2 = generate_user()
3011 with session_scope() as session:
3012 create_community(session, 0, 2, "Community", [user2], [], None)
3014 # Create event starting 23 hours from now (within 24h reminder window)
3015 start_time = now() + timedelta(hours=23)
3016 end_time = start_time + timedelta(hours=1)
3018 with events_session(token1) as api:
3019 res = api.CreateEvent(
3020 events_pb2.CreateEventReq(
3021 title="Cancelled Reminder Test",
3022 content="Content.",
3023 offline_information=events_pb2.OfflineEventInformation(
3024 address="Near Null Island",
3025 lat=0.1,
3026 lng=0.2,
3027 ),
3028 start_time=Timestamp_from_datetime(start_time),
3029 end_time=Timestamp_from_datetime(end_time),
3030 timezone="UTC",
3031 )
3032 )
3033 event_id = res.event_id
3035 moderator.approve_event_occurrence(event_id)
3036 process_jobs()
3038 # User2 marks attendance
3039 with events_session(token2) as api:
3040 api.SetEventAttendance(
3041 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3042 )
3044 # User1 cancels the event
3045 with events_session(token1) as api:
3046 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3048 process_jobs()
3049 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3050 while push_collector.count_for_user(user2.id):
3051 push_collector.pop_for_user(user2.id)
3053 # Run the event reminder handler
3054 send_event_reminders(empty_pb2.Empty())
3055 process_jobs()
3057 # Verify that no reminder notification was sent for user2
3058 with session_scope() as session:
3059 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3060 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3061 assert len(reminder_notifs) == 0
3064def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3065 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3066 user1, token1 = generate_user()
3067 user2, token2 = generate_user()
3069 with session_scope() as session:
3070 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3072 start = now()
3074 # User1 creates event A with 3 occurrences
3075 event_a_ids = []
3076 with events_session(token1) as api:
3077 res = api.CreateEvent(
3078 events_pb2.CreateEventReq(
3079 title="Event A",
3080 content="Content A.",
3081 parent_community_id=c_id,
3082 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3083 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
3084 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
3085 timezone="UTC",
3086 )
3087 )
3088 event_a_ids.append(res.event_id)
3089 for i in range(2):
3090 res = api.ScheduleEvent(
3091 events_pb2.ScheduleEventReq(
3092 event_id=event_a_ids[-1],
3093 content=f"A occurrence {i}",
3094 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3095 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
3096 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
3097 timezone="UTC",
3098 )
3099 )
3100 event_a_ids.append(res.event_id)
3102 # User2 creates event B with 2 occurrences
3103 event_b_ids = []
3104 with events_session(token2) as api:
3105 res = api.CreateEvent(
3106 events_pb2.CreateEventReq(
3107 title="Event B",
3108 content="Content B.",
3109 parent_community_id=c_id,
3110 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3111 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
3112 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
3113 timezone="UTC",
3114 )
3115 )
3116 event_b_ids.append(res.event_id)
3117 res = api.ScheduleEvent(
3118 events_pb2.ScheduleEventReq(
3119 event_id=event_b_ids[-1],
3120 content="B occurrence 1",
3121 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3122 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
3123 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
3124 timezone="UTC",
3125 )
3126 )
3127 event_b_ids.append(res.event_id)
3129 moderator.approve_event_occurrence(event_a_ids[0])
3130 moderator.approve_event_occurrence(event_b_ids[0])
3132 # List occurrences for event A — should only get event A's 3 occurrences
3133 with events_session(token1) as api:
3134 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3135 returned_ids = [e.event_id for e in res.events]
3136 assert sorted(returned_ids) == sorted(event_a_ids)
3138 # List occurrences for event B — should only get event B's 2 occurrences
3139 with events_session(token2) as api:
3140 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3141 returned_ids = [e.event_id for e in res.events]
3142 assert sorted(returned_ids) == sorted(event_b_ids)
3145def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3146 """Event comment notifications should carry the event's moderation_state_id for deferral."""
3147 user1, token1 = generate_user()
3148 user2, token2 = generate_user()
3150 with session_scope() as session:
3151 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3153 start_time = now() + timedelta(hours=2)
3154 end_time = start_time + timedelta(hours=3)
3156 with events_session(token1) as api:
3157 res = api.CreateEvent(
3158 events_pb2.CreateEventReq(
3159 title="Comment Test",
3160 content="Content.",
3161 offline_information=events_pb2.OfflineEventInformation(
3162 address="Near Null Island",
3163 lat=0.1,
3164 lng=0.2,
3165 ),
3166 start_time=Timestamp_from_datetime(start_time),
3167 end_time=Timestamp_from_datetime(end_time),
3168 timezone="UTC",
3169 )
3170 )
3171 event_id = res.event_id
3172 thread_id = res.thread.thread_id
3174 moderator.approve_event_occurrence(event_id)
3175 process_jobs()
3176 while push_collector.count_for_user(user1.id): 3176 ↛ 3177line 3176 didn't jump to line 3177 because the condition on line 3176 was never true
3177 push_collector.pop_for_user(user1.id)
3179 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3180 with events_session(token1) as api:
3181 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3183 # User2 posts a top-level comment on the event thread
3184 with threads_session(token2) as api:
3185 api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!"))
3187 process_jobs()
3189 # The comment notification for user1 should have moderation_state_id set
3190 with session_scope() as session:
3191 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3193 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3194 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3195 assert len(comment_notifs) == 1
3196 assert comment_notifs[0].moderation_state_id == occurrence.moderation_state_id
3199def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3200 """Event thread reply notifications should carry the event's moderation_state_id for deferral."""
3201 user1, token1 = generate_user()
3202 user2, token2 = generate_user()
3203 user3, token3 = generate_user()
3205 with session_scope() as session:
3206 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3208 start_time = now() + timedelta(hours=2)
3209 end_time = start_time + timedelta(hours=3)
3211 with events_session(token1) as api:
3212 res = api.CreateEvent(
3213 events_pb2.CreateEventReq(
3214 title="Reply Test",
3215 content="Content.",
3216 offline_information=events_pb2.OfflineEventInformation(
3217 address="Near Null Island",
3218 lat=0.1,
3219 lng=0.2,
3220 ),
3221 start_time=Timestamp_from_datetime(start_time),
3222 end_time=Timestamp_from_datetime(end_time),
3223 timezone="UTC",
3224 )
3225 )
3226 event_id = res.event_id
3227 thread_id = res.thread.thread_id
3229 moderator.approve_event_occurrence(event_id)
3230 process_jobs()
3231 while push_collector.count_for_user(user1.id): 3231 ↛ 3232line 3231 didn't jump to line 3232 because the condition on line 3231 was never true
3232 push_collector.pop_for_user(user1.id)
3234 # User2 posts a top-level comment
3235 with threads_session(token2) as api:
3236 comment_thread_id = api.PostReply(
3237 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3238 ).thread_id
3240 process_jobs()
3241 while push_collector.count_for_user(user1.id):
3242 push_collector.pop_for_user(user1.id)
3244 # User3 replies to user2's comment (depth=2 reply)
3245 with threads_session(token3) as api:
3246 api.PostReply(threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply"))
3248 process_jobs()
3250 # The nested reply notification for user2 should have moderation_state_id set
3251 with session_scope() as session:
3252 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3254 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3255 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3256 assert len(reply_notifs) == 1
3257 assert reply_notifs[0].moderation_state_id == occurrence.moderation_state_id