Coverage for app / backend / src / tests / test_events.py: 99%
1514 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-19 14:14 +0000
1from datetime import timedelta
3import grpc
4import pytest
5from google.protobuf import empty_pb2, wrappers_pb2
6from psycopg2.extras import DateTimeTZRange
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 Notification,
17 NotificationDelivery,
18 Upload,
19)
20from couchers.proto import editor_pb2, events_pb2, threads_pb2
21from couchers.tasks import enforce_community_memberships
22from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime
23from tests.fixtures.db import generate_user
24from tests.fixtures.misc import Moderator, PushCollector, email_fields, mock_notification_email, process_jobs
25from tests.fixtures.sessions import events_session, real_editor_session, threads_session
26from tests.test_communities import create_community, create_group
29@pytest.fixture(autouse=True)
30def _(testconfig):
31 pass
34def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
35 # test cases:
36 # can create event
37 # cannot create event with missing details
38 # can create online event
39 # can create in person event
40 # can't create event that starts in the past
41 # can create in different timezones
43 # event creator
44 user1, token1 = generate_user()
45 # community moderator
46 user2, token2 = generate_user()
47 # third party
48 user3, token3 = generate_user()
50 with session_scope() as session:
51 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
53 time_before = now()
54 start_time = now() + timedelta(hours=2)
55 end_time = start_time + timedelta(hours=3)
57 with events_session(token1) as api:
58 # in person event
59 res = api.CreateEvent(
60 events_pb2.CreateEventReq(
61 title="Dummy Title",
62 content="Dummy content.",
63 photo_key=None,
64 offline_information=events_pb2.OfflineEventInformation(
65 address="Near Null Island",
66 lat=0.1,
67 lng=0.2,
68 ),
69 start_time=Timestamp_from_datetime(start_time),
70 end_time=Timestamp_from_datetime(end_time),
71 timezone="UTC",
72 )
73 )
75 assert res.is_next
76 assert res.title == "Dummy Title"
77 assert res.slug == "dummy-title"
78 assert res.content == "Dummy content."
79 assert not res.photo_url
80 assert res.WhichOneof("mode") == "offline_information"
81 assert res.offline_information.lat == 0.1
82 assert res.offline_information.lng == 0.2
83 assert res.offline_information.address == "Near Null Island"
84 assert time_before <= to_aware_datetime(res.created) <= now()
85 assert time_before <= to_aware_datetime(res.last_edited) <= now()
86 assert res.creator_user_id == user1.id
87 assert to_aware_datetime(res.start_time) == start_time
88 assert to_aware_datetime(res.end_time) == end_time
89 # assert res.timezone == "UTC"
90 assert res.start_time_display
91 assert res.end_time_display
92 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
93 assert res.organizer
94 assert res.subscriber
95 assert res.going_count == 1
96 assert res.maybe_count == 0
97 assert res.organizer_count == 1
98 assert res.subscriber_count == 1
99 assert res.owner_user_id == user1.id
100 assert not res.owner_community_id
101 assert not res.owner_group_id
102 assert res.thread.thread_id
103 assert res.can_edit
104 assert not res.can_moderate
106 event_id = res.event_id
108 # Approve the event so other users can see it
109 moderator.approve_event_occurrence(event_id)
111 with events_session(token2) as api:
112 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
114 assert res.is_next
115 assert res.title == "Dummy Title"
116 assert res.slug == "dummy-title"
117 assert res.content == "Dummy content."
118 assert not res.photo_url
119 assert res.WhichOneof("mode") == "offline_information"
120 assert res.offline_information.lat == 0.1
121 assert res.offline_information.lng == 0.2
122 assert res.offline_information.address == "Near Null Island"
123 assert time_before <= to_aware_datetime(res.created) <= now()
124 assert time_before <= to_aware_datetime(res.last_edited) <= now()
125 assert res.creator_user_id == user1.id
126 assert to_aware_datetime(res.start_time) == start_time
127 assert to_aware_datetime(res.end_time) == end_time
128 # assert res.timezone == "UTC"
129 assert res.start_time_display
130 assert res.end_time_display
131 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
132 assert not res.organizer
133 assert not res.subscriber
134 assert res.going_count == 1
135 assert res.maybe_count == 0
136 assert res.organizer_count == 1
137 assert res.subscriber_count == 1
138 assert res.owner_user_id == user1.id
139 assert not res.owner_community_id
140 assert not res.owner_group_id
141 assert res.thread.thread_id
142 assert res.can_edit
143 assert res.can_moderate
145 with events_session(token3) as api:
146 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
148 assert res.is_next
149 assert res.title == "Dummy Title"
150 assert res.slug == "dummy-title"
151 assert res.content == "Dummy content."
152 assert not res.photo_url
153 assert res.WhichOneof("mode") == "offline_information"
154 assert res.offline_information.lat == 0.1
155 assert res.offline_information.lng == 0.2
156 assert res.offline_information.address == "Near Null Island"
157 assert time_before <= to_aware_datetime(res.created) <= now()
158 assert time_before <= to_aware_datetime(res.last_edited) <= now()
159 assert res.creator_user_id == user1.id
160 assert to_aware_datetime(res.start_time) == start_time
161 assert to_aware_datetime(res.end_time) == end_time
162 # assert res.timezone == "UTC"
163 assert res.start_time_display
164 assert res.end_time_display
165 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
166 assert not res.organizer
167 assert not res.subscriber
168 assert res.going_count == 1
169 assert res.maybe_count == 0
170 assert res.organizer_count == 1
171 assert res.subscriber_count == 1
172 assert res.owner_user_id == user1.id
173 assert not res.owner_community_id
174 assert not res.owner_group_id
175 assert res.thread.thread_id
176 assert not res.can_edit
177 assert not res.can_moderate
179 with events_session(token1) as api:
180 # online only event
181 res = api.CreateEvent(
182 events_pb2.CreateEventReq(
183 title="Dummy Title",
184 content="Dummy content.",
185 photo_key=None,
186 online_information=events_pb2.OnlineEventInformation(
187 link="https://couchers.org/meet/",
188 ),
189 parent_community_id=c_id,
190 start_time=Timestamp_from_datetime(start_time),
191 end_time=Timestamp_from_datetime(end_time),
192 timezone="UTC",
193 )
194 )
196 assert res.is_next
197 assert res.title == "Dummy Title"
198 assert res.slug == "dummy-title"
199 assert res.content == "Dummy content."
200 assert not res.photo_url
201 assert res.WhichOneof("mode") == "online_information"
202 assert res.online_information.link == "https://couchers.org/meet/"
203 assert time_before <= to_aware_datetime(res.created) <= now()
204 assert time_before <= to_aware_datetime(res.last_edited) <= now()
205 assert res.creator_user_id == user1.id
206 assert to_aware_datetime(res.start_time) == start_time
207 assert to_aware_datetime(res.end_time) == end_time
208 # assert res.timezone == "UTC"
209 assert res.start_time_display
210 assert res.end_time_display
211 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
212 assert res.organizer
213 assert res.subscriber
214 assert res.going_count == 1
215 assert res.maybe_count == 0
216 assert res.organizer_count == 1
217 assert res.subscriber_count == 1
218 assert res.owner_user_id == user1.id
219 assert not res.owner_community_id
220 assert not res.owner_group_id
221 assert res.thread.thread_id
222 assert res.can_edit
223 assert not res.can_moderate
225 event_id = res.event_id
227 # Approve the online event so other users can see it
228 moderator.approve_event_occurrence(event_id)
230 with events_session(token2) as api:
231 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
233 assert res.is_next
234 assert res.title == "Dummy Title"
235 assert res.slug == "dummy-title"
236 assert res.content == "Dummy content."
237 assert not res.photo_url
238 assert res.WhichOneof("mode") == "online_information"
239 assert res.online_information.link == "https://couchers.org/meet/"
240 assert time_before <= to_aware_datetime(res.created) <= now()
241 assert time_before <= to_aware_datetime(res.last_edited) <= now()
242 assert res.creator_user_id == user1.id
243 assert to_aware_datetime(res.start_time) == start_time
244 assert to_aware_datetime(res.end_time) == end_time
245 # assert res.timezone == "UTC"
246 assert res.start_time_display
247 assert res.end_time_display
248 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
249 assert not res.organizer
250 assert not res.subscriber
251 assert res.going_count == 1
252 assert res.maybe_count == 0
253 assert res.organizer_count == 1
254 assert res.subscriber_count == 1
255 assert res.owner_user_id == user1.id
256 assert not res.owner_community_id
257 assert not res.owner_group_id
258 assert res.thread.thread_id
259 assert res.can_edit
260 assert res.can_moderate
262 with events_session(token3) as api:
263 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
265 assert res.is_next
266 assert res.title == "Dummy Title"
267 assert res.slug == "dummy-title"
268 assert res.content == "Dummy content."
269 assert not res.photo_url
270 assert res.WhichOneof("mode") == "online_information"
271 assert res.online_information.link == "https://couchers.org/meet/"
272 assert time_before <= to_aware_datetime(res.created) <= now()
273 assert time_before <= to_aware_datetime(res.last_edited) <= now()
274 assert res.creator_user_id == user1.id
275 assert to_aware_datetime(res.start_time) == start_time
276 assert to_aware_datetime(res.end_time) == end_time
277 # assert res.timezone == "UTC"
278 assert res.start_time_display
279 assert res.end_time_display
280 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
281 assert not res.organizer
282 assert not res.subscriber
283 assert res.going_count == 1
284 assert res.maybe_count == 0
285 assert res.organizer_count == 1
286 assert res.subscriber_count == 1
287 assert res.owner_user_id == user1.id
288 assert not res.owner_community_id
289 assert not res.owner_group_id
290 assert res.thread.thread_id
291 assert not res.can_edit
292 assert not res.can_moderate
294 with events_session(token1) as api:
295 with pytest.raises(grpc.RpcError) as e:
296 api.CreateEvent(
297 events_pb2.CreateEventReq(
298 title="Dummy Title",
299 content="Dummy content.",
300 photo_key=None,
301 online_information=events_pb2.OnlineEventInformation(
302 link="https://couchers.org/meet/",
303 ),
304 start_time=Timestamp_from_datetime(start_time),
305 end_time=Timestamp_from_datetime(end_time),
306 timezone="UTC",
307 )
308 )
309 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
310 assert e.value.details() == "The online event is missing a parent community."
312 with pytest.raises(grpc.RpcError) as e:
313 api.CreateEvent(
314 events_pb2.CreateEventReq(
315 # title="Dummy Title",
316 content="Dummy content.",
317 photo_key=None,
318 offline_information=events_pb2.OfflineEventInformation(
319 address="Near Null Island",
320 lat=0.1,
321 lng=0.1,
322 ),
323 start_time=Timestamp_from_datetime(start_time),
324 end_time=Timestamp_from_datetime(end_time),
325 timezone="UTC",
326 )
327 )
328 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
329 assert e.value.details() == "Missing event title."
331 with pytest.raises(grpc.RpcError) as e:
332 api.CreateEvent(
333 events_pb2.CreateEventReq(
334 title="Dummy Title",
335 # content="Dummy content.",
336 photo_key=None,
337 offline_information=events_pb2.OfflineEventInformation(
338 address="Near Null Island",
339 lat=0.1,
340 lng=0.1,
341 ),
342 start_time=Timestamp_from_datetime(start_time),
343 end_time=Timestamp_from_datetime(end_time),
344 timezone="UTC",
345 )
346 )
347 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
348 assert e.value.details() == "Missing event content."
350 with pytest.raises(grpc.RpcError) as e:
351 api.CreateEvent(
352 events_pb2.CreateEventReq(
353 title="Dummy Title",
354 content="Dummy content.",
355 photo_key="nonexistent",
356 offline_information=events_pb2.OfflineEventInformation(
357 address="Near Null Island",
358 lat=0.1,
359 lng=0.1,
360 ),
361 start_time=Timestamp_from_datetime(start_time),
362 end_time=Timestamp_from_datetime(end_time),
363 timezone="UTC",
364 )
365 )
366 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
367 assert e.value.details() == "Photo not found."
369 with pytest.raises(grpc.RpcError) as e:
370 api.CreateEvent(
371 events_pb2.CreateEventReq(
372 title="Dummy Title",
373 content="Dummy content.",
374 photo_key=None,
375 offline_information=events_pb2.OfflineEventInformation(
376 address="Near Null Island",
377 ),
378 start_time=Timestamp_from_datetime(start_time),
379 end_time=Timestamp_from_datetime(end_time),
380 timezone="UTC",
381 )
382 )
383 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
384 assert e.value.details() == "Missing event address or location."
386 with pytest.raises(grpc.RpcError) as e:
387 api.CreateEvent(
388 events_pb2.CreateEventReq(
389 title="Dummy Title",
390 content="Dummy content.",
391 photo_key=None,
392 offline_information=events_pb2.OfflineEventInformation(
393 lat=0.1,
394 lng=0.1,
395 ),
396 start_time=Timestamp_from_datetime(start_time),
397 end_time=Timestamp_from_datetime(end_time),
398 timezone="UTC",
399 )
400 )
401 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
402 assert e.value.details() == "Missing event address or location."
404 with pytest.raises(grpc.RpcError) as e:
405 api.CreateEvent(
406 events_pb2.CreateEventReq(
407 title="Dummy Title",
408 content="Dummy content.",
409 photo_key=None,
410 online_information=events_pb2.OnlineEventInformation(),
411 start_time=Timestamp_from_datetime(start_time),
412 end_time=Timestamp_from_datetime(end_time),
413 timezone="UTC",
414 )
415 )
416 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
417 assert e.value.details() == "An online-only event requires a link."
419 with pytest.raises(grpc.RpcError) as e:
420 api.CreateEvent(
421 events_pb2.CreateEventReq(
422 title="Dummy Title",
423 content="Dummy content.",
424 parent_community_id=c_id,
425 online_information=events_pb2.OnlineEventInformation(
426 link="https://couchers.org/meet/",
427 ),
428 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)),
429 end_time=Timestamp_from_datetime(end_time),
430 timezone="UTC",
431 )
432 )
433 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
434 assert e.value.details() == "The event must be in the future."
436 with pytest.raises(grpc.RpcError) as e:
437 api.CreateEvent(
438 events_pb2.CreateEventReq(
439 title="Dummy Title",
440 content="Dummy content.",
441 parent_community_id=c_id,
442 online_information=events_pb2.OnlineEventInformation(
443 link="https://couchers.org/meet/",
444 ),
445 start_time=Timestamp_from_datetime(end_time),
446 end_time=Timestamp_from_datetime(start_time),
447 timezone="UTC",
448 )
449 )
450 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
451 assert e.value.details() == "The event must end after it starts."
453 with pytest.raises(grpc.RpcError) as e:
454 api.CreateEvent(
455 events_pb2.CreateEventReq(
456 title="Dummy Title",
457 content="Dummy content.",
458 parent_community_id=c_id,
459 online_information=events_pb2.OnlineEventInformation(
460 link="https://couchers.org/meet/",
461 ),
462 start_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)),
463 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)),
464 timezone="UTC",
465 )
466 )
467 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
468 assert e.value.details() == "The event needs to start within the next year."
470 with pytest.raises(grpc.RpcError) as e:
471 api.CreateEvent(
472 events_pb2.CreateEventReq(
473 title="Dummy Title",
474 content="Dummy content.",
475 parent_community_id=c_id,
476 online_information=events_pb2.OnlineEventInformation(
477 link="https://couchers.org/meet/",
478 ),
479 start_time=Timestamp_from_datetime(start_time),
480 end_time=Timestamp_from_datetime(now() + timedelta(days=100)),
481 timezone="UTC",
482 )
483 )
484 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
485 assert e.value.details() == "Events cannot last longer than 7 days."
488def test_CreateEvent_incomplete_profile(db):
489 user1, token1 = generate_user(complete_profile=False)
490 user2, token2 = generate_user()
492 with session_scope() as session:
493 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
495 start_time = now() + timedelta(hours=2)
496 end_time = start_time + timedelta(hours=3)
498 with events_session(token1) as api:
499 with pytest.raises(grpc.RpcError) as e:
500 api.CreateEvent(
501 events_pb2.CreateEventReq(
502 title="Dummy Title",
503 content="Dummy content.",
504 photo_key=None,
505 offline_information=events_pb2.OfflineEventInformation(
506 address="Near Null Island",
507 lat=0.1,
508 lng=0.2,
509 ),
510 start_time=Timestamp_from_datetime(start_time),
511 end_time=Timestamp_from_datetime(end_time),
512 timezone="UTC",
513 )
514 )
515 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
516 assert e.value.details() == "You have to complete your profile before you can create an event."
519def test_ScheduleEvent(db):
520 # test cases:
521 # can schedule a new event occurrence
523 user, token = generate_user()
525 with session_scope() as session:
526 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
528 time_before = now()
529 start_time = now() + timedelta(hours=2)
530 end_time = start_time + timedelta(hours=3)
532 with events_session(token) as api:
533 res = api.CreateEvent(
534 events_pb2.CreateEventReq(
535 title="Dummy Title",
536 content="Dummy content.",
537 parent_community_id=c_id,
538 online_information=events_pb2.OnlineEventInformation(
539 link="https://couchers.org/meet/",
540 ),
541 start_time=Timestamp_from_datetime(start_time),
542 end_time=Timestamp_from_datetime(end_time),
543 timezone="UTC",
544 )
545 )
547 new_start_time = now() + timedelta(hours=6)
548 new_end_time = new_start_time + timedelta(hours=2)
550 res = api.ScheduleEvent(
551 events_pb2.ScheduleEventReq(
552 event_id=res.event_id,
553 content="New event occurrence",
554 offline_information=events_pb2.OfflineEventInformation(
555 address="A bit further but still near Null Island",
556 lat=0.3,
557 lng=0.2,
558 ),
559 start_time=Timestamp_from_datetime(new_start_time),
560 end_time=Timestamp_from_datetime(new_end_time),
561 timezone="UTC",
562 )
563 )
565 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
567 assert not res.is_next
568 assert res.title == "Dummy Title"
569 assert res.slug == "dummy-title"
570 assert res.content == "New event occurrence"
571 assert not res.photo_url
572 assert res.WhichOneof("mode") == "offline_information"
573 assert res.offline_information.lat == 0.3
574 assert res.offline_information.lng == 0.2
575 assert res.offline_information.address == "A bit further but still near Null Island"
576 assert time_before <= to_aware_datetime(res.created) <= now()
577 assert time_before <= to_aware_datetime(res.last_edited) <= now()
578 assert res.creator_user_id == user.id
579 assert to_aware_datetime(res.start_time) == new_start_time
580 assert to_aware_datetime(res.end_time) == new_end_time
581 # assert res.timezone == "UTC"
582 assert res.start_time_display
583 assert res.end_time_display
584 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
585 assert res.organizer
586 assert res.subscriber
587 assert res.going_count == 1
588 assert res.maybe_count == 0
589 assert res.organizer_count == 1
590 assert res.subscriber_count == 1
591 assert res.owner_user_id == user.id
592 assert not res.owner_community_id
593 assert not res.owner_group_id
594 assert res.thread.thread_id
595 assert res.can_edit
596 assert res.can_moderate
599def test_cannot_overlap_occurrences_schedule(db):
600 user, token = generate_user()
602 with session_scope() as session:
603 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
605 start = now()
607 with events_session(token) as api:
608 res = api.CreateEvent(
609 events_pb2.CreateEventReq(
610 title="Dummy Title",
611 content="Dummy content.",
612 parent_community_id=c_id,
613 online_information=events_pb2.OnlineEventInformation(
614 link="https://couchers.org/meet/",
615 ),
616 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
617 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
618 timezone="UTC",
619 )
620 )
622 with pytest.raises(grpc.RpcError) as e:
623 api.ScheduleEvent(
624 events_pb2.ScheduleEventReq(
625 event_id=res.event_id,
626 content="New event occurrence",
627 offline_information=events_pb2.OfflineEventInformation(
628 address="A bit further but still near Null Island",
629 lat=0.3,
630 lng=0.2,
631 ),
632 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
633 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
634 timezone="UTC",
635 )
636 )
637 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
638 assert e.value.details() == "An event cannot have overlapping occurrences."
641def test_cannot_overlap_occurrences_update(db):
642 user, token = generate_user()
644 with session_scope() as session:
645 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
647 start = now()
649 with events_session(token) as api:
650 res = api.CreateEvent(
651 events_pb2.CreateEventReq(
652 title="Dummy Title",
653 content="Dummy content.",
654 parent_community_id=c_id,
655 online_information=events_pb2.OnlineEventInformation(
656 link="https://couchers.org/meet/",
657 ),
658 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
659 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
660 timezone="UTC",
661 )
662 )
664 event_id = api.ScheduleEvent(
665 events_pb2.ScheduleEventReq(
666 event_id=res.event_id,
667 content="New event occurrence",
668 offline_information=events_pb2.OfflineEventInformation(
669 address="A bit further but still near Null Island",
670 lat=0.3,
671 lng=0.2,
672 ),
673 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
674 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
675 timezone="UTC",
676 )
677 ).event_id
679 # can overlap with this current existing occurrence
680 api.UpdateEvent(
681 events_pb2.UpdateEventReq(
682 event_id=event_id,
683 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
684 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
685 )
686 )
688 with pytest.raises(grpc.RpcError) as e:
689 api.UpdateEvent(
690 events_pb2.UpdateEventReq(
691 event_id=event_id,
692 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
693 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
694 )
695 )
696 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
697 assert e.value.details() == "An event cannot have overlapping occurrences."
700def test_UpdateEvent_single(db, moderator: Moderator):
701 # test cases:
702 # owner can update
703 # community owner can update
704 # can't mess up online/in person dichotomy
705 # notifies attendees
707 # event creator
708 user1, token1 = generate_user()
709 # community moderator
710 user2, token2 = generate_user()
711 # third parties
712 user3, token3 = generate_user()
713 user4, token4 = generate_user()
714 user5, token5 = generate_user()
715 user6, token6 = generate_user()
717 with session_scope() as session:
718 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
720 time_before = now()
721 start_time = now() + timedelta(hours=2)
722 end_time = start_time + timedelta(hours=3)
724 with events_session(token1) as api:
725 res = api.CreateEvent(
726 events_pb2.CreateEventReq(
727 title="Dummy Title",
728 content="Dummy content.",
729 offline_information=events_pb2.OfflineEventInformation(
730 address="Near Null Island",
731 lat=0.1,
732 lng=0.2,
733 ),
734 start_time=Timestamp_from_datetime(start_time),
735 end_time=Timestamp_from_datetime(end_time),
736 timezone="UTC",
737 )
738 )
740 event_id = res.event_id
742 moderator.approve_event_occurrence(event_id)
744 with events_session(token4) as api:
745 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
747 with events_session(token5) as api:
748 api.SetEventAttendance(
749 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
750 )
752 with events_session(token6) as api:
753 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
754 api.SetEventAttendance(
755 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
756 )
758 time_before_update = now()
760 with events_session(token1) as api:
761 res = api.UpdateEvent(
762 events_pb2.UpdateEventReq(
763 event_id=event_id,
764 )
765 )
767 with events_session(token1) as api:
768 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
770 assert res.is_next
771 assert res.title == "Dummy Title"
772 assert res.slug == "dummy-title"
773 assert res.content == "Dummy content."
774 assert not res.photo_url
775 assert res.WhichOneof("mode") == "offline_information"
776 assert res.offline_information.lat == 0.1
777 assert res.offline_information.lng == 0.2
778 assert res.offline_information.address == "Near Null Island"
779 assert time_before <= to_aware_datetime(res.created) <= time_before_update
780 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
781 assert res.creator_user_id == user1.id
782 assert to_aware_datetime(res.start_time) == start_time
783 assert to_aware_datetime(res.end_time) == end_time
784 # assert res.timezone == "UTC"
785 assert res.start_time_display
786 assert res.end_time_display
787 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
788 assert res.organizer
789 assert res.subscriber
790 assert res.going_count == 2
791 assert res.maybe_count == 1
792 assert res.organizer_count == 1
793 assert res.subscriber_count == 3
794 assert res.owner_user_id == user1.id
795 assert not res.owner_community_id
796 assert not res.owner_group_id
797 assert res.thread.thread_id
798 assert res.can_edit
799 assert not res.can_moderate
801 with events_session(token2) as api:
802 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
804 assert res.is_next
805 assert res.title == "Dummy Title"
806 assert res.slug == "dummy-title"
807 assert res.content == "Dummy content."
808 assert not res.photo_url
809 assert res.WhichOneof("mode") == "offline_information"
810 assert res.offline_information.lat == 0.1
811 assert res.offline_information.lng == 0.2
812 assert res.offline_information.address == "Near Null Island"
813 assert time_before <= to_aware_datetime(res.created) <= time_before_update
814 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
815 assert res.creator_user_id == user1.id
816 assert to_aware_datetime(res.start_time) == start_time
817 assert to_aware_datetime(res.end_time) == end_time
818 # assert res.timezone == "UTC"
819 assert res.start_time_display
820 assert res.end_time_display
821 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
822 assert not res.organizer
823 assert not res.subscriber
824 assert res.going_count == 2
825 assert res.maybe_count == 1
826 assert res.organizer_count == 1
827 assert res.subscriber_count == 3
828 assert res.owner_user_id == user1.id
829 assert not res.owner_community_id
830 assert not res.owner_group_id
831 assert res.thread.thread_id
832 assert res.can_edit
833 assert res.can_moderate
835 with events_session(token3) as api:
836 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
838 assert res.is_next
839 assert res.title == "Dummy Title"
840 assert res.slug == "dummy-title"
841 assert res.content == "Dummy content."
842 assert not res.photo_url
843 assert res.WhichOneof("mode") == "offline_information"
844 assert res.offline_information.lat == 0.1
845 assert res.offline_information.lng == 0.2
846 assert res.offline_information.address == "Near Null Island"
847 assert time_before <= to_aware_datetime(res.created) <= time_before_update
848 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
849 assert res.creator_user_id == user1.id
850 assert to_aware_datetime(res.start_time) == start_time
851 assert to_aware_datetime(res.end_time) == end_time
852 # assert res.timezone == "UTC"
853 assert res.start_time_display
854 assert res.end_time_display
855 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
856 assert not res.organizer
857 assert not res.subscriber
858 assert res.going_count == 2
859 assert res.maybe_count == 1
860 assert res.organizer_count == 1
861 assert res.subscriber_count == 3
862 assert res.owner_user_id == user1.id
863 assert not res.owner_community_id
864 assert not res.owner_group_id
865 assert res.thread.thread_id
866 assert not res.can_edit
867 assert not res.can_moderate
869 with events_session(token1) as api:
870 res = api.UpdateEvent(
871 events_pb2.UpdateEventReq(
872 event_id=event_id,
873 title=wrappers_pb2.StringValue(value="Dummy Title"),
874 content=wrappers_pb2.StringValue(value="Dummy content."),
875 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
876 start_time=Timestamp_from_datetime(start_time),
877 end_time=Timestamp_from_datetime(end_time),
878 timezone=wrappers_pb2.StringValue(value="UTC"),
879 )
880 )
882 assert res.is_next
883 assert res.title == "Dummy Title"
884 assert res.slug == "dummy-title"
885 assert res.content == "Dummy content."
886 assert not res.photo_url
887 assert res.WhichOneof("mode") == "online_information"
888 assert res.online_information.link == "https://couchers.org/meet/"
889 assert time_before <= to_aware_datetime(res.created) <= time_before_update
890 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
891 assert res.creator_user_id == user1.id
892 assert to_aware_datetime(res.start_time) == start_time
893 assert to_aware_datetime(res.end_time) == end_time
894 # assert res.timezone == "UTC"
895 assert res.start_time_display
896 assert res.end_time_display
897 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
898 assert res.organizer
899 assert res.subscriber
900 assert res.going_count == 2
901 assert res.maybe_count == 1
902 assert res.organizer_count == 1
903 assert res.subscriber_count == 3
904 assert res.owner_user_id == user1.id
905 assert not res.owner_community_id
906 assert not res.owner_group_id
907 assert res.thread.thread_id
908 assert res.can_edit
909 assert not res.can_moderate
911 event_id = res.event_id
913 with events_session(token2) as api:
914 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
916 assert res.is_next
917 assert res.title == "Dummy Title"
918 assert res.slug == "dummy-title"
919 assert res.content == "Dummy content."
920 assert not res.photo_url
921 assert res.WhichOneof("mode") == "online_information"
922 assert res.online_information.link == "https://couchers.org/meet/"
923 assert time_before <= to_aware_datetime(res.created) <= time_before_update
924 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
925 assert res.creator_user_id == user1.id
926 assert to_aware_datetime(res.start_time) == start_time
927 assert to_aware_datetime(res.end_time) == end_time
928 # assert res.timezone == "UTC"
929 assert res.start_time_display
930 assert res.end_time_display
931 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
932 assert not res.organizer
933 assert not res.subscriber
934 assert res.going_count == 2
935 assert res.maybe_count == 1
936 assert res.organizer_count == 1
937 assert res.subscriber_count == 3
938 assert res.owner_user_id == user1.id
939 assert not res.owner_community_id
940 assert not res.owner_group_id
941 assert res.thread.thread_id
942 assert res.can_edit
943 assert res.can_moderate
945 with events_session(token3) as api:
946 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
948 assert res.is_next
949 assert res.title == "Dummy Title"
950 assert res.slug == "dummy-title"
951 assert res.content == "Dummy content."
952 assert not res.photo_url
953 assert res.WhichOneof("mode") == "online_information"
954 assert res.online_information.link == "https://couchers.org/meet/"
955 assert time_before <= to_aware_datetime(res.created) <= time_before_update
956 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
957 assert res.creator_user_id == user1.id
958 assert to_aware_datetime(res.start_time) == start_time
959 assert to_aware_datetime(res.end_time) == end_time
960 # assert res.timezone == "UTC"
961 assert res.start_time_display
962 assert res.end_time_display
963 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
964 assert not res.organizer
965 assert not res.subscriber
966 assert res.going_count == 2
967 assert res.maybe_count == 1
968 assert res.organizer_count == 1
969 assert res.subscriber_count == 3
970 assert res.owner_user_id == user1.id
971 assert not res.owner_community_id
972 assert not res.owner_group_id
973 assert res.thread.thread_id
974 assert not res.can_edit
975 assert not res.can_moderate
977 with events_session(token1) as api:
978 res = api.UpdateEvent(
979 events_pb2.UpdateEventReq(
980 event_id=event_id,
981 offline_information=events_pb2.OfflineEventInformation(
982 address="Near Null Island",
983 lat=0.1,
984 lng=0.2,
985 ),
986 )
987 )
989 with events_session(token3) as api:
990 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
992 assert res.WhichOneof("mode") == "offline_information"
993 assert res.offline_information.address == "Near Null Island"
994 assert res.offline_information.lat == 0.1
995 assert res.offline_information.lng == 0.2
998def test_UpdateEvent_all(db, moderator: Moderator):
999 # event creator
1000 user1, token1 = generate_user()
1001 # community moderator
1002 user2, token2 = generate_user()
1003 # third parties
1004 user3, token3 = generate_user()
1005 user4, token4 = generate_user()
1006 user5, token5 = generate_user()
1007 user6, token6 = generate_user()
1009 with session_scope() as session:
1010 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1012 time_before = now()
1013 start_time = now() + timedelta(hours=1)
1014 end_time = start_time + timedelta(hours=1.5)
1016 event_ids = []
1018 with events_session(token1) as api:
1019 res = api.CreateEvent(
1020 events_pb2.CreateEventReq(
1021 title="Dummy Title",
1022 content="0th occurrence",
1023 offline_information=events_pb2.OfflineEventInformation(
1024 address="Near Null Island",
1025 lat=0.1,
1026 lng=0.2,
1027 ),
1028 start_time=Timestamp_from_datetime(start_time),
1029 end_time=Timestamp_from_datetime(end_time),
1030 timezone="UTC",
1031 )
1032 )
1034 event_id = res.event_id
1035 event_ids.append(event_id)
1037 moderator.approve_event_occurrence(event_id)
1039 with events_session(token4) as api:
1040 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1042 with events_session(token5) as api:
1043 api.SetEventAttendance(
1044 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1045 )
1047 with events_session(token6) as api:
1048 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1049 api.SetEventAttendance(
1050 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1051 )
1053 with events_session(token1) as api:
1054 for i in range(5):
1055 res = api.ScheduleEvent(
1056 events_pb2.ScheduleEventReq(
1057 event_id=event_ids[-1],
1058 content=f"{i + 1}th occurrence",
1059 online_information=events_pb2.OnlineEventInformation(
1060 link="https://couchers.org/meet/",
1061 ),
1062 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)),
1063 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)),
1064 timezone="UTC",
1065 )
1066 )
1068 event_ids.append(res.event_id)
1070 # Approve all scheduled occurrences
1071 for eid in event_ids[1:]:
1072 moderator.approve_event_occurrence(eid)
1074 updated_event_id = event_ids[3]
1076 time_before_update = now()
1078 with events_session(token1) as api:
1079 res = api.UpdateEvent(
1080 events_pb2.UpdateEventReq(
1081 event_id=updated_event_id,
1082 title=wrappers_pb2.StringValue(value="New Title"),
1083 content=wrappers_pb2.StringValue(value="New content."),
1084 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
1085 update_all_future=True,
1086 )
1087 )
1089 time_after_update = now()
1091 with events_session(token2) as api:
1092 for i in range(3):
1093 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1094 assert res.content == f"{i}th occurrence"
1095 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
1097 for i in range(3, 6):
1098 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
1099 assert res.content == "New content."
1100 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
1103def test_GetEvent(db, moderator: Moderator):
1104 # event creator
1105 user1, token1 = generate_user()
1106 # community moderator
1107 user2, token2 = generate_user()
1108 # third parties
1109 user3, token3 = generate_user()
1110 user4, token4 = generate_user()
1111 user5, token5 = generate_user()
1112 user6, token6 = generate_user()
1114 with session_scope() as session:
1115 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1117 time_before = now()
1118 start_time = now() + timedelta(hours=2)
1119 end_time = start_time + timedelta(hours=3)
1121 with events_session(token1) as api:
1122 # in person event
1123 res = api.CreateEvent(
1124 events_pb2.CreateEventReq(
1125 title="Dummy Title",
1126 content="Dummy content.",
1127 offline_information=events_pb2.OfflineEventInformation(
1128 address="Near Null Island",
1129 lat=0.1,
1130 lng=0.2,
1131 ),
1132 start_time=Timestamp_from_datetime(start_time),
1133 end_time=Timestamp_from_datetime(end_time),
1134 timezone="UTC",
1135 )
1136 )
1138 event_id = res.event_id
1140 moderator.approve_event_occurrence(event_id)
1142 with events_session(token4) as api:
1143 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1145 with events_session(token5) as api:
1146 api.SetEventAttendance(
1147 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1148 )
1150 with events_session(token6) as api:
1151 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1152 api.SetEventAttendance(
1153 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1154 )
1156 with events_session(token1) as api:
1157 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1159 assert res.is_next
1160 assert res.title == "Dummy Title"
1161 assert res.slug == "dummy-title"
1162 assert res.content == "Dummy content."
1163 assert not res.photo_url
1164 assert res.WhichOneof("mode") == "offline_information"
1165 assert res.offline_information.lat == 0.1
1166 assert res.offline_information.lng == 0.2
1167 assert res.offline_information.address == "Near Null Island"
1168 assert time_before <= to_aware_datetime(res.created) <= now()
1169 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1170 assert res.creator_user_id == user1.id
1171 assert to_aware_datetime(res.start_time) == start_time
1172 assert to_aware_datetime(res.end_time) == end_time
1173 # assert res.timezone == "UTC"
1174 assert res.start_time_display
1175 assert res.end_time_display
1176 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
1177 assert res.organizer
1178 assert res.subscriber
1179 assert res.going_count == 2
1180 assert res.maybe_count == 1
1181 assert res.organizer_count == 1
1182 assert res.subscriber_count == 3
1183 assert res.owner_user_id == user1.id
1184 assert not res.owner_community_id
1185 assert not res.owner_group_id
1186 assert res.thread.thread_id
1187 assert res.can_edit
1188 assert not res.can_moderate
1190 with events_session(token2) as api:
1191 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1193 assert res.is_next
1194 assert res.title == "Dummy Title"
1195 assert res.slug == "dummy-title"
1196 assert res.content == "Dummy content."
1197 assert not res.photo_url
1198 assert res.WhichOneof("mode") == "offline_information"
1199 assert res.offline_information.lat == 0.1
1200 assert res.offline_information.lng == 0.2
1201 assert res.offline_information.address == "Near Null Island"
1202 assert time_before <= to_aware_datetime(res.created) <= now()
1203 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1204 assert res.creator_user_id == user1.id
1205 assert to_aware_datetime(res.start_time) == start_time
1206 assert to_aware_datetime(res.end_time) == end_time
1207 # assert res.timezone == "UTC"
1208 assert res.start_time_display
1209 assert res.end_time_display
1210 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1211 assert not res.organizer
1212 assert not res.subscriber
1213 assert res.going_count == 2
1214 assert res.maybe_count == 1
1215 assert res.organizer_count == 1
1216 assert res.subscriber_count == 3
1217 assert res.owner_user_id == user1.id
1218 assert not res.owner_community_id
1219 assert not res.owner_group_id
1220 assert res.thread.thread_id
1221 assert res.can_edit
1222 assert res.can_moderate
1224 with events_session(token3) as api:
1225 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1227 assert res.is_next
1228 assert res.title == "Dummy Title"
1229 assert res.slug == "dummy-title"
1230 assert res.content == "Dummy content."
1231 assert not res.photo_url
1232 assert res.WhichOneof("mode") == "offline_information"
1233 assert res.offline_information.lat == 0.1
1234 assert res.offline_information.lng == 0.2
1235 assert res.offline_information.address == "Near Null Island"
1236 assert time_before <= to_aware_datetime(res.created) <= now()
1237 assert time_before <= to_aware_datetime(res.last_edited) <= now()
1238 assert res.creator_user_id == user1.id
1239 assert to_aware_datetime(res.start_time) == start_time
1240 assert to_aware_datetime(res.end_time) == end_time
1241 # assert res.timezone == "UTC"
1242 assert res.start_time_display
1243 assert res.end_time_display
1244 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
1245 assert not res.organizer
1246 assert not res.subscriber
1247 assert res.going_count == 2
1248 assert res.maybe_count == 1
1249 assert res.organizer_count == 1
1250 assert res.subscriber_count == 3
1251 assert res.owner_user_id == user1.id
1252 assert not res.owner_community_id
1253 assert not res.owner_group_id
1254 assert res.thread.thread_id
1255 assert not res.can_edit
1256 assert not res.can_moderate
1259def test_CancelEvent(db, moderator: Moderator):
1260 # event creator
1261 user1, token1 = generate_user()
1262 # community moderator
1263 user2, token2 = generate_user()
1264 # third parties
1265 user3, token3 = generate_user()
1266 user4, token4 = generate_user()
1267 user5, token5 = generate_user()
1268 user6, token6 = generate_user()
1270 with session_scope() as session:
1271 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1273 start_time = now() + timedelta(hours=2)
1274 end_time = start_time + timedelta(hours=3)
1276 with events_session(token1) as api:
1277 res = api.CreateEvent(
1278 events_pb2.CreateEventReq(
1279 title="Dummy Title",
1280 content="Dummy content.",
1281 offline_information=events_pb2.OfflineEventInformation(
1282 address="Near Null Island",
1283 lat=0.1,
1284 lng=0.2,
1285 ),
1286 start_time=Timestamp_from_datetime(start_time),
1287 end_time=Timestamp_from_datetime(end_time),
1288 timezone="UTC",
1289 )
1290 )
1292 event_id = res.event_id
1294 moderator.approve_event_occurrence(event_id)
1296 with events_session(token4) as api:
1297 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1299 with events_session(token5) as api:
1300 api.SetEventAttendance(
1301 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1302 )
1304 with events_session(token6) as api:
1305 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1306 api.SetEventAttendance(
1307 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1308 )
1310 with events_session(token1) as api:
1311 res = api.CancelEvent(
1312 events_pb2.CancelEventReq(
1313 event_id=event_id,
1314 )
1315 )
1317 with events_session(token1) as api:
1318 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1319 assert res.is_cancelled
1321 with events_session(token1) as api:
1322 with pytest.raises(grpc.RpcError) as e:
1323 api.UpdateEvent(
1324 events_pb2.UpdateEventReq(
1325 event_id=event_id,
1326 title=wrappers_pb2.StringValue(value="New Title"),
1327 )
1328 )
1329 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1330 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1332 with pytest.raises(grpc.RpcError) as e:
1333 api.InviteEventOrganizer(
1334 events_pb2.InviteEventOrganizerReq(
1335 event_id=event_id,
1336 user_id=user3.id,
1337 )
1338 )
1339 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1340 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1342 with pytest.raises(grpc.RpcError) as e:
1343 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1344 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1345 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1347 with events_session(token3) as api:
1348 with pytest.raises(grpc.RpcError) as e:
1349 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1350 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1351 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1353 with pytest.raises(grpc.RpcError) as e:
1354 api.SetEventAttendance(
1355 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1356 )
1357 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1358 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1360 with events_session(token1) as api:
1361 for include_cancelled in [True, False]:
1362 res = api.ListEventOccurrences(
1363 events_pb2.ListEventOccurrencesReq(
1364 event_id=event_id,
1365 include_cancelled=include_cancelled,
1366 )
1367 )
1368 if include_cancelled:
1369 assert len(res.events) > 0
1370 else:
1371 assert len(res.events) == 0
1373 res = api.ListMyEvents(
1374 events_pb2.ListMyEventsReq(
1375 include_cancelled=include_cancelled,
1376 )
1377 )
1378 if include_cancelled:
1379 assert len(res.events) > 0
1380 else:
1381 assert len(res.events) == 0
1384def test_ListEventAttendees(db, moderator: Moderator):
1385 # event creator
1386 user1, token1 = generate_user()
1387 # others
1388 user2, token2 = generate_user()
1389 user3, token3 = generate_user()
1390 user4, token4 = generate_user()
1391 user5, token5 = generate_user()
1392 user6, token6 = generate_user()
1394 with session_scope() as session:
1395 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1397 with events_session(token1) as api:
1398 event_id = api.CreateEvent(
1399 events_pb2.CreateEventReq(
1400 title="Dummy Title",
1401 content="Dummy content.",
1402 offline_information=events_pb2.OfflineEventInformation(
1403 address="Near Null Island",
1404 lat=0.1,
1405 lng=0.2,
1406 ),
1407 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1408 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1409 timezone="UTC",
1410 )
1411 ).event_id
1413 moderator.approve_event_occurrence(event_id)
1415 for token in [token2, token3, token4, token5]:
1416 with events_session(token) as api:
1417 api.SetEventAttendance(
1418 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1419 )
1421 with events_session(token6) as api:
1422 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1424 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1425 assert res.attendee_user_ids == [user1.id, user2.id]
1427 res = api.ListEventAttendees(
1428 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1429 )
1430 assert res.attendee_user_ids == [user3.id, user4.id]
1432 res = api.ListEventAttendees(
1433 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1434 )
1435 assert res.attendee_user_ids == [user5.id]
1436 assert not res.next_page_token
1439def test_ListEventSubscribers(db, moderator: Moderator):
1440 # event creator
1441 user1, token1 = generate_user()
1442 # others
1443 user2, token2 = generate_user()
1444 user3, token3 = generate_user()
1445 user4, token4 = generate_user()
1446 user5, token5 = generate_user()
1447 user6, token6 = generate_user()
1449 with session_scope() as session:
1450 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1452 with events_session(token1) as api:
1453 event_id = api.CreateEvent(
1454 events_pb2.CreateEventReq(
1455 title="Dummy Title",
1456 content="Dummy content.",
1457 offline_information=events_pb2.OfflineEventInformation(
1458 address="Near Null Island",
1459 lat=0.1,
1460 lng=0.2,
1461 ),
1462 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1463 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1464 timezone="UTC",
1465 )
1466 ).event_id
1468 moderator.approve_event_occurrence(event_id)
1470 for token in [token2, token3, token4, token5]:
1471 with events_session(token) as api:
1472 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1474 with events_session(token6) as api:
1475 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1477 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1478 assert res.subscriber_user_ids == [user1.id, user2.id]
1480 res = api.ListEventSubscribers(
1481 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1482 )
1483 assert res.subscriber_user_ids == [user3.id, user4.id]
1485 res = api.ListEventSubscribers(
1486 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1487 )
1488 assert res.subscriber_user_ids == [user5.id]
1489 assert not res.next_page_token
1492def test_ListEventOrganizers(db, moderator: Moderator):
1493 # event creator
1494 user1, token1 = generate_user()
1495 # others
1496 user2, token2 = generate_user()
1497 user3, token3 = generate_user()
1498 user4, token4 = generate_user()
1499 user5, token5 = generate_user()
1500 user6, token6 = generate_user()
1502 with session_scope() as session:
1503 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1505 with events_session(token1) as api:
1506 event_id = api.CreateEvent(
1507 events_pb2.CreateEventReq(
1508 title="Dummy Title",
1509 content="Dummy content.",
1510 offline_information=events_pb2.OfflineEventInformation(
1511 address="Near Null Island",
1512 lat=0.1,
1513 lng=0.2,
1514 ),
1515 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1516 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1517 timezone="UTC",
1518 )
1519 ).event_id
1521 moderator.approve_event_occurrence(event_id)
1523 with events_session(token1) as api:
1524 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1525 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1527 with events_session(token6) as api:
1528 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1530 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1531 assert res.organizer_user_ids == [user1.id, user2.id]
1533 res = api.ListEventOrganizers(
1534 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1535 )
1536 assert res.organizer_user_ids == [user3.id, user4.id]
1538 res = api.ListEventOrganizers(
1539 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1540 )
1541 assert res.organizer_user_ids == [user5.id]
1542 assert not res.next_page_token
1545def test_TransferEvent(db):
1546 user1, token1 = generate_user()
1547 user2, token2 = generate_user()
1548 user3, token3 = generate_user()
1549 user4, token4 = generate_user()
1551 with session_scope() as session:
1552 c = create_community(session, 0, 2, "Community", [user3], [], None)
1553 h = create_group(session, "Group", [user4], [], c)
1554 c_id = c.id
1555 h_id = h.id
1557 with events_session(token1) as api:
1558 event_id = api.CreateEvent(
1559 events_pb2.CreateEventReq(
1560 title="Dummy Title",
1561 content="Dummy content.",
1562 offline_information=events_pb2.OfflineEventInformation(
1563 address="Near Null Island",
1564 lat=0.1,
1565 lng=0.2,
1566 ),
1567 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1568 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1569 timezone="UTC",
1570 )
1571 ).event_id
1573 api.TransferEvent(
1574 events_pb2.TransferEventReq(
1575 event_id=event_id,
1576 new_owner_community_id=c_id,
1577 )
1578 )
1580 # remove ourselves as organizer, otherwise we can still edit it
1581 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1583 with pytest.raises(grpc.RpcError) as e:
1584 api.TransferEvent(
1585 events_pb2.TransferEventReq(
1586 event_id=event_id,
1587 new_owner_group_id=h_id,
1588 )
1589 )
1590 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1591 assert e.value.details() == "You're not allowed to transfer that event."
1593 event_id = api.CreateEvent(
1594 events_pb2.CreateEventReq(
1595 title="Dummy Title",
1596 content="Dummy content.",
1597 offline_information=events_pb2.OfflineEventInformation(
1598 address="Near Null Island",
1599 lat=0.1,
1600 lng=0.2,
1601 ),
1602 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1603 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1604 timezone="UTC",
1605 )
1606 ).event_id
1608 api.TransferEvent(
1609 events_pb2.TransferEventReq(
1610 event_id=event_id,
1611 new_owner_group_id=h_id,
1612 )
1613 )
1615 # remove ourselves as organizer, otherwise we can still edit it
1616 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1618 with pytest.raises(grpc.RpcError) as e:
1619 api.TransferEvent(
1620 events_pb2.TransferEventReq(
1621 event_id=event_id,
1622 new_owner_community_id=c_id,
1623 )
1624 )
1625 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1626 assert e.value.details() == "You're not allowed to transfer that event."
1629def test_SetEventSubscription(db, moderator: Moderator):
1630 user1, token1 = generate_user()
1631 user2, token2 = generate_user()
1633 with session_scope() as session:
1634 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1636 with events_session(token1) as api:
1637 event_id = api.CreateEvent(
1638 events_pb2.CreateEventReq(
1639 title="Dummy Title",
1640 content="Dummy content.",
1641 offline_information=events_pb2.OfflineEventInformation(
1642 address="Near Null Island",
1643 lat=0.1,
1644 lng=0.2,
1645 ),
1646 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1647 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1648 timezone="UTC",
1649 )
1650 ).event_id
1652 moderator.approve_event_occurrence(event_id)
1654 with events_session(token2) as api:
1655 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1656 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1657 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1658 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1659 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1662def test_SetEventAttendance(db, moderator: Moderator):
1663 user1, token1 = generate_user()
1664 user2, token2 = generate_user()
1666 with session_scope() as session:
1667 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1669 with events_session(token1) as api:
1670 event_id = api.CreateEvent(
1671 events_pb2.CreateEventReq(
1672 title="Dummy Title",
1673 content="Dummy content.",
1674 offline_information=events_pb2.OfflineEventInformation(
1675 address="Near Null Island",
1676 lat=0.1,
1677 lng=0.2,
1678 ),
1679 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1680 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1681 timezone="UTC",
1682 )
1683 ).event_id
1685 moderator.approve_event_occurrence(event_id)
1687 with events_session(token2) as api:
1688 assert (
1689 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1690 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1691 )
1692 api.SetEventAttendance(
1693 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1694 )
1695 assert (
1696 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1697 == events_pb2.ATTENDANCE_STATE_GOING
1698 )
1699 api.SetEventAttendance(
1700 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1701 )
1702 assert (
1703 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1704 == events_pb2.ATTENDANCE_STATE_MAYBE
1705 )
1706 api.SetEventAttendance(
1707 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1708 )
1709 assert (
1710 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1711 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1712 )
1715def test_InviteEventOrganizer(db, moderator: Moderator):
1716 user1, token1 = generate_user()
1717 user2, token2 = generate_user()
1719 with session_scope() as session:
1720 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1722 with events_session(token1) as api:
1723 event_id = api.CreateEvent(
1724 events_pb2.CreateEventReq(
1725 title="Dummy Title",
1726 content="Dummy content.",
1727 offline_information=events_pb2.OfflineEventInformation(
1728 address="Near Null Island",
1729 lat=0.1,
1730 lng=0.2,
1731 ),
1732 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
1733 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
1734 timezone="UTC",
1735 )
1736 ).event_id
1738 moderator.approve_event_occurrence(event_id)
1740 with events_session(token2) as api:
1741 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1743 with pytest.raises(grpc.RpcError) as e:
1744 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1745 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1746 assert e.value.details() == "You're not allowed to edit that event."
1748 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1750 with events_session(token1) as api:
1751 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1753 with events_session(token2) as api:
1754 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1757def test_ListEventOccurrences(db):
1758 user1, token1 = generate_user()
1759 user2, token2 = generate_user()
1760 user3, token3 = generate_user()
1762 with session_scope() as session:
1763 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1765 start = now()
1767 event_ids = []
1769 with events_session(token1) as api:
1770 res = api.CreateEvent(
1771 events_pb2.CreateEventReq(
1772 title="First occurrence",
1773 content="Dummy content.",
1774 parent_community_id=c_id,
1775 online_information=events_pb2.OnlineEventInformation(
1776 link="https://couchers.org/meet/",
1777 ),
1778 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
1779 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
1780 timezone="UTC",
1781 )
1782 )
1784 event_ids.append(res.event_id)
1786 for i in range(5):
1787 res = api.ScheduleEvent(
1788 events_pb2.ScheduleEventReq(
1789 event_id=event_ids[-1],
1790 content=f"{i}th occurrence",
1791 online_information=events_pb2.OnlineEventInformation(
1792 link="https://couchers.org/meet/",
1793 ),
1794 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
1795 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
1796 timezone="UTC",
1797 )
1798 )
1800 event_ids.append(res.event_id)
1802 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1803 assert [event.event_id for event in res.events] == event_ids[:2]
1805 res = api.ListEventOccurrences(
1806 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1807 )
1808 assert [event.event_id for event in res.events] == event_ids[2:4]
1810 res = api.ListEventOccurrences(
1811 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1812 )
1813 assert [event.event_id for event in res.events] == event_ids[4:6]
1814 assert not res.next_page_token
1817def test_ListMyEvents(db, moderator: Moderator):
1818 user1, token1 = generate_user()
1819 user2, token2 = generate_user()
1820 user3, token3 = generate_user()
1821 user4, token4 = generate_user()
1822 user5, token5 = generate_user()
1824 with session_scope() as session:
1825 # Create global (world) -> macroregion -> region -> subregion hierarchy
1826 # my_communities_exclude_global filters out world, macroregion, and region level communities
1827 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1828 c_id = global_community.id
1829 macroregion_community = create_community(
1830 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1831 )
1832 region_community = create_community(
1833 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1834 )
1835 subregion_community = create_community(
1836 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1837 )
1838 c2_id = subregion_community.id
1840 start = now()
1842 def new_event(hours_from_now: int, community_id: int, online: bool = True) -> events_pb2.CreateEventReq:
1843 if online:
1844 return events_pb2.CreateEventReq(
1845 title="Dummy Online Title",
1846 content="Dummy content.",
1847 online_information=events_pb2.OnlineEventInformation(
1848 link="https://couchers.org/meet/",
1849 ),
1850 parent_community_id=community_id,
1851 timezone="UTC",
1852 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1853 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1854 )
1855 else:
1856 return events_pb2.CreateEventReq(
1857 title="Dummy Offline Title",
1858 content="Dummy content.",
1859 offline_information=events_pb2.OfflineEventInformation(
1860 address="Near Null Island",
1861 lat=0.1,
1862 lng=0.2,
1863 ),
1864 parent_community_id=community_id,
1865 timezone="UTC",
1866 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)),
1867 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)),
1868 )
1870 with events_session(token1) as api:
1871 e2 = api.CreateEvent(new_event(2, c_id, True)).event_id
1873 moderator.approve_event_occurrence(e2)
1875 with events_session(token2) as api:
1876 e1 = api.CreateEvent(new_event(1, c_id, False)).event_id
1878 moderator.approve_event_occurrence(e1)
1880 with events_session(token1) as api:
1881 e3 = api.CreateEvent(new_event(3, c_id, False)).event_id
1883 moderator.approve_event_occurrence(e3)
1885 with events_session(token2) as api:
1886 e5 = api.CreateEvent(new_event(5, c_id, True)).event_id
1888 moderator.approve_event_occurrence(e5)
1890 with events_session(token3) as api:
1891 e4 = api.CreateEvent(new_event(4, c_id, True)).event_id
1893 moderator.approve_event_occurrence(e4)
1895 with events_session(token4) as api:
1896 e6 = api.CreateEvent(new_event(6, c2_id, True)).event_id
1898 moderator.approve_event_occurrence(e6)
1900 with events_session(token1) as api:
1901 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1903 with events_session(token1) as api:
1904 api.SetEventAttendance(
1905 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_MAYBE)
1906 )
1907 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1909 with events_session(token2) as api:
1910 api.SetEventAttendance(
1911 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1912 )
1914 with events_session(token3) as api:
1915 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1917 with events_session(token1) as api:
1918 # test pagination with token first
1919 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1920 assert [event.event_id for event in res.events] == [e1, e2]
1921 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1922 assert [event.event_id for event in res.events] == [e3, e4]
1923 assert not res.next_page_token
1925 res = api.ListMyEvents(
1926 events_pb2.ListMyEventsReq(
1927 subscribed=True,
1928 attending=True,
1929 organizing=True,
1930 )
1931 )
1932 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1934 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1935 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1937 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1938 assert [event.event_id for event in res.events] == [e2, e3, e4]
1940 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1941 assert [event.event_id for event in res.events] == [e1, e2, e3]
1943 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1944 assert [event.event_id for event in res.events] == [e2, e3]
1946 with events_session(token1) as api:
1947 # Test pagination with page_number and verify total_items
1948 res = api.ListMyEvents(
1949 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1950 )
1951 assert [event.event_id for event in res.events] == [e1, e2]
1952 assert res.total_items == 4
1954 res = api.ListMyEvents(
1955 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1956 )
1957 assert [event.event_id for event in res.events] == [e3, e4]
1958 assert res.total_items == 4
1960 # Verify no more pages
1961 res = api.ListMyEvents(
1962 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1963 )
1964 assert not res.events
1965 assert res.total_items == 4
1967 with events_session(token2) as api:
1968 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1969 assert [event.event_id for event in res.events] == [e1, e3, e5]
1971 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1972 assert [event.event_id for event in res.events] == [e1, e5]
1974 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1975 assert [event.event_id for event in res.events] == [e1, e3, e5]
1977 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1978 assert [event.event_id for event in res.events] == [e1, e5]
1980 with events_session(token3) as api:
1981 # user3 is member of both global (c_id) and child (c2_id) communities
1982 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1983 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1985 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1986 assert [event.event_id for event in res.events] == [e2, e4]
1988 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1989 assert [event.event_id for event in res.events] == [e4]
1991 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1992 assert [event.event_id for event in res.events] == [e3, e4]
1994 # my_communities returns events from both communities user3 is a member of
1995 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1996 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1998 # my_communities_exclude_global filters out events from global community (node_id=1)
1999 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
2000 assert [event.event_id for event in res.events] == [e6]
2002 # my_communities_exclude_global works independently of my_communities flag
2003 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
2004 assert [event.event_id for event in res.events] == [e6]
2006 # my_communities_exclude_global filters organizing results too
2007 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
2008 assert [event.event_id for event in res.events] == []
2010 # my_communities_exclude_global filters subscribed results too
2011 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
2012 assert [event.event_id for event in res.events] == []
2014 with events_session(token5) as api:
2015 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2016 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
2019def test_RemoveEventOrganizer(db, moderator: Moderator):
2020 user1, token1 = generate_user()
2021 user2, token2 = generate_user()
2023 with session_scope() as session:
2024 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2026 with events_session(token1) as api:
2027 event_id = api.CreateEvent(
2028 events_pb2.CreateEventReq(
2029 title="Dummy Title",
2030 content="Dummy content.",
2031 offline_information=events_pb2.OfflineEventInformation(
2032 address="Near Null Island",
2033 lat=0.1,
2034 lng=0.2,
2035 ),
2036 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2037 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2038 timezone="UTC",
2039 )
2040 ).event_id
2042 moderator.approve_event_occurrence(event_id)
2044 with events_session(token2) as api:
2045 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2047 with pytest.raises(grpc.RpcError) as e:
2048 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2049 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2050 assert e.value.details() == "You're not allowed to edit that event."
2052 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2054 with events_session(token1) as api:
2055 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2057 with pytest.raises(grpc.RpcError) as e:
2058 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2059 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2060 assert e.value.details() == "You cannot remove the event owner as an organizer."
2062 with events_session(token2) as api:
2063 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2064 assert res.organizer
2065 assert res.organizer_count == 2
2066 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2067 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
2069 with pytest.raises(grpc.RpcError) as e:
2070 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
2071 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2072 assert e.value.details() == "You're not allowed to edit that event."
2074 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2075 assert not res.organizer
2076 assert res.organizer_count == 1
2078 # Test that event owner can remove co-organizers
2079 with events_session(token1) as api:
2080 # Add user2 back as organizer
2081 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2083 # Verify user2 is now an organizer
2084 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2085 assert res.organizer_count == 2
2087 # Event owner can remove co-organizer
2088 api.RemoveEventOrganizer(
2089 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
2090 )
2092 # Verify user2 is no longer an organizer
2093 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2094 assert res.organizer_count == 1
2096 # Test that non-organizers cannot remove other organizers
2097 with events_session(token2) as api:
2098 # User2 cannot invite themselves as organizer (not the owner)
2099 with pytest.raises(grpc.RpcError) as e:
2100 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2101 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
2102 assert e.value.details() == "You're not allowed to edit that event."
2104 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
2105 with events_session(token1) as api:
2106 # Add user2 back as organizer
2107 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
2110def test_ListEventAttendees_regression(db):
2111 # see issue #1617:
2112 #
2113 # 1. Create an event
2114 # 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`
2115 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
2116 # 4. Change the current user's attendance state to "going" again
2117 #
2118 # **Expected behaviour**
2119 # `ListEventAttendees` should return the current user's ID
2120 #
2121 # **Actual/current behaviour**
2122 # `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
2124 user1, token1 = generate_user()
2125 user2, token2 = generate_user()
2126 user3, token3 = generate_user()
2127 user4, token4 = generate_user()
2128 user5, token5 = generate_user()
2130 with session_scope() as session:
2131 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
2133 start_time = now() + timedelta(hours=2)
2134 end_time = start_time + timedelta(hours=3)
2136 with events_session(token1) as api:
2137 res = api.CreateEvent(
2138 events_pb2.CreateEventReq(
2139 title="Dummy Title",
2140 content="Dummy content.",
2141 online_information=events_pb2.OnlineEventInformation(
2142 link="https://couchers.org",
2143 ),
2144 parent_community_id=c_id,
2145 start_time=Timestamp_from_datetime(start_time),
2146 end_time=Timestamp_from_datetime(end_time),
2147 timezone="UTC",
2148 )
2149 )
2151 res = api.TransferEvent(
2152 events_pb2.TransferEventReq(
2153 event_id=res.event_id,
2154 new_owner_community_id=c_id,
2155 )
2156 )
2158 event_id = res.event_id
2160 api.SetEventAttendance(
2161 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
2162 )
2163 api.SetEventAttendance(
2164 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2165 )
2167 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
2168 assert len(res.attendee_user_ids) == 1
2169 assert res.attendee_user_ids[0] == user1.id
2172def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2173 user1, token1 = generate_user()
2174 user2, token2 = generate_user()
2175 user3, token3 = generate_user()
2176 user4, token4 = generate_user()
2178 with session_scope() as session:
2179 c = create_community(session, 0, 2, "Community", [user3], [], None)
2180 h = create_group(session, "Group", [user4], [], c)
2181 c_id = c.id
2182 h_id = h.id
2183 user4_id = user4.id
2185 with events_session(token1) as api:
2186 event = api.CreateEvent(
2187 events_pb2.CreateEventReq(
2188 title="Dummy Title",
2189 content="Dummy content.",
2190 offline_information=events_pb2.OfflineEventInformation(
2191 address="Near Null Island",
2192 lat=0.1,
2193 lng=0.2,
2194 ),
2195 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)),
2196 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)),
2197 timezone="UTC",
2198 )
2199 )
2201 moderator.approve_event_occurrence(event.event_id)
2203 with threads_session(token2) as api:
2204 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2206 with events_session(token3) as api:
2207 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2208 assert res.thread.num_responses == 1
2210 with threads_session(token3) as api:
2211 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2212 assert len(ret.replies) == 1
2213 assert not ret.next_page_token
2214 assert ret.replies[0].thread_id == reply_id
2215 assert ret.replies[0].content == "hi"
2216 assert ret.replies[0].author_user_id == user2.id
2217 assert ret.replies[0].num_replies == 0
2219 api.PostReply(threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment"))
2221 process_jobs()
2223 assert push_collector.pop_for_user(user1.id, last=True).content.title == f"{user2.name} • Dummy Title"
2224 assert push_collector.pop_for_user(user2.id, last=True).content.title == f"{user3.name} • Dummy Title"
2225 assert push_collector.count_for_user(user4_id) == 0
2228def test_can_overlap_other_events_schedule_regression(db):
2229 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2230 user, token = generate_user()
2232 with session_scope() as session:
2233 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2235 start = now()
2237 with events_session(token) as api:
2238 # create another event, should be able to overlap with this one
2239 api.CreateEvent(
2240 events_pb2.CreateEventReq(
2241 title="Dummy Title",
2242 content="Dummy content.",
2243 parent_community_id=c_id,
2244 online_information=events_pb2.OnlineEventInformation(
2245 link="https://couchers.org/meet/",
2246 ),
2247 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2248 end_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2249 timezone="UTC",
2250 )
2251 )
2253 # this event
2254 res = api.CreateEvent(
2255 events_pb2.CreateEventReq(
2256 title="Dummy Title",
2257 content="Dummy content.",
2258 parent_community_id=c_id,
2259 online_information=events_pb2.OnlineEventInformation(
2260 link="https://couchers.org/meet/",
2261 ),
2262 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2263 end_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2264 timezone="UTC",
2265 )
2266 )
2268 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2269 api.ScheduleEvent(
2270 events_pb2.ScheduleEventReq(
2271 event_id=res.event_id,
2272 content="New event occurrence",
2273 offline_information=events_pb2.OfflineEventInformation(
2274 address="A bit further but still near Null Island",
2275 lat=0.3,
2276 lng=0.2,
2277 ),
2278 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2279 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2280 timezone="UTC",
2281 )
2282 )
2285def test_can_overlap_other_events_update_regression(db):
2286 user, token = generate_user()
2288 with session_scope() as session:
2289 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2291 start = now()
2293 with events_session(token) as api:
2294 # create another event, should be able to overlap with this one
2295 api.CreateEvent(
2296 events_pb2.CreateEventReq(
2297 title="Dummy Title",
2298 content="Dummy content.",
2299 parent_community_id=c_id,
2300 online_information=events_pb2.OnlineEventInformation(
2301 link="https://couchers.org/meet/",
2302 ),
2303 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
2304 end_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2305 timezone="UTC",
2306 )
2307 )
2309 res = api.CreateEvent(
2310 events_pb2.CreateEventReq(
2311 title="Dummy Title",
2312 content="Dummy content.",
2313 parent_community_id=c_id,
2314 online_information=events_pb2.OnlineEventInformation(
2315 link="https://couchers.org/meet/",
2316 ),
2317 start_time=Timestamp_from_datetime(start + timedelta(hours=7)),
2318 end_time=Timestamp_from_datetime(start + timedelta(hours=8)),
2319 timezone="UTC",
2320 )
2321 )
2323 event_id = api.ScheduleEvent(
2324 events_pb2.ScheduleEventReq(
2325 event_id=res.event_id,
2326 content="New event occurrence",
2327 offline_information=events_pb2.OfflineEventInformation(
2328 address="A bit further but still near Null Island",
2329 lat=0.3,
2330 lng=0.2,
2331 ),
2332 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2333 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2334 timezone="UTC",
2335 )
2336 ).event_id
2338 # can overlap with this current existing occurrence
2339 api.UpdateEvent(
2340 events_pb2.UpdateEventReq(
2341 event_id=event_id,
2342 start_time=Timestamp_from_datetime(start + timedelta(hours=5)),
2343 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2344 )
2345 )
2347 api.UpdateEvent(
2348 events_pb2.UpdateEventReq(
2349 event_id=event_id,
2350 start_time=Timestamp_from_datetime(start + timedelta(hours=2)),
2351 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2352 )
2353 )
2356def test_list_past_events_regression(db):
2357 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2358 user, token = generate_user()
2360 with session_scope() as session:
2361 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2363 start = now()
2365 with events_session(token) as api:
2366 api.CreateEvent(
2367 events_pb2.CreateEventReq(
2368 title="Dummy Title",
2369 content="Dummy content.",
2370 parent_community_id=c_id,
2371 online_information=events_pb2.OnlineEventInformation(
2372 link="https://couchers.org/meet/",
2373 ),
2374 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2375 end_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2376 timezone="UTC",
2377 )
2378 )
2380 with session_scope() as session:
2381 session.execute(
2382 update(EventOccurrence).values(
2383 during=DateTimeTZRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2384 )
2385 )
2387 with events_session(token) as api:
2388 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2389 assert len(res.events) == 1
2392def test_community_invite_requests(db, moderator: Moderator):
2393 user1, token1 = generate_user(complete_profile=True)
2394 user2, token2 = generate_user()
2395 user3, token3 = generate_user()
2396 user4, token4 = generate_user()
2397 user5, token5 = generate_user(is_superuser=True)
2399 with session_scope() as session:
2400 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2401 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2402 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2403 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2405 enforce_community_memberships()
2407 with events_session(token1) as api:
2408 res = api.CreateEvent(
2409 events_pb2.CreateEventReq(
2410 title="Dummy Title",
2411 content="Dummy content.",
2412 parent_community_id=c_id,
2413 online_information=events_pb2.OnlineEventInformation(
2414 link="https://couchers.org/meet/",
2415 ),
2416 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)),
2417 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)),
2418 timezone="UTC",
2419 )
2420 )
2421 user_url = f"http://localhost:3000/user/{user1.username}"
2422 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2424 event_id = res.event_id
2426 moderator.approve_event_occurrence(event_id)
2428 with events_session(token1) as api:
2429 with mock_notification_email() as mock:
2430 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2431 assert mock.call_count == 1
2432 e = email_fields(mock)
2433 assert e.recipient == "mods@couchers.org.invalid"
2435 assert user_url in e.plain
2436 assert event_url in e.plain
2438 # can't send another req
2439 with pytest.raises(grpc.RpcError) as err:
2440 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2441 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2442 assert err.value.details() == "You have already requested a community invite for this event."
2444 # another user can send one though
2445 with events_session(token3) as api:
2446 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2448 # but not a non-admin
2449 with events_session(token2) as api:
2450 with pytest.raises(grpc.RpcError) as err:
2451 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2452 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2453 assert err.value.details() == "You're not allowed to edit that event."
2455 with real_editor_session(token5) as editor:
2456 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2457 assert len(res.requests) == 2
2458 assert res.requests[0].user_id == user1.id
2459 assert res.requests[0].approx_users_to_notify == 3
2460 assert res.requests[1].user_id == user3.id
2461 assert res.requests[1].approx_users_to_notify == 3
2463 editor.DecideEventCommunityInviteRequest(
2464 editor_pb2.DecideEventCommunityInviteRequestReq(
2465 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2466 approve=False,
2467 )
2468 )
2470 editor.DecideEventCommunityInviteRequest(
2471 editor_pb2.DecideEventCommunityInviteRequestReq(
2472 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2473 approve=True,
2474 )
2475 )
2477 # not after approve
2478 with events_session(token4) as api:
2479 with pytest.raises(grpc.RpcError) as err:
2480 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2481 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2482 assert err.value.details() == "A community invite has already been sent out for this event."
2485def test_update_event_should_notify_queues_job():
2486 user, token = generate_user()
2487 start = now()
2489 with session_scope() as session:
2490 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2492 # create an event
2493 with events_session(token) as api:
2494 create_res = api.CreateEvent(
2495 events_pb2.CreateEventReq(
2496 title="Dummy Title",
2497 content="Dummy content.",
2498 parent_community_id=c_id,
2499 offline_information=events_pb2.OfflineEventInformation(
2500 address="https://couchers.org/meet/",
2501 lat=1.0,
2502 lng=2.0,
2503 ),
2504 start_time=Timestamp_from_datetime(start + timedelta(hours=3)),
2505 end_time=Timestamp_from_datetime(start + timedelta(hours=6)),
2506 timezone="UTC",
2507 )
2508 )
2510 event_id = create_res.event_id
2512 # measure initial background job queue length
2513 with session_scope() as session:
2514 jobs = session.query(BackgroundJob).all()
2515 job_length_before_update = len(jobs)
2517 # update with should_notify=False, expect no change in background job queue
2518 api.UpdateEvent(
2519 events_pb2.UpdateEventReq(
2520 event_id=event_id,
2521 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2522 should_notify=False,
2523 )
2524 )
2526 with session_scope() as session:
2527 jobs = session.query(BackgroundJob).all()
2528 assert len(jobs) == job_length_before_update
2530 # update with should_notify=True, expect one new background job added
2531 api.UpdateEvent(
2532 events_pb2.UpdateEventReq(
2533 event_id=event_id,
2534 start_time=Timestamp_from_datetime(start + timedelta(hours=4)),
2535 should_notify=True,
2536 )
2537 )
2539 with session_scope() as session:
2540 jobs = session.query(BackgroundJob).all()
2541 assert len(jobs) == job_length_before_update + 1
2544def test_event_photo_key(db):
2545 """Test that events return the photo_key field when a photo is set."""
2546 user, token = generate_user()
2548 start_time = now() + timedelta(hours=2)
2549 end_time = start_time + timedelta(hours=3)
2551 # Create a community and an upload for the event photo
2552 with session_scope() as session:
2553 create_community(session, 0, 2, "Community", [user], [], None)
2554 upload = Upload(
2555 key="test_event_photo_key_123",
2556 filename="test_event_photo_key_123.jpg",
2557 creator_user_id=user.id,
2558 )
2559 session.add(upload)
2561 with events_session(token) as api:
2562 # Create event without photo
2563 res = api.CreateEvent(
2564 events_pb2.CreateEventReq(
2565 title="Event Without Photo",
2566 content="No photo content.",
2567 photo_key=None,
2568 offline_information=events_pb2.OfflineEventInformation(
2569 address="Near Null Island",
2570 lat=0.1,
2571 lng=0.2,
2572 ),
2573 start_time=Timestamp_from_datetime(start_time),
2574 end_time=Timestamp_from_datetime(end_time),
2575 timezone="UTC",
2576 )
2577 )
2579 assert res.photo_key == ""
2580 assert res.photo_url == ""
2582 # Create event with photo
2583 res_with_photo = api.CreateEvent(
2584 events_pb2.CreateEventReq(
2585 title="Event With Photo",
2586 content="Has photo content.",
2587 photo_key="test_event_photo_key_123",
2588 offline_information=events_pb2.OfflineEventInformation(
2589 address="Near Null Island",
2590 lat=0.1,
2591 lng=0.2,
2592 ),
2593 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)),
2594 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)),
2595 timezone="UTC",
2596 )
2597 )
2599 assert res_with_photo.photo_key == "test_event_photo_key_123"
2600 assert "test_event_photo_key_123" in res_with_photo.photo_url
2602 event_id = res_with_photo.event_id
2604 # Verify photo_key is returned when getting the event
2605 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2606 assert get_res.photo_key == "test_event_photo_key_123"
2607 assert "test_event_photo_key_123" in get_res.photo_url
2610def test_event_created_with_shadowed_visibility(db):
2611 """Events start in SHADOWED state when created."""
2612 from couchers.models import ModerationState, ModerationVisibility
2614 user, token = generate_user()
2616 with session_scope() as session:
2617 create_community(session, 0, 2, "Community", [user], [], None)
2619 start_time = now() + timedelta(hours=2)
2620 end_time = start_time + timedelta(hours=3)
2622 with events_session(token) as api:
2623 res = api.CreateEvent(
2624 events_pb2.CreateEventReq(
2625 title="Test UMS Event",
2626 content="UMS content.",
2627 offline_information=events_pb2.OfflineEventInformation(
2628 address="Near Null Island",
2629 lat=0.1,
2630 lng=0.2,
2631 ),
2632 start_time=Timestamp_from_datetime(start_time),
2633 end_time=Timestamp_from_datetime(end_time),
2634 timezone="UTC",
2635 )
2636 )
2637 event_id = res.event_id
2639 with session_scope() as session:
2640 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2641 mod_state = session.execute(
2642 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2643 ).scalar_one()
2644 assert mod_state.visibility == ModerationVisibility.shadowed
2647def test_shadowed_event_visible_to_creator_only(db):
2648 """SHADOWED events are visible to the creator but not to other users."""
2649 user1, token1 = generate_user()
2650 user2, token2 = generate_user()
2652 with session_scope() as session:
2653 create_community(session, 0, 2, "Community", [user1], [], None)
2655 start_time = now() + timedelta(hours=2)
2656 end_time = start_time + timedelta(hours=3)
2658 with events_session(token1) as api:
2659 res = api.CreateEvent(
2660 events_pb2.CreateEventReq(
2661 title="Shadowed Event",
2662 content="Content.",
2663 offline_information=events_pb2.OfflineEventInformation(
2664 address="Near Null Island",
2665 lat=0.1,
2666 lng=0.2,
2667 ),
2668 start_time=Timestamp_from_datetime(start_time),
2669 end_time=Timestamp_from_datetime(end_time),
2670 timezone="UTC",
2671 )
2672 )
2673 event_id = res.event_id
2675 # Creator can see it
2676 with events_session(token1) as api:
2677 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2678 assert res.title == "Shadowed Event"
2680 # Other user cannot
2681 with events_session(token2) as api:
2682 with pytest.raises(grpc.RpcError) as e:
2683 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2684 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2687def test_event_visible_after_approval(db, moderator: Moderator):
2688 """Events become visible to all users after moderation approval."""
2689 user1, token1 = generate_user()
2690 user2, token2 = generate_user()
2692 with session_scope() as session:
2693 create_community(session, 0, 2, "Community", [user1], [], None)
2695 start_time = now() + timedelta(hours=2)
2696 end_time = start_time + timedelta(hours=3)
2698 with events_session(token1) as api:
2699 res = api.CreateEvent(
2700 events_pb2.CreateEventReq(
2701 title="Approved Event",
2702 content="Content.",
2703 offline_information=events_pb2.OfflineEventInformation(
2704 address="Near Null Island",
2705 lat=0.1,
2706 lng=0.2,
2707 ),
2708 start_time=Timestamp_from_datetime(start_time),
2709 end_time=Timestamp_from_datetime(end_time),
2710 timezone="UTC",
2711 )
2712 )
2713 event_id = res.event_id
2715 # Other user cannot see it yet
2716 with events_session(token2) as api:
2717 with pytest.raises(grpc.RpcError) as e:
2718 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2719 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2721 # Approve the event
2722 moderator.approve_event_occurrence(event_id)
2724 # Now other user can see it
2725 with events_session(token2) as api:
2726 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2727 assert res.title == "Approved Event"
2730def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2731 """SHADOWED events appear in lists for the creator but not for other users."""
2732 user1, token1 = generate_user()
2733 user2, token2 = generate_user()
2735 with session_scope() as session:
2736 create_community(session, 0, 2, "Community", [user1], [], None)
2738 start_time = now() + timedelta(hours=2)
2739 end_time = start_time + timedelta(hours=3)
2741 with events_session(token1) as api:
2742 res = api.CreateEvent(
2743 events_pb2.CreateEventReq(
2744 title="List Test Event",
2745 content="Content.",
2746 offline_information=events_pb2.OfflineEventInformation(
2747 address="Near Null Island",
2748 lat=0.1,
2749 lng=0.2,
2750 ),
2751 start_time=Timestamp_from_datetime(start_time),
2752 end_time=Timestamp_from_datetime(end_time),
2753 timezone="UTC",
2754 )
2755 )
2756 event_id = res.event_id
2758 # Creator can see their own SHADOWED event in lists
2759 with events_session(token1) as api:
2760 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2761 event_ids = [e.event_id for e in list_res.events]
2762 assert event_id in event_ids
2764 # Other user cannot see the SHADOWED event in lists
2765 with events_session(token2) as api:
2766 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2767 event_ids = [e.event_id for e in list_res.events]
2768 assert event_id not in event_ids
2770 # After approval, other user can see it
2771 moderator.approve_event_occurrence(event_id)
2773 with events_session(token2) as api:
2774 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2775 event_ids = [e.event_id for e in list_res.events]
2776 assert event_id in event_ids
2779def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2780 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2781 user1, token1 = generate_user()
2782 user2, token2 = generate_user()
2784 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2785 with session_scope() as session:
2786 world = create_community(session, 0, 10, "World", [user1], [], None)
2787 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2788 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2789 create_community(session, 0, 2, "Child", [user2], [], region)
2791 start_time = now() + timedelta(hours=2)
2792 end_time = start_time + timedelta(hours=3)
2794 with events_session(token1) as api:
2795 res = api.CreateEvent(
2796 events_pb2.CreateEventReq(
2797 title="Deferred Event",
2798 content="Content.",
2799 offline_information=events_pb2.OfflineEventInformation(
2800 address="Near Null Island",
2801 lat=0.1,
2802 lng=0.2,
2803 ),
2804 start_time=Timestamp_from_datetime(start_time),
2805 end_time=Timestamp_from_datetime(end_time),
2806 timezone="UTC",
2807 )
2808 )
2809 event_id = res.event_id
2811 # Process all jobs — notification should be deferred (event is SHADOWED)
2812 process_jobs()
2814 with session_scope() as session:
2815 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2816 # Notification was created with moderation_state_id for deferral
2817 assert notif.moderation_state_id is not None
2818 # No delivery exists (deferred because event is SHADOWED)
2819 delivery_count = session.execute(
2820 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2821 ).scalar_one_or_none()
2822 assert delivery_count is None
2824 # Approve the event — handle_notification is re-queued for deferred notifications
2825 moderator.approve_event_occurrence(event_id)
2827 # Verify handle_notification job was queued
2828 with session_scope() as session:
2829 pending_jobs = (
2830 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2831 .scalars()
2832 .all()
2833 )
2834 assert any("handle_notification" in j.job_type for j in pending_jobs)
2837def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2838 """Event update notifications should carry the event's moderation_state_id for deferral."""
2839 user1, token1 = generate_user()
2840 user2, token2 = generate_user()
2842 with session_scope() as session:
2843 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2845 start_time = now() + timedelta(hours=2)
2846 end_time = start_time + timedelta(hours=3)
2848 with events_session(token1) as api:
2849 res = api.CreateEvent(
2850 events_pb2.CreateEventReq(
2851 title="Update Test",
2852 content="Content.",
2853 offline_information=events_pb2.OfflineEventInformation(
2854 address="Near Null Island",
2855 lat=0.1,
2856 lng=0.2,
2857 ),
2858 start_time=Timestamp_from_datetime(start_time),
2859 end_time=Timestamp_from_datetime(end_time),
2860 timezone="UTC",
2861 )
2862 )
2863 event_id = res.event_id
2865 moderator.approve_event_occurrence(event_id)
2866 process_jobs()
2867 # Clear any create notifications
2868 while push_collector.count_for_user(user2.id): 2868 ↛ 2869line 2868 didn't jump to line 2869 because the condition on line 2868 was never true
2869 push_collector.pop_for_user(user2.id)
2871 # User2 subscribes to the event
2872 with events_session(token2) as api:
2873 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2875 # User1 updates the event with should_notify=True
2876 with events_session(token1) as api:
2877 api.UpdateEvent(
2878 events_pb2.UpdateEventReq(
2879 event_id=event_id,
2880 title=wrappers_pb2.StringValue(value="Updated Title"),
2881 should_notify=True,
2882 )
2883 )
2885 process_jobs()
2887 # Verify that the update notification for user2 has moderation_state_id set
2888 with session_scope() as session:
2889 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2891 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2892 # Find the update notification (most recent one)
2893 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2894 assert len(update_notifs) == 1
2895 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2898def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2899 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2900 user1, token1 = generate_user()
2901 user2, token2 = generate_user()
2903 with session_scope() as session:
2904 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2906 start_time = now() + timedelta(hours=2)
2907 end_time = start_time + timedelta(hours=3)
2909 with events_session(token1) as api:
2910 res = api.CreateEvent(
2911 events_pb2.CreateEventReq(
2912 title="Cancel Test",
2913 content="Content.",
2914 offline_information=events_pb2.OfflineEventInformation(
2915 address="Near Null Island",
2916 lat=0.1,
2917 lng=0.2,
2918 ),
2919 start_time=Timestamp_from_datetime(start_time),
2920 end_time=Timestamp_from_datetime(end_time),
2921 timezone="UTC",
2922 )
2923 )
2924 event_id = res.event_id
2926 moderator.approve_event_occurrence(event_id)
2927 process_jobs()
2928 while push_collector.count_for_user(user2.id): 2928 ↛ 2929line 2928 didn't jump to line 2929 because the condition on line 2928 was never true
2929 push_collector.pop_for_user(user2.id)
2931 # User2 subscribes
2932 with events_session(token2) as api:
2933 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2935 # User1 cancels the event
2936 with events_session(token1) as api:
2937 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2939 process_jobs()
2941 # Verify that the cancel notification for user2 has moderation_state_id set
2942 with session_scope() as session:
2943 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2945 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2946 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2947 assert len(cancel_notifs) == 1
2948 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2951def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2952 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2953 user1, token1 = generate_user()
2954 user2, token2 = generate_user()
2956 with session_scope() as session:
2957 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2959 # Create event starting 23 hours from now (within 24h reminder window)
2960 start_time = now() + timedelta(hours=23)
2961 end_time = start_time + timedelta(hours=1)
2963 with events_session(token1) as api:
2964 res = api.CreateEvent(
2965 events_pb2.CreateEventReq(
2966 title="Reminder Test",
2967 content="Content.",
2968 offline_information=events_pb2.OfflineEventInformation(
2969 address="Near Null Island",
2970 lat=0.1,
2971 lng=0.2,
2972 ),
2973 start_time=Timestamp_from_datetime(start_time),
2974 end_time=Timestamp_from_datetime(end_time),
2975 timezone="UTC",
2976 )
2977 )
2978 event_id = res.event_id
2980 moderator.approve_event_occurrence(event_id)
2981 process_jobs()
2982 while push_collector.count_for_user(user2.id): 2982 ↛ 2983line 2982 didn't jump to line 2983 because the condition on line 2982 was never true
2983 push_collector.pop_for_user(user2.id)
2985 # User2 marks attendance
2986 with events_session(token2) as api:
2987 api.SetEventAttendance(
2988 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2989 )
2991 # Run the event reminder handler
2992 send_event_reminders(empty_pb2.Empty())
2993 process_jobs()
2995 # Verify that the reminder notification for user2 has moderation_state_id set
2996 with session_scope() as session:
2997 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2999 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3000 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
3001 assert len(reminder_notifs) == 1
3002 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
3005def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3006 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3007 user1, token1 = generate_user()
3008 user2, token2 = generate_user()
3010 with session_scope() as session:
3011 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3013 start = now()
3015 # User1 creates event A with 3 occurrences
3016 event_a_ids = []
3017 with events_session(token1) as api:
3018 res = api.CreateEvent(
3019 events_pb2.CreateEventReq(
3020 title="Event A",
3021 content="Content A.",
3022 parent_community_id=c_id,
3023 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3024 start_time=Timestamp_from_datetime(start + timedelta(hours=1)),
3025 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)),
3026 timezone="UTC",
3027 )
3028 )
3029 event_a_ids.append(res.event_id)
3030 for i in range(2):
3031 res = api.ScheduleEvent(
3032 events_pb2.ScheduleEventReq(
3033 event_id=event_a_ids[-1],
3034 content=f"A occurrence {i}",
3035 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3036 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)),
3037 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)),
3038 timezone="UTC",
3039 )
3040 )
3041 event_a_ids.append(res.event_id)
3043 # User2 creates event B with 2 occurrences
3044 event_b_ids = []
3045 with events_session(token2) as api:
3046 res = api.CreateEvent(
3047 events_pb2.CreateEventReq(
3048 title="Event B",
3049 content="Content B.",
3050 parent_community_id=c_id,
3051 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3052 start_time=Timestamp_from_datetime(start + timedelta(hours=10)),
3053 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)),
3054 timezone="UTC",
3055 )
3056 )
3057 event_b_ids.append(res.event_id)
3058 res = api.ScheduleEvent(
3059 events_pb2.ScheduleEventReq(
3060 event_id=event_b_ids[-1],
3061 content="B occurrence 1",
3062 online_information=events_pb2.OnlineEventInformation(link="https://couchers.org/meet/"),
3063 start_time=Timestamp_from_datetime(start + timedelta(hours=11)),
3064 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)),
3065 timezone="UTC",
3066 )
3067 )
3068 event_b_ids.append(res.event_id)
3070 moderator.approve_event_occurrence(event_a_ids[0])
3071 moderator.approve_event_occurrence(event_b_ids[0])
3073 # List occurrences for event A — should only get event A's 3 occurrences
3074 with events_session(token1) as api:
3075 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3076 returned_ids = [e.event_id for e in res.events]
3077 assert sorted(returned_ids) == sorted(event_a_ids)
3079 # List occurrences for event B — should only get event B's 2 occurrences
3080 with events_session(token2) as api:
3081 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3082 returned_ids = [e.event_id for e in res.events]
3083 assert sorted(returned_ids) == sorted(event_b_ids)
3086def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3087 """Event comment notifications should carry the event's moderation_state_id for deferral."""
3088 user1, token1 = generate_user()
3089 user2, token2 = generate_user()
3091 with session_scope() as session:
3092 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3094 start_time = now() + timedelta(hours=2)
3095 end_time = start_time + timedelta(hours=3)
3097 with events_session(token1) as api:
3098 res = api.CreateEvent(
3099 events_pb2.CreateEventReq(
3100 title="Comment Test",
3101 content="Content.",
3102 offline_information=events_pb2.OfflineEventInformation(
3103 address="Near Null Island",
3104 lat=0.1,
3105 lng=0.2,
3106 ),
3107 start_time=Timestamp_from_datetime(start_time),
3108 end_time=Timestamp_from_datetime(end_time),
3109 timezone="UTC",
3110 )
3111 )
3112 event_id = res.event_id
3113 thread_id = res.thread.thread_id
3115 moderator.approve_event_occurrence(event_id)
3116 process_jobs()
3117 while push_collector.count_for_user(user1.id): 3117 ↛ 3118line 3117 didn't jump to line 3118 because the condition on line 3117 was never true
3118 push_collector.pop_for_user(user1.id)
3120 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3121 with events_session(token1) as api:
3122 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3124 # User2 posts a top-level comment on the event thread
3125 with threads_session(token2) as api:
3126 api.PostReply(threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!"))
3128 process_jobs()
3130 # The comment notification for user1 should have moderation_state_id set
3131 with session_scope() as session:
3132 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3134 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3135 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3136 assert len(comment_notifs) == 1
3137 assert comment_notifs[0].moderation_state_id == occurrence.moderation_state_id
3140def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3141 """Event thread reply notifications should carry the event's moderation_state_id for deferral."""
3142 user1, token1 = generate_user()
3143 user2, token2 = generate_user()
3144 user3, token3 = generate_user()
3146 with session_scope() as session:
3147 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3149 start_time = now() + timedelta(hours=2)
3150 end_time = start_time + timedelta(hours=3)
3152 with events_session(token1) as api:
3153 res = api.CreateEvent(
3154 events_pb2.CreateEventReq(
3155 title="Reply Test",
3156 content="Content.",
3157 offline_information=events_pb2.OfflineEventInformation(
3158 address="Near Null Island",
3159 lat=0.1,
3160 lng=0.2,
3161 ),
3162 start_time=Timestamp_from_datetime(start_time),
3163 end_time=Timestamp_from_datetime(end_time),
3164 timezone="UTC",
3165 )
3166 )
3167 event_id = res.event_id
3168 thread_id = res.thread.thread_id
3170 moderator.approve_event_occurrence(event_id)
3171 process_jobs()
3172 while push_collector.count_for_user(user1.id): 3172 ↛ 3173line 3172 didn't jump to line 3173 because the condition on line 3172 was never true
3173 push_collector.pop_for_user(user1.id)
3175 # User2 posts a top-level comment
3176 with threads_session(token2) as api:
3177 comment_thread_id = api.PostReply(
3178 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3179 ).thread_id
3181 process_jobs()
3182 while push_collector.count_for_user(user1.id):
3183 push_collector.pop_for_user(user1.id)
3185 # User3 replies to user2's comment (depth=2 reply)
3186 with threads_session(token3) as api:
3187 api.PostReply(threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply"))
3189 process_jobs()
3191 # The nested reply notification for user2 should have moderation_state_id set
3192 with session_scope() as session:
3193 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
3195 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3196 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3197 assert len(reply_notifs) == 1
3198 assert reply_notifs[0].moderation_state_id == occurrence.moderation_state_id