Coverage for app/backend/src/tests/test_events.py: 99%
1445 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 18:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-25 18:54 +0000
1import re
2from datetime import datetime, timedelta
3from zoneinfo import ZoneInfo
5import grpc
6import pytest
7from google.protobuf import empty_pb2, wrappers_pb2
8from psycopg.types.range import TimestamptzRange
9from sqlalchemy import select
10from sqlalchemy.sql.expression import update
12from couchers.db import session_scope
13from couchers.jobs.handlers import send_event_reminders
14from couchers.models import (
15 BackgroundJob,
16 BackgroundJobState,
17 Comment,
18 EventOccurrence,
19 ModerationState,
20 ModerationVisibility,
21 Notification,
22 NotificationDelivery,
23 NotificationTopicAction,
24 Reply,
25 Upload,
26 User,
27)
28from couchers.proto import editor_pb2, events_pb2, threads_pb2
29from couchers.tasks import enforce_community_memberships
30from couchers.utils import datetime_to_iso8601_local, now, to_aware_datetime
31from tests.fixtures.db import generate_user
32from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs
33from tests.fixtures.sessions import events_session, real_editor_session, threads_session
34from tests.test_communities import create_community, create_group
37@pytest.fixture(autouse=True)
38def _(testconfig):
39 pass
42def to_event_time_granularity(value: datetime) -> datetime:
43 """Events are scheduled at the minute granularity."""
44 return value.replace(second=0, microsecond=0)
47def is_utc_or_gmt(timezone: str) -> bool:
48 # Our lightweight "timezone_areas.sql-fake" uses Etc/UTC, whereas the real file uses Etc/GMT.
49 # Tests should be agnostic to which one we're using.
50 return timezone in ("Etc/UTC", "Etc/GMT")
53def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator):
54 # test cases:
55 # can create event
56 # cannot create event with missing details
57 # can't create event that starts in the past
58 # can create in different timezones
60 # event creator
61 user1, token1 = generate_user()
62 # community moderator
63 user2, token2 = generate_user()
64 # third party
65 user3, token3 = generate_user()
67 with session_scope() as session:
68 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
70 time_before = now()
71 start_time = now() + timedelta(hours=2)
72 end_time = start_time + timedelta(hours=3)
74 # Can create an event
75 with events_session(token1) as api:
76 res = api.CreateEvent(
77 events_pb2.CreateEventReq(
78 title="Dummy Title",
79 content="Dummy content.",
80 photo_key=None,
81 location=events_pb2.EventLocation(
82 address="Near Null Island",
83 lat=0.1,
84 lng=0.2,
85 ),
86 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
87 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
88 )
89 )
91 assert res.is_next
92 assert res.title == "Dummy Title"
93 assert res.slug == "dummy-title"
94 assert res.content == "Dummy content."
95 assert not res.photo_url
96 assert res.HasField("location")
97 assert res.location.lat == 0.1
98 assert res.location.lng == 0.2
99 assert res.location.address == "Near Null Island"
100 assert time_before <= to_aware_datetime(res.created) <= now()
101 assert time_before <= to_aware_datetime(res.last_edited) <= now()
102 assert res.creator_user_id == user1.id
103 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
104 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
105 assert is_utc_or_gmt(res.timezone)
106 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
107 assert res.organizer
108 assert res.subscriber
109 assert res.going_count == 1
110 assert res.organizer_count == 1
111 assert res.subscriber_count == 1
112 assert res.owner_user_id == user1.id
113 assert not res.owner_community_id
114 assert not res.owner_group_id
115 assert res.thread.thread_id
116 assert res.can_edit
117 assert not res.can_moderate
119 event_id = res.event_id
121 # Approve the event so other users can see it
122 moderator.approve_event_occurrence(event_id)
124 with events_session(token2) as api:
125 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
127 assert res.is_next
128 assert res.title == "Dummy Title"
129 assert res.slug == "dummy-title"
130 assert res.content == "Dummy content."
131 assert not res.photo_url
132 assert res.HasField("location")
133 assert res.location.lat == 0.1
134 assert res.location.lng == 0.2
135 assert res.location.address == "Near Null Island"
136 assert time_before <= to_aware_datetime(res.created) <= now()
137 assert time_before <= to_aware_datetime(res.last_edited) <= now()
138 assert res.creator_user_id == user1.id
139 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
140 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
141 assert is_utc_or_gmt(res.timezone)
142 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
143 assert not res.organizer
144 assert not res.subscriber
145 assert res.going_count == 1
146 assert res.organizer_count == 1
147 assert res.subscriber_count == 1
148 assert res.owner_user_id == user1.id
149 assert not res.owner_community_id
150 assert not res.owner_group_id
151 assert res.thread.thread_id
152 assert res.can_edit
153 assert res.can_moderate
155 with events_session(token3) as api:
156 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
158 assert res.is_next
159 assert res.title == "Dummy Title"
160 assert res.slug == "dummy-title"
161 assert res.content == "Dummy content."
162 assert not res.photo_url
163 assert res.HasField("location")
164 assert res.location.lat == 0.1
165 assert res.location.lng == 0.2
166 assert res.location.address == "Near Null Island"
167 assert time_before <= to_aware_datetime(res.created) <= now()
168 assert time_before <= to_aware_datetime(res.last_edited) <= now()
169 assert res.creator_user_id == user1.id
170 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
171 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
172 assert is_utc_or_gmt(res.timezone)
173 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
174 assert not res.organizer
175 assert not res.subscriber
176 assert res.going_count == 1
177 assert res.organizer_count == 1
178 assert res.subscriber_count == 1
179 assert res.owner_user_id == user1.id
180 assert not res.owner_community_id
181 assert not res.owner_group_id
182 assert res.thread.thread_id
183 assert not res.can_edit
184 assert not res.can_moderate
186 # Failure cases
187 with events_session(token1) as api:
188 with pytest.raises(grpc.RpcError) as e:
189 api.CreateEvent(
190 events_pb2.CreateEventReq(
191 # title="Dummy Title",
192 content="Dummy content.",
193 photo_key=None,
194 location=events_pb2.EventLocation(
195 address="Near Null Island",
196 lat=0.1,
197 lng=0.2,
198 ),
199 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
200 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
201 )
202 )
203 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
204 assert e.value.details() == "Missing event title."
206 with pytest.raises(grpc.RpcError) as e:
207 api.CreateEvent(
208 events_pb2.CreateEventReq(
209 title="Dummy Title",
210 # content="Dummy content.",
211 photo_key=None,
212 location=events_pb2.EventLocation(
213 address="Near Null Island",
214 lat=0.1,
215 lng=0.2,
216 ),
217 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
218 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
219 )
220 )
221 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
222 assert e.value.details() == "Missing event content."
224 with pytest.raises(grpc.RpcError) as e:
225 api.CreateEvent(
226 events_pb2.CreateEventReq(
227 title="Dummy Title",
228 content="Dummy content.",
229 photo_key="nonexistent",
230 location=events_pb2.EventLocation(
231 address="Near Null Island",
232 lat=0.1,
233 lng=0.2,
234 ),
235 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
236 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
237 )
238 )
239 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
240 assert e.value.details() == "Photo not found."
242 with pytest.raises(grpc.RpcError) as e:
243 api.CreateEvent(
244 events_pb2.CreateEventReq(
245 title="Dummy Title",
246 content="Dummy content.",
247 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
248 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
249 )
250 )
251 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
252 assert e.value.details() == "Missing event address or location."
254 with pytest.raises(grpc.RpcError) as e:
255 api.CreateEvent(
256 events_pb2.CreateEventReq(
257 title="Dummy Title",
258 content="Dummy content.",
259 location=events_pb2.EventLocation(
260 address="Near Null Island",
261 ),
262 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
263 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
264 )
265 )
266 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
267 assert e.value.details() == "Invalid coordinate."
269 with pytest.raises(grpc.RpcError) as e:
270 api.CreateEvent(
271 events_pb2.CreateEventReq(
272 title="Dummy Title",
273 content="Dummy content.",
274 location=events_pb2.EventLocation(
275 lat=0.1,
276 lng=0.1,
277 ),
278 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
279 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
280 )
281 )
282 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
283 assert e.value.details() == "Missing event address or location."
285 with pytest.raises(grpc.RpcError) as e:
286 api.CreateEvent(
287 events_pb2.CreateEventReq(
288 title="Dummy Title",
289 content="Dummy content.",
290 location=events_pb2.EventLocation(
291 address="Near Null Island",
292 lat=0.1,
293 lng=0.2,
294 ),
295 start_datetime_iso8601_local=datetime_to_iso8601_local(now() - timedelta(hours=2)),
296 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
297 )
298 )
299 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
300 assert e.value.details() == "The event must be in the future."
302 with pytest.raises(grpc.RpcError) as e:
303 api.CreateEvent(
304 events_pb2.CreateEventReq(
305 title="Dummy Title",
306 content="Dummy content.",
307 location=events_pb2.EventLocation(
308 address="Near Null Island",
309 lat=0.1,
310 lng=0.2,
311 ),
312 start_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
313 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
314 )
315 )
316 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
317 assert e.value.details() == "The event must end after it starts."
319 with pytest.raises(grpc.RpcError) as e:
320 api.CreateEvent(
321 events_pb2.CreateEventReq(
322 title="Dummy Title",
323 content="Dummy content.",
324 location=events_pb2.EventLocation(
325 address="Near Null Island",
326 lat=0.1,
327 lng=0.2,
328 ),
329 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=2)),
330 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=500, hours=5)),
331 )
332 )
333 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
334 assert e.value.details() == "The event needs to start within the next year."
336 with pytest.raises(grpc.RpcError) as e:
337 api.CreateEvent(
338 events_pb2.CreateEventReq(
339 title="Dummy Title",
340 content="Dummy content.",
341 location=events_pb2.EventLocation(
342 address="Near Null Island",
343 lat=0.1,
344 lng=0.2,
345 ),
346 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
347 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(days=100)),
348 )
349 )
350 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
351 assert e.value.details() == "Events cannot last longer than 7 days."
354def test_CreateEvent_incomplete_profile(db):
355 user1, token1 = generate_user(complete_profile=False)
356 user2, token2 = generate_user()
358 with session_scope() as session:
359 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
361 start_time = now() + timedelta(hours=2)
362 end_time = start_time + timedelta(hours=3)
364 with events_session(token1) as api:
365 with pytest.raises(grpc.RpcError) as e:
366 api.CreateEvent(
367 events_pb2.CreateEventReq(
368 title="Dummy Title",
369 content="Dummy content.",
370 photo_key=None,
371 location=events_pb2.EventLocation(
372 address="Near Null Island",
373 lat=0.1,
374 lng=0.2,
375 ),
376 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
377 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
378 )
379 )
380 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
381 assert e.value.details() == "You have to complete your profile before you can create an event."
384def test_ScheduleEvent(db):
385 # test cases:
386 # can schedule a new event occurrence
388 user, token = generate_user()
390 with session_scope() as session:
391 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
393 time_before = now()
394 start_time = now() + timedelta(hours=2)
395 end_time = start_time + timedelta(hours=3)
397 with events_session(token) as api:
398 res = api.CreateEvent(
399 events_pb2.CreateEventReq(
400 title="Dummy Title",
401 content="Dummy content.",
402 parent_community_id=c_id,
403 location=events_pb2.EventLocation(
404 address="Near Null Island",
405 lat=0.1,
406 lng=0.2,
407 ),
408 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
409 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
410 )
411 )
413 new_start_time = now() + timedelta(hours=6)
414 new_end_time = new_start_time + timedelta(hours=2)
416 res = api.ScheduleEvent(
417 events_pb2.ScheduleEventReq(
418 event_id=res.event_id,
419 content="New event occurrence",
420 location=events_pb2.EventLocation(
421 address="A bit further but still near Null Island",
422 lat=0.3,
423 lng=0.2,
424 ),
425 start_datetime_iso8601_local=datetime_to_iso8601_local(new_start_time),
426 end_datetime_iso8601_local=datetime_to_iso8601_local(new_end_time),
427 )
428 )
430 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id))
432 assert not res.is_next
433 assert res.title == "Dummy Title"
434 assert res.slug == "dummy-title"
435 assert res.content == "New event occurrence"
436 assert not res.photo_url
437 assert res.HasField("location")
438 assert res.location.lat == 0.3
439 assert res.location.lng == 0.2
440 assert res.location.address == "A bit further but still near Null Island"
441 assert time_before <= to_aware_datetime(res.created) <= now()
442 assert time_before <= to_aware_datetime(res.last_edited) <= now()
443 assert res.creator_user_id == user.id
444 assert to_aware_datetime(res.start_time) == to_event_time_granularity(new_start_time)
445 assert to_aware_datetime(res.end_time) == to_event_time_granularity(new_end_time)
446 assert is_utc_or_gmt(res.timezone)
447 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
448 assert res.organizer
449 assert res.subscriber
450 assert res.going_count == 1
451 assert res.organizer_count == 1
452 assert res.subscriber_count == 1
453 assert res.owner_user_id == user.id
454 assert not res.owner_community_id
455 assert not res.owner_group_id
456 assert res.thread.thread_id
457 assert res.can_edit
458 assert res.can_moderate
461def test_cannot_overlap_occurrences_schedule(db):
462 user, token = generate_user()
464 with session_scope() as session:
465 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
467 start = now()
469 with events_session(token) as api:
470 res = api.CreateEvent(
471 events_pb2.CreateEventReq(
472 title="Dummy Title",
473 content="Dummy content.",
474 parent_community_id=c_id,
475 location=events_pb2.EventLocation(
476 address="Near Null Island",
477 lat=0.1,
478 lng=0.2,
479 ),
480 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
481 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
482 )
483 )
485 with pytest.raises(grpc.RpcError) as e:
486 api.ScheduleEvent(
487 events_pb2.ScheduleEventReq(
488 event_id=res.event_id,
489 content="New event occurrence",
490 location=events_pb2.EventLocation(
491 address="A bit further but still near Null Island",
492 lat=0.3,
493 lng=0.2,
494 ),
495 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
496 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
497 )
498 )
499 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
500 assert e.value.details() == "An event cannot have overlapping occurrences."
503def test_cannot_overlap_occurrences_update(db):
504 user, token = generate_user()
506 with session_scope() as session:
507 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
509 start = now()
511 with events_session(token) as api:
512 res = api.CreateEvent(
513 events_pb2.CreateEventReq(
514 title="Dummy Title",
515 content="Dummy content.",
516 parent_community_id=c_id,
517 location=events_pb2.EventLocation(
518 address="Near Null Island",
519 lat=0.1,
520 lng=0.2,
521 ),
522 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
523 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
524 )
525 )
527 event_id = api.ScheduleEvent(
528 events_pb2.ScheduleEventReq(
529 event_id=res.event_id,
530 content="New event occurrence",
531 location=events_pb2.EventLocation(
532 address="A bit further but still near Null Island",
533 lat=0.3,
534 lng=0.2,
535 ),
536 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
537 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
538 )
539 ).event_id
541 # can overlap with this current existing occurrence
542 api.UpdateEvent(
543 events_pb2.UpdateEventReq(
544 event_id=event_id,
545 start_datetime_iso8601_local=wrappers_pb2.StringValue(
546 value=datetime_to_iso8601_local(start + timedelta(hours=5))
547 ),
548 end_datetime_iso8601_local=wrappers_pb2.StringValue(
549 value=datetime_to_iso8601_local(start + timedelta(hours=6))
550 ),
551 )
552 )
554 with pytest.raises(grpc.RpcError) as e:
555 api.UpdateEvent(
556 events_pb2.UpdateEventReq(
557 event_id=event_id,
558 start_datetime_iso8601_local=wrappers_pb2.StringValue(
559 value=datetime_to_iso8601_local(start + timedelta(hours=2))
560 ),
561 end_datetime_iso8601_local=wrappers_pb2.StringValue(
562 value=datetime_to_iso8601_local(start + timedelta(hours=4))
563 ),
564 )
565 )
566 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
567 assert e.value.details() == "An event cannot have overlapping occurrences."
570def test_UpdateEvent_single(db, moderator: Moderator):
571 # test cases:
572 # owner can update
573 # community owner can update
574 # notifies attendees
576 # event creator
577 user1, token1 = generate_user()
578 # community moderator
579 user2, token2 = generate_user()
580 # third parties
581 user3, token3 = generate_user()
582 user4, token4 = generate_user()
583 user5, token5 = generate_user()
584 user6, token6 = generate_user()
586 with session_scope() as session:
587 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
589 time_before = now()
590 start_time = now() + timedelta(hours=2)
591 end_time = start_time + timedelta(hours=3)
593 with events_session(token1) as api:
594 res = api.CreateEvent(
595 events_pb2.CreateEventReq(
596 title="Dummy Title",
597 content="Dummy content.",
598 parent_community_id=c_id,
599 location=events_pb2.EventLocation(
600 address="Near Null Island",
601 lat=0.1,
602 lng=0.2,
603 ),
604 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
605 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
606 )
607 )
609 event_id = res.event_id
611 moderator.approve_event_occurrence(event_id)
613 with events_session(token4) as api:
614 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
616 with events_session(token5) as api:
617 api.SetEventAttendance(
618 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
619 )
621 with events_session(token6) as api:
622 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
624 time_before_update = now()
626 with events_session(token1) as api:
627 res = api.UpdateEvent(
628 events_pb2.UpdateEventReq(
629 event_id=event_id,
630 )
631 )
633 with events_session(token1) as api:
634 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
636 assert res.is_next
637 assert res.title == "Dummy Title"
638 assert res.slug == "dummy-title"
639 assert res.content == "Dummy content."
640 assert not res.photo_url
641 assert res.HasField("location")
642 assert res.location.lat == 0.1
643 assert res.location.lng == 0.2
644 assert res.location.address == "Near Null Island"
645 assert time_before <= to_aware_datetime(res.created) <= time_before_update
646 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
647 assert res.creator_user_id == user1.id
648 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
649 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
650 assert is_utc_or_gmt(res.timezone)
651 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
652 assert res.organizer
653 assert res.subscriber
654 assert res.going_count == 2
655 assert res.organizer_count == 1
656 assert res.subscriber_count == 3
657 assert res.owner_user_id == user1.id
658 assert not res.owner_community_id
659 assert not res.owner_group_id
660 assert res.thread.thread_id
661 assert res.can_edit
662 assert not res.can_moderate
664 with events_session(token2) as api:
665 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
667 assert res.is_next
668 assert res.title == "Dummy Title"
669 assert res.slug == "dummy-title"
670 assert res.content == "Dummy content."
671 assert not res.photo_url
672 assert res.HasField("location")
673 assert res.location.lat == 0.1
674 assert res.location.lng == 0.2
675 assert res.location.address == "Near Null Island"
676 assert time_before <= to_aware_datetime(res.created) <= time_before_update
677 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
678 assert res.creator_user_id == user1.id
679 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
680 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
681 assert is_utc_or_gmt(res.timezone)
682 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
683 assert not res.organizer
684 assert not res.subscriber
685 assert res.going_count == 2
686 assert res.organizer_count == 1
687 assert res.subscriber_count == 3
688 assert res.owner_user_id == user1.id
689 assert not res.owner_community_id
690 assert not res.owner_group_id
691 assert res.thread.thread_id
692 assert res.can_edit
693 assert res.can_moderate
695 with events_session(token3) as api:
696 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
698 assert res.is_next
699 assert res.title == "Dummy Title"
700 assert res.slug == "dummy-title"
701 assert res.content == "Dummy content."
702 assert not res.photo_url
703 assert res.HasField("location")
704 assert res.location.lat == 0.1
705 assert res.location.lng == 0.2
706 assert res.location.address == "Near Null Island"
707 assert time_before <= to_aware_datetime(res.created) <= time_before_update
708 assert time_before_update <= to_aware_datetime(res.last_edited) <= now()
709 assert res.creator_user_id == user1.id
710 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
711 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
712 assert is_utc_or_gmt(res.timezone)
713 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
714 assert not res.organizer
715 assert not res.subscriber
716 assert res.going_count == 2
717 assert res.organizer_count == 1
718 assert res.subscriber_count == 3
719 assert res.owner_user_id == user1.id
720 assert not res.owner_community_id
721 assert not res.owner_group_id
722 assert res.thread.thread_id
723 assert not res.can_edit
724 assert not res.can_moderate
726 with events_session(token1) as api:
727 res = api.UpdateEvent(
728 events_pb2.UpdateEventReq(
729 event_id=event_id,
730 location=events_pb2.EventLocation(
731 address="Nearer Null Island",
732 lat=0.01,
733 lng=0.02,
734 ),
735 )
736 )
738 with events_session(token3) as api:
739 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
741 assert res.HasField("location")
742 assert res.location.address == "Nearer Null Island"
743 assert res.location.lat == 0.01
744 assert res.location.lng == 0.02
747def test_UpdateEvent_all(db, moderator: Moderator):
748 # event creator
749 user1, token1 = generate_user()
750 # community moderator
751 user2, token2 = generate_user()
752 # third parties
753 user3, token3 = generate_user()
754 user4, token4 = generate_user()
755 user5, token5 = generate_user()
756 user6, token6 = generate_user()
758 with session_scope() as session:
759 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
761 time_before = now()
762 start_time = now() + timedelta(hours=1)
763 end_time = start_time + timedelta(hours=1.5)
765 event_ids = []
767 with events_session(token1) as api:
768 res = api.CreateEvent(
769 events_pb2.CreateEventReq(
770 title="Dummy Title",
771 content="0th occurrence",
772 location=events_pb2.EventLocation(
773 address="Near Null Island",
774 lat=0.1,
775 lng=0.2,
776 ),
777 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
778 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
779 )
780 )
782 event_id = res.event_id
783 event_ids.append(event_id)
785 moderator.approve_event_occurrence(event_id)
787 with events_session(token4) as api:
788 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
790 with events_session(token5) as api:
791 api.SetEventAttendance(
792 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
793 )
795 with events_session(token6) as api:
796 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
798 with events_session(token1) as api:
799 for i in range(5):
800 res = api.ScheduleEvent(
801 events_pb2.ScheduleEventReq(
802 event_id=event_ids[-1],
803 content=f"{i + 1}th occurrence",
804 location=events_pb2.EventLocation(
805 address="Near Null Island",
806 lat=0.1,
807 lng=0.2,
808 ),
809 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2 + i)),
810 end_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(hours=2.5 + i)),
811 )
812 )
814 event_ids.append(res.event_id)
816 # Approve all scheduled occurrences
817 for eid in event_ids[1:]:
818 moderator.approve_event_occurrence(eid)
820 updated_event_id = event_ids[3]
822 time_before_update = now()
824 with events_session(token1) as api:
825 res = api.UpdateEvent(
826 events_pb2.UpdateEventReq(
827 event_id=updated_event_id,
828 title=wrappers_pb2.StringValue(value="New Title"),
829 content=wrappers_pb2.StringValue(value="New content."),
830 location=events_pb2.EventLocation(
831 address="Not so near Null Island",
832 lat=0.2,
833 lng=0.2,
834 ),
835 update_all_future=True,
836 )
837 )
839 time_after_update = now()
841 with events_session(token2) as api:
842 for i in range(3):
843 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
844 assert res.content == f"{i}th occurrence"
845 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update
847 for i in range(3, 6):
848 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i]))
849 assert res.content == "New content."
850 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update
853def test_GetEvent(db, moderator: Moderator):
854 # event creator
855 user1, token1 = generate_user()
856 # community moderator
857 user2, token2 = generate_user()
858 # third parties
859 user3, token3 = generate_user()
860 user4, token4 = generate_user()
861 user5, token5 = generate_user()
862 user6, token6 = generate_user()
864 with session_scope() as session:
865 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
867 time_before = now()
868 start_time = now() + timedelta(hours=2)
869 end_time = start_time + timedelta(hours=3)
871 with events_session(token1) as api:
872 # in person event
873 res = api.CreateEvent(
874 events_pb2.CreateEventReq(
875 title="Dummy Title",
876 content="Dummy content.",
877 location=events_pb2.EventLocation(
878 address="Near Null Island",
879 lat=0.1,
880 lng=0.2,
881 ),
882 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
883 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
884 )
885 )
887 event_id = res.event_id
889 moderator.approve_event_occurrence(event_id)
891 with events_session(token4) as api:
892 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
894 with events_session(token5) as api:
895 api.SetEventAttendance(
896 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
897 )
899 with events_session(token6) as api:
900 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
902 with events_session(token1) as api:
903 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
905 assert res.is_next
906 assert res.title == "Dummy Title"
907 assert res.slug == "dummy-title"
908 assert res.content == "Dummy content."
909 assert not res.photo_url
910 assert res.HasField("location")
911 assert res.location.lat == 0.1
912 assert res.location.lng == 0.2
913 assert res.location.address == "Near Null Island"
914 assert time_before <= to_aware_datetime(res.created) <= now()
915 assert time_before <= to_aware_datetime(res.last_edited) <= now()
916 assert res.creator_user_id == user1.id
917 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
918 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
919 assert is_utc_or_gmt(res.timezone)
920 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING
921 assert res.organizer
922 assert res.subscriber
923 assert res.going_count == 2
924 assert res.organizer_count == 1
925 assert res.subscriber_count == 3
926 assert res.owner_user_id == user1.id
927 assert not res.owner_community_id
928 assert not res.owner_group_id
929 assert res.thread.thread_id
930 assert res.can_edit
931 assert not res.can_moderate
933 with events_session(token2) as api:
934 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
936 assert res.is_next
937 assert res.title == "Dummy Title"
938 assert res.slug == "dummy-title"
939 assert res.content == "Dummy content."
940 assert not res.photo_url
941 assert res.HasField("location")
942 assert res.location.lat == 0.1
943 assert res.location.lng == 0.2
944 assert res.location.address == "Near Null Island"
945 assert time_before <= to_aware_datetime(res.created) <= now()
946 assert time_before <= to_aware_datetime(res.last_edited) <= now()
947 assert res.creator_user_id == user1.id
948 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
949 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
950 assert is_utc_or_gmt(res.timezone)
951 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
952 assert not res.organizer
953 assert not res.subscriber
954 assert res.going_count == 2
955 assert res.organizer_count == 1
956 assert res.subscriber_count == 3
957 assert res.owner_user_id == user1.id
958 assert not res.owner_community_id
959 assert not res.owner_group_id
960 assert res.thread.thread_id
961 assert res.can_edit
962 assert res.can_moderate
964 with events_session(token3) as api:
965 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
967 assert res.is_next
968 assert res.title == "Dummy Title"
969 assert res.slug == "dummy-title"
970 assert res.content == "Dummy content."
971 assert not res.photo_url
972 assert res.HasField("location")
973 assert res.location.lat == 0.1
974 assert res.location.lng == 0.2
975 assert res.location.address == "Near Null Island"
976 assert time_before <= to_aware_datetime(res.created) <= now()
977 assert time_before <= to_aware_datetime(res.last_edited) <= now()
978 assert res.creator_user_id == user1.id
979 assert to_aware_datetime(res.start_time) == to_event_time_granularity(start_time)
980 assert to_aware_datetime(res.end_time) == to_event_time_granularity(end_time)
981 assert is_utc_or_gmt(res.timezone)
982 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING
983 assert not res.organizer
984 assert not res.subscriber
985 assert res.going_count == 2
986 assert res.organizer_count == 1
987 assert res.subscriber_count == 3
988 assert res.owner_user_id == user1.id
989 assert not res.owner_community_id
990 assert not res.owner_group_id
991 assert res.thread.thread_id
992 assert not res.can_edit
993 assert not res.can_moderate
996def test_CancelEvent(db, moderator: Moderator):
997 # event creator
998 user1, token1 = generate_user()
999 # community moderator
1000 user2, token2 = generate_user()
1001 # third parties
1002 user3, token3 = generate_user()
1003 user4, token4 = generate_user()
1004 user5, token5 = generate_user()
1005 user6, token6 = generate_user()
1007 with session_scope() as session:
1008 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1010 start_time = now() + timedelta(hours=2)
1011 end_time = start_time + timedelta(hours=3)
1013 with events_session(token1) as api:
1014 res = api.CreateEvent(
1015 events_pb2.CreateEventReq(
1016 title="Dummy Title",
1017 content="Dummy content.",
1018 location=events_pb2.EventLocation(
1019 address="Near Null Island",
1020 lat=0.1,
1021 lng=0.2,
1022 ),
1023 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1024 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1025 )
1026 )
1028 event_id = res.event_id
1030 moderator.approve_event_occurrence(event_id)
1032 with events_session(token4) as api:
1033 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1035 with events_session(token5) as api:
1036 api.SetEventAttendance(
1037 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1038 )
1040 with events_session(token6) as api:
1041 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1043 with events_session(token1) as api:
1044 res = api.CancelEvent(
1045 events_pb2.CancelEventReq(
1046 event_id=event_id,
1047 )
1048 )
1050 with events_session(token1) as api:
1051 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1052 assert res.is_cancelled
1054 with events_session(token1) as api:
1055 with pytest.raises(grpc.RpcError) as e:
1056 api.UpdateEvent(
1057 events_pb2.UpdateEventReq(
1058 event_id=event_id,
1059 title=wrappers_pb2.StringValue(value="New Title"),
1060 )
1061 )
1062 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1063 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1065 with pytest.raises(grpc.RpcError) as e:
1066 api.InviteEventOrganizer(
1067 events_pb2.InviteEventOrganizerReq(
1068 event_id=event_id,
1069 user_id=user3.id,
1070 )
1071 )
1072 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1073 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1075 with pytest.raises(grpc.RpcError) as e:
1076 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id))
1077 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1078 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1080 with events_session(token3) as api:
1081 with pytest.raises(grpc.RpcError) as e:
1082 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1083 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1084 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1086 with pytest.raises(grpc.RpcError) as e:
1087 api.SetEventAttendance(
1088 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1089 )
1090 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1091 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled."
1093 with events_session(token1) as api:
1094 for include_cancelled in [True, False]:
1095 res = api.ListEventOccurrences(
1096 events_pb2.ListEventOccurrencesReq(
1097 event_id=event_id,
1098 include_cancelled=include_cancelled,
1099 )
1100 )
1101 if include_cancelled:
1102 assert len(res.events) > 0
1103 else:
1104 assert len(res.events) == 0
1106 res = api.ListMyEvents(
1107 events_pb2.ListMyEventsReq(
1108 include_cancelled=include_cancelled,
1109 )
1110 )
1111 if include_cancelled:
1112 assert len(res.events) > 0
1113 else:
1114 assert len(res.events) == 0
1117def test_ListEventAttendees(db, moderator: Moderator):
1118 # event creator
1119 user1, token1 = generate_user()
1120 # others
1121 user2, token2 = generate_user()
1122 user3, token3 = generate_user()
1123 user4, token4 = generate_user()
1124 user5, token5 = generate_user()
1125 user6, token6 = generate_user()
1127 with session_scope() as session:
1128 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1130 with events_session(token1) as api:
1131 event_id = api.CreateEvent(
1132 events_pb2.CreateEventReq(
1133 title="Dummy Title",
1134 content="Dummy content.",
1135 location=events_pb2.EventLocation(
1136 address="Near Null Island",
1137 lat=0.1,
1138 lng=0.2,
1139 ),
1140 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1141 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1142 )
1143 ).event_id
1145 moderator.approve_event_occurrence(event_id)
1147 for token in [token2, token3, token4, token5]:
1148 with events_session(token) as api:
1149 api.SetEventAttendance(
1150 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1151 )
1153 with events_session(token6) as api:
1154 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5
1156 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2))
1157 assert res.attendee_user_ids == [user1.id, user2.id]
1159 res = api.ListEventAttendees(
1160 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1161 )
1162 assert res.attendee_user_ids == [user3.id, user4.id]
1164 res = api.ListEventAttendees(
1165 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1166 )
1167 assert res.attendee_user_ids == [user5.id]
1168 assert not res.next_page_token
1171def test_ListEventSubscribers(db, moderator: Moderator):
1172 # event creator
1173 user1, token1 = generate_user()
1174 # others
1175 user2, token2 = generate_user()
1176 user3, token3 = generate_user()
1177 user4, token4 = generate_user()
1178 user5, token5 = generate_user()
1179 user6, token6 = generate_user()
1181 with session_scope() as session:
1182 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1184 with events_session(token1) as api:
1185 event_id = api.CreateEvent(
1186 events_pb2.CreateEventReq(
1187 title="Dummy Title",
1188 content="Dummy content.",
1189 location=events_pb2.EventLocation(
1190 address="Near Null Island",
1191 lat=0.1,
1192 lng=0.2,
1193 ),
1194 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1195 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1196 )
1197 ).event_id
1199 moderator.approve_event_occurrence(event_id)
1201 for token in [token2, token3, token4, token5]:
1202 with events_session(token) as api:
1203 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1205 with events_session(token6) as api:
1206 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5
1208 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2))
1209 assert res.subscriber_user_ids == [user1.id, user2.id]
1211 res = api.ListEventSubscribers(
1212 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1213 )
1214 assert res.subscriber_user_ids == [user3.id, user4.id]
1216 res = api.ListEventSubscribers(
1217 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1218 )
1219 assert res.subscriber_user_ids == [user5.id]
1220 assert not res.next_page_token
1223def test_ListEventOrganizers(db, moderator: Moderator):
1224 # event creator
1225 user1, token1 = generate_user()
1226 # others
1227 user2, token2 = generate_user()
1228 user3, token3 = generate_user()
1229 user4, token4 = generate_user()
1230 user5, token5 = generate_user()
1231 user6, token6 = generate_user()
1233 with session_scope() as session:
1234 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1236 with events_session(token1) as api:
1237 event_id = api.CreateEvent(
1238 events_pb2.CreateEventReq(
1239 title="Dummy Title",
1240 content="Dummy content.",
1241 location=events_pb2.EventLocation(
1242 address="Near Null Island",
1243 lat=0.1,
1244 lng=0.2,
1245 ),
1246 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1247 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1248 )
1249 ).event_id
1251 moderator.approve_event_occurrence(event_id)
1253 with events_session(token1) as api:
1254 for user_id in [user2.id, user3.id, user4.id, user5.id]:
1255 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id))
1257 with events_session(token6) as api:
1258 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5
1260 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2))
1261 assert res.organizer_user_ids == [user1.id, user2.id]
1263 res = api.ListEventOrganizers(
1264 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1265 )
1266 assert res.organizer_user_ids == [user3.id, user4.id]
1268 res = api.ListEventOrganizers(
1269 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token)
1270 )
1271 assert res.organizer_user_ids == [user5.id]
1272 assert not res.next_page_token
1275def test_TransferEvent(db):
1276 user1, token1 = generate_user()
1277 user2, token2 = generate_user()
1278 user3, token3 = generate_user()
1279 user4, token4 = generate_user()
1281 with session_scope() as session:
1282 c = create_community(session, 0, 2, "Community", [user3], [], None)
1283 h = create_group(session, "Group", [user4], [], c)
1284 c_id = c.id
1285 h_id = h.id
1287 with events_session(token1) as api:
1288 event_id = api.CreateEvent(
1289 events_pb2.CreateEventReq(
1290 title="Dummy Title",
1291 content="Dummy content.",
1292 location=events_pb2.EventLocation(
1293 address="Near Null Island",
1294 lat=0.1,
1295 lng=0.2,
1296 ),
1297 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1298 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1299 )
1300 ).event_id
1302 api.TransferEvent(
1303 events_pb2.TransferEventReq(
1304 event_id=event_id,
1305 new_owner_community_id=c_id,
1306 )
1307 )
1309 # remove ourselves as organizer, otherwise we can still edit it
1310 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1312 with pytest.raises(grpc.RpcError) as e:
1313 api.TransferEvent(
1314 events_pb2.TransferEventReq(
1315 event_id=event_id,
1316 new_owner_group_id=h_id,
1317 )
1318 )
1319 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1320 assert e.value.details() == "You're not allowed to transfer that event."
1322 event_id = api.CreateEvent(
1323 events_pb2.CreateEventReq(
1324 title="Dummy Title",
1325 content="Dummy content.",
1326 location=events_pb2.EventLocation(
1327 address="Near Null Island",
1328 lat=0.1,
1329 lng=0.2,
1330 ),
1331 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1332 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1333 )
1334 ).event_id
1336 api.TransferEvent(
1337 events_pb2.TransferEventReq(
1338 event_id=event_id,
1339 new_owner_group_id=h_id,
1340 )
1341 )
1343 # remove ourselves as organizer, otherwise we can still edit it
1344 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1346 with pytest.raises(grpc.RpcError) as e:
1347 api.TransferEvent(
1348 events_pb2.TransferEventReq(
1349 event_id=event_id,
1350 new_owner_community_id=c_id,
1351 )
1352 )
1353 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1354 assert e.value.details() == "You're not allowed to transfer that event."
1357def test_SetEventSubscription(db, moderator: Moderator):
1358 user1, token1 = generate_user()
1359 user2, token2 = generate_user()
1361 with session_scope() as session:
1362 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1364 with events_session(token1) as api:
1365 event_id = api.CreateEvent(
1366 events_pb2.CreateEventReq(
1367 title="Dummy Title",
1368 content="Dummy content.",
1369 location=events_pb2.EventLocation(
1370 address="Near Null Island",
1371 lat=0.1,
1372 lng=0.2,
1373 ),
1374 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1375 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1376 )
1377 ).event_id
1379 moderator.approve_event_occurrence(event_id)
1381 with events_session(token2) as api:
1382 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1383 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
1384 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1385 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False))
1386 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber
1389def test_SetEventAttendance(db, moderator: Moderator):
1390 user1, token1 = generate_user()
1391 user2, token2 = generate_user()
1393 with session_scope() as session:
1394 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1396 with events_session(token1) as api:
1397 event_id = api.CreateEvent(
1398 events_pb2.CreateEventReq(
1399 title="Dummy Title",
1400 content="Dummy content.",
1401 location=events_pb2.EventLocation(
1402 address="Near Null Island",
1403 lat=0.1,
1404 lng=0.2,
1405 ),
1406 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1407 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1408 )
1409 ).event_id
1411 moderator.approve_event_occurrence(event_id)
1413 with events_session(token2) as api:
1414 assert (
1415 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1416 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1417 )
1418 api.SetEventAttendance(
1419 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1420 )
1421 assert (
1422 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1423 == events_pb2.ATTENDANCE_STATE_GOING
1424 )
1425 api.SetEventAttendance(
1426 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1427 )
1428 assert (
1429 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state
1430 == events_pb2.ATTENDANCE_STATE_NOT_GOING
1431 )
1434def test_InviteEventOrganizer(db, moderator: Moderator):
1435 user1, token1 = generate_user()
1436 user2, token2 = generate_user()
1438 with session_scope() as session:
1439 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1441 with events_session(token1) as api:
1442 event_id = api.CreateEvent(
1443 events_pb2.CreateEventReq(
1444 title="Dummy Title",
1445 content="Dummy content.",
1446 location=events_pb2.EventLocation(
1447 address="Near Null Island",
1448 lat=0.1,
1449 lng=0.2,
1450 ),
1451 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1452 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1453 )
1454 ).event_id
1456 moderator.approve_event_occurrence(event_id)
1458 with events_session(token2) as api:
1459 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1461 with pytest.raises(grpc.RpcError) as e:
1462 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id))
1463 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1464 assert e.value.details() == "You're not allowed to edit that event."
1466 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1468 with events_session(token1) as api:
1469 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1471 with events_session(token2) as api:
1472 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1475def test_ListEventOccurrences(db):
1476 user1, token1 = generate_user()
1477 user2, token2 = generate_user()
1478 user3, token3 = generate_user()
1480 with session_scope() as session:
1481 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1483 start = now()
1485 event_ids = []
1487 with events_session(token1) as api:
1488 res = api.CreateEvent(
1489 events_pb2.CreateEventReq(
1490 title="First occurrence",
1491 content="Dummy content.",
1492 parent_community_id=c_id,
1493 location=events_pb2.EventLocation(
1494 address="Near Null Island",
1495 lat=0.1,
1496 lng=0.2,
1497 ),
1498 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
1499 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
1500 )
1501 )
1503 event_ids.append(res.event_id)
1505 for i in range(5):
1506 res = api.ScheduleEvent(
1507 events_pb2.ScheduleEventReq(
1508 event_id=event_ids[-1],
1509 content=f"{i}th occurrence",
1510 location=events_pb2.EventLocation(
1511 address="Near Null Island",
1512 lat=0.1,
1513 lng=0.2,
1514 ),
1515 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
1516 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
1517 )
1518 )
1520 event_ids.append(res.event_id)
1522 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2))
1523 assert [event.event_id for event in res.events] == event_ids[:2]
1525 res = api.ListEventOccurrences(
1526 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1527 )
1528 assert [event.event_id for event in res.events] == event_ids[2:4]
1530 res = api.ListEventOccurrences(
1531 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token)
1532 )
1533 assert [event.event_id for event in res.events] == event_ids[4:6]
1534 assert not res.next_page_token
1537def test_ListMyEvents(db, moderator: Moderator):
1538 user1, token1 = generate_user()
1539 user2, token2 = generate_user()
1540 user3, token3 = generate_user()
1541 user4, token4 = generate_user()
1542 user5, token5 = generate_user()
1544 with session_scope() as session:
1545 # Create global (world) -> macroregion -> region -> subregion hierarchy
1546 # my_communities_exclude_global filters out world, macroregion, and region level communities
1547 global_community = create_community(session, 0, 100, "Global", [user3], [], None)
1548 c_id = global_community.id
1549 macroregion_community = create_community(
1550 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community
1551 )
1552 region_community = create_community(
1553 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community
1554 )
1555 subregion_community = create_community(
1556 session, 0, 25, "Subregion Community", [user3, user4], [], region_community
1557 )
1558 c2_id = subregion_community.id
1560 start = now()
1562 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq:
1563 return events_pb2.CreateEventReq(
1564 title="Dummy Title",
1565 content="Dummy content.",
1566 location=events_pb2.EventLocation(
1567 address="Near Null Island",
1568 lat=0.1,
1569 lng=0.2,
1570 ),
1571 parent_community_id=community_id,
1572 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now)),
1573 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours_from_now + 0.5)),
1574 )
1576 with events_session(token1) as api:
1577 e2 = api.CreateEvent(new_event(2, c_id)).event_id
1579 moderator.approve_event_occurrence(e2)
1581 with events_session(token2) as api:
1582 e1 = api.CreateEvent(new_event(1, c_id)).event_id
1584 moderator.approve_event_occurrence(e1)
1586 with events_session(token1) as api:
1587 e3 = api.CreateEvent(new_event(3, c_id)).event_id
1589 moderator.approve_event_occurrence(e3)
1591 with events_session(token2) as api:
1592 e5 = api.CreateEvent(new_event(5, c_id)).event_id
1594 moderator.approve_event_occurrence(e5)
1596 with events_session(token3) as api:
1597 e4 = api.CreateEvent(new_event(4, c_id)).event_id
1599 moderator.approve_event_occurrence(e4)
1601 with events_session(token4) as api:
1602 e6 = api.CreateEvent(new_event(6, c2_id)).event_id
1604 moderator.approve_event_occurrence(e6)
1606 with events_session(token1) as api:
1607 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id))
1609 with events_session(token1) as api:
1610 api.SetEventAttendance(
1611 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1612 )
1613 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True))
1615 with events_session(token2) as api:
1616 api.SetEventAttendance(
1617 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1618 )
1620 with events_session(token3) as api:
1621 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True))
1623 with events_session(token1) as api:
1624 # test pagination with token first
1625 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2))
1626 assert [event.event_id for event in res.events] == [e1, e2]
1627 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token))
1628 assert [event.event_id for event in res.events] == [e3, e4]
1629 assert not res.next_page_token
1631 res = api.ListMyEvents(
1632 events_pb2.ListMyEventsReq(
1633 subscribed=True,
1634 attending=True,
1635 organizing=True,
1636 )
1637 )
1638 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1640 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1641 assert [event.event_id for event in res.events] == [e1, e2, e3, e4]
1643 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1644 assert [event.event_id for event in res.events] == [e2, e3, e4]
1646 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1647 assert [event.event_id for event in res.events] == [e1, e2, e3]
1649 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1650 assert [event.event_id for event in res.events] == [e2, e3]
1652 with events_session(token1) as api:
1653 # Test pagination with page_number and verify total_items
1654 res = api.ListMyEvents(
1655 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True)
1656 )
1657 assert [event.event_id for event in res.events] == [e1, e2]
1658 assert res.total_items == 4
1660 res = api.ListMyEvents(
1661 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True)
1662 )
1663 assert [event.event_id for event in res.events] == [e3, e4]
1664 assert res.total_items == 4
1666 # Verify no more pages
1667 res = api.ListMyEvents(
1668 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True)
1669 )
1670 assert not res.events
1671 assert res.total_items == 4
1673 with events_session(token2) as api:
1674 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1675 assert [event.event_id for event in res.events] == [e1, e3, e5]
1677 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1678 assert [event.event_id for event in res.events] == [e1, e5]
1680 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1681 assert [event.event_id for event in res.events] == [e1, e3, e5]
1683 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1684 assert [event.event_id for event in res.events] == [e1, e5]
1686 with events_session(token3) as api:
1687 # user3 is member of both global (c_id) and child (c2_id) communities
1688 res = api.ListMyEvents(events_pb2.ListMyEventsReq())
1689 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1691 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True))
1692 assert [event.event_id for event in res.events] == [e2, e4]
1694 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True))
1695 assert [event.event_id for event in res.events] == [e4]
1697 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True))
1698 assert [event.event_id for event in res.events] == [e3, e4]
1700 # my_communities returns events from both communities user3 is a member of
1701 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1702 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1704 # my_communities_exclude_global filters out events from global community (node_id=1)
1705 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True))
1706 assert [event.event_id for event in res.events] == [e6]
1708 # my_communities_exclude_global works independently of my_communities flag
1709 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True))
1710 assert [event.event_id for event in res.events] == [e6]
1712 # my_communities_exclude_global filters organizing results too
1713 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True))
1714 assert [event.event_id for event in res.events] == []
1716 # my_communities_exclude_global filters subscribed results too
1717 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True))
1718 assert [event.event_id for event in res.events] == []
1720 with events_session(token5) as api:
1721 res = api.ListAllEvents(events_pb2.ListAllEventsReq())
1722 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6]
1725def test_list_my_events_exclude_attending(db, moderator: Moderator):
1726 user1, token1 = generate_user()
1727 user2, token2 = generate_user()
1729 with session_scope() as session:
1730 c = create_community(session, 0, 100, "Community", [user1, user2], [], None)
1731 c_id = c.id
1733 start = now()
1735 def make_event(hours):
1736 return events_pb2.CreateEventReq(
1737 title="Test Event",
1738 content="Test content.",
1739 location=events_pb2.EventLocation(
1740 address="Near Null Island",
1741 lat=0.1,
1742 lng=0.2,
1743 ),
1744 parent_community_id=c_id,
1745 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours)),
1746 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=hours + 1)),
1747 )
1749 # user1 organizes e_own; user2 organizes e_attending and e_community_only
1750 with events_session(token1) as api:
1751 e_own = api.CreateEvent(make_event(1)).event_id
1753 with events_session(token2) as api:
1754 e_attending = api.CreateEvent(make_event(2)).event_id
1755 e_community_only = api.CreateEvent(make_event(3)).event_id
1756 # e_both: user1 will be both organizer and attendee
1757 e_both = api.CreateEvent(make_event(4)).event_id
1759 moderator.approve_event_occurrence(e_own)
1760 moderator.approve_event_occurrence(e_attending)
1761 moderator.approve_event_occurrence(e_community_only)
1762 moderator.approve_event_occurrence(e_both)
1764 # invite user1 as organizer of e_both
1765 with events_session(token2) as api:
1766 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id))
1768 # user1 RSVPs to e_attending and e_both
1769 with events_session(token1) as api:
1770 api.SetEventAttendance(
1771 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1772 )
1773 api.SetEventAttendance(
1774 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1775 )
1777 with events_session(token1) as api:
1778 # baseline: all four community events visible
1779 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True))
1780 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both}
1782 # exclude_attending removes events user1 is attending (e_attending, e_both)
1783 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only
1784 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1785 assert [e.event_id for e in res.events] == [e_community_only]
1787 # exclude_attending with attending=True: invalid combination
1788 with pytest.raises(grpc.RpcError) as e:
1789 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True))
1790 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT
1792 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it
1793 with events_session(token2) as api:
1794 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True))
1795 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation)
1796 assert [e.event_id for e in res.events] == [e_own]
1799def test_RemoveEventOrganizer(db, moderator: Moderator):
1800 user1, token1 = generate_user()
1801 user2, token2 = generate_user()
1803 with session_scope() as session:
1804 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1806 with events_session(token1) as api:
1807 event_id = api.CreateEvent(
1808 events_pb2.CreateEventReq(
1809 title="Dummy Title",
1810 content="Dummy content.",
1811 location=events_pb2.EventLocation(
1812 address="Near Null Island",
1813 lat=0.1,
1814 lng=0.2,
1815 ),
1816 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
1817 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
1818 )
1819 ).event_id
1821 moderator.approve_event_occurrence(event_id)
1823 with events_session(token2) as api:
1824 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1826 with pytest.raises(grpc.RpcError) as e:
1827 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1828 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1829 assert e.value.details() == "You're not allowed to edit that event."
1831 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1833 with events_session(token1) as api:
1834 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1836 with pytest.raises(grpc.RpcError) as e:
1837 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1838 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1839 assert e.value.details() == "You cannot remove the event owner as an organizer."
1841 with events_session(token2) as api:
1842 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1843 assert res.organizer
1844 assert res.organizer_count == 2
1845 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1846 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer
1848 with pytest.raises(grpc.RpcError) as e:
1849 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id))
1850 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION
1851 assert e.value.details() == "You're not allowed to edit that event."
1853 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1854 assert not res.organizer
1855 assert res.organizer_count == 1
1857 # Test that event owner can remove co-organizers
1858 with events_session(token1) as api:
1859 # Add user2 back as organizer
1860 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1862 # Verify user2 is now an organizer
1863 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1864 assert res.organizer_count == 2
1866 # Event owner can remove co-organizer
1867 api.RemoveEventOrganizer(
1868 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id))
1869 )
1871 # Verify user2 is no longer an organizer
1872 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
1873 assert res.organizer_count == 1
1875 # Test that non-organizers cannot remove other organizers
1876 with events_session(token2) as api:
1877 # User2 cannot invite themselves as organizer (not the owner)
1878 with pytest.raises(grpc.RpcError) as e:
1879 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1880 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED
1881 assert e.value.details() == "You're not allowed to edit that event."
1883 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first)
1884 with events_session(token1) as api:
1885 # Add user2 back as organizer
1886 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id))
1889def test_ListEventAttendees_regression(db):
1890 # see issue #1617:
1891 #
1892 # 1. Create an event
1893 # 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`
1894 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`)
1895 # 4. Change the current user's attendance state to "going" again
1896 #
1897 # **Expected behaviour**
1898 # `ListEventAttendees` should return the current user's ID
1899 #
1900 # **Actual/current behaviour**
1901 # `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
1903 user1, token1 = generate_user()
1904 user2, token2 = generate_user()
1905 user3, token3 = generate_user()
1906 user4, token4 = generate_user()
1907 user5, token5 = generate_user()
1909 with session_scope() as session:
1910 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id
1912 start_time = now() + timedelta(hours=2)
1913 end_time = start_time + timedelta(hours=3)
1915 with events_session(token1) as api:
1916 res = api.CreateEvent(
1917 events_pb2.CreateEventReq(
1918 title="Dummy Title",
1919 content="Dummy content.",
1920 location=events_pb2.EventLocation(
1921 address="Near Null Island",
1922 lat=0.1,
1923 lng=0.2,
1924 ),
1925 parent_community_id=c_id,
1926 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1927 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1928 )
1929 )
1931 res = api.TransferEvent(
1932 events_pb2.TransferEventReq(
1933 event_id=res.event_id,
1934 new_owner_community_id=c_id,
1935 )
1936 )
1938 event_id = res.event_id
1940 api.SetEventAttendance(
1941 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING)
1942 )
1943 api.SetEventAttendance(
1944 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
1945 )
1947 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id))
1948 assert len(res.attendee_user_ids) == 1
1949 assert res.attendee_user_ids[0] == user1.id
1952def test_GetEventCalendarFile(db, moderator: Moderator):
1953 user1, token1 = generate_user()
1954 user2, token2 = generate_user()
1956 with session_scope() as session:
1957 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
1959 start_time = now() + timedelta(hours=2)
1960 end_time = start_time + timedelta(hours=3)
1962 with events_session(token1) as api:
1963 created_event: events_pb2.Event = api.CreateEvent(
1964 events_pb2.CreateEventReq(
1965 title="Dummy Title",
1966 content="Dummy content.",
1967 parent_community_id=c_id,
1968 location=events_pb2.EventLocation(
1969 address="Near Null Island",
1970 lat=0.1,
1971 lng=0.2,
1972 ),
1973 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
1974 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
1975 )
1976 )
1977 event_id = created_event.event_id
1979 moderator.approve_event_occurrence(event_id)
1981 with events_session(token1) as api:
1982 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
1983 ics_string = file_res.data.decode("utf-8")
1984 assert "SUMMARY:Dummy Title" in ics_string
1985 assert "DESCRIPTION:Dummy content." in ics_string
1986 assert "LOCATION:Near Null Island" in ics_string
1987 assert "STATUS:CANCELLED" not in ics_string
1988 pre_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1))
1990 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
1992 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
1993 ics_string = file_res.data.decode("utf-8")
1994 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
1995 assert "STATUS:CANCELLED" in ics_string
1996 post_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1))
1997 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
1998 assert post_cancel_sequence >= pre_cancel_sequence
2001def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2002 user1, token1 = generate_user()
2003 user2, token2 = generate_user()
2004 user3, token3 = generate_user()
2005 user4, token4 = generate_user()
2007 with session_scope() as session:
2008 c = create_community(session, 0, 2, "Community", [user3], [], None)
2009 h = create_group(session, "Group", [user4], [], c)
2010 c_id = c.id
2011 h_id = h.id
2012 user4_id = user4.id
2014 with events_session(token1) as api:
2015 event = api.CreateEvent(
2016 events_pb2.CreateEventReq(
2017 title="Dummy Title",
2018 content="Dummy content.",
2019 location=events_pb2.EventLocation(
2020 address="Near Null Island",
2021 lat=0.1,
2022 lng=0.2,
2023 ),
2024 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2025 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2026 )
2027 )
2029 moderator.approve_event_occurrence(event.event_id)
2031 with threads_session(token2) as api:
2032 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2034 moderator.approve_thread_post(reply_id)
2036 with events_session(token3) as api:
2037 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2038 assert res.thread.num_responses == 1
2040 with threads_session(token3) as api:
2041 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2042 assert len(ret.replies) == 1
2043 assert not ret.next_page_token
2044 assert ret.replies[0].thread_id == reply_id
2045 assert ret.replies[0].content == "hi"
2046 assert ret.replies[0].author_user_id == user2.id
2047 assert ret.replies[0].num_replies == 0
2049 nested_reply_id = api.PostReply(
2050 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2051 ).thread_id
2053 moderator.approve_thread_post(nested_reply_id)
2055 process_jobs()
2057 push = push_collector.pop_for_user(user1.id, last=True)
2058 assert push.topic_action == NotificationTopicAction.event__comment.display
2059 assert push.content.title == f"{user2.name} • Dummy Title"
2060 assert push.content.ios_title == user2.name
2061 assert push.content.ios_subtitle == "Commented on Dummy Title"
2062 assert push.content.body == "hi"
2064 push = push_collector.pop_for_user(user2.id, last=True)
2065 assert push.content.title == f"{user3.name} • Dummy Title"
2067 assert push_collector.count_for_user(user4_id) == 0
2070def test_can_overlap_other_events_schedule_regression(db):
2071 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2072 user, token = generate_user()
2074 with session_scope() as session:
2075 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2077 start = now()
2079 with events_session(token) as api:
2080 # create another event, should be able to overlap with this one
2081 api.CreateEvent(
2082 events_pb2.CreateEventReq(
2083 title="Dummy Title",
2084 content="Dummy content.",
2085 parent_community_id=c_id,
2086 location=events_pb2.EventLocation(
2087 address="Near Null Island",
2088 lat=0.1,
2089 lng=0.2,
2090 ),
2091 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2092 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2093 )
2094 )
2096 # this event
2097 res = api.CreateEvent(
2098 events_pb2.CreateEventReq(
2099 title="Dummy Title",
2100 content="Dummy content.",
2101 parent_community_id=c_id,
2102 location=events_pb2.EventLocation(
2103 address="Near Null Island",
2104 lat=0.1,
2105 lng=0.2,
2106 ),
2107 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2108 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2109 )
2110 )
2112 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2113 api.ScheduleEvent(
2114 events_pb2.ScheduleEventReq(
2115 event_id=res.event_id,
2116 content="New event occurrence",
2117 location=events_pb2.EventLocation(
2118 address="A bit further but still near Null Island",
2119 lat=0.3,
2120 lng=0.2,
2121 ),
2122 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2123 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2124 )
2125 )
2128def test_can_overlap_other_events_update_regression(db):
2129 user, token = generate_user()
2131 with session_scope() as session:
2132 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2134 start = now()
2136 with events_session(token) as api:
2137 # create another event, should be able to overlap with this one
2138 api.CreateEvent(
2139 events_pb2.CreateEventReq(
2140 title="Dummy Title",
2141 content="Dummy content.",
2142 parent_community_id=c_id,
2143 location=events_pb2.EventLocation(
2144 address="Near Null Island",
2145 lat=0.1,
2146 lng=0.2,
2147 ),
2148 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2149 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2150 )
2151 )
2153 res = api.CreateEvent(
2154 events_pb2.CreateEventReq(
2155 title="Dummy Title",
2156 content="Dummy content.",
2157 parent_community_id=c_id,
2158 location=events_pb2.EventLocation(
2159 address="Near Null Island",
2160 lat=0.1,
2161 lng=0.2,
2162 ),
2163 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2164 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2165 )
2166 )
2168 event_id = api.ScheduleEvent(
2169 events_pb2.ScheduleEventReq(
2170 event_id=res.event_id,
2171 content="New event occurrence",
2172 location=events_pb2.EventLocation(
2173 address="A bit further but still near Null Island",
2174 lat=0.3,
2175 lng=0.2,
2176 ),
2177 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2178 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2179 )
2180 ).event_id
2182 # can overlap with this current existing occurrence
2183 api.UpdateEvent(
2184 events_pb2.UpdateEventReq(
2185 event_id=event_id,
2186 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2187 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2188 ),
2189 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2190 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2191 ),
2192 )
2193 )
2195 api.UpdateEvent(
2196 events_pb2.UpdateEventReq(
2197 event_id=event_id,
2198 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2199 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2200 ),
2201 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2202 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2203 ),
2204 )
2205 )
2208def test_list_past_events_regression(db):
2209 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2210 user, token = generate_user()
2212 with session_scope() as session:
2213 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2215 start = now()
2217 with events_session(token) as api:
2218 api.CreateEvent(
2219 events_pb2.CreateEventReq(
2220 title="Dummy Title",
2221 content="Dummy content.",
2222 parent_community_id=c_id,
2223 location=events_pb2.EventLocation(
2224 address="Near Null Island",
2225 lat=0.1,
2226 lng=0.2,
2227 ),
2228 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2229 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2230 )
2231 )
2233 with session_scope() as session:
2234 session.execute(
2235 update(EventOccurrence).values(
2236 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2237 )
2238 )
2240 with events_session(token) as api:
2241 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2242 assert len(res.events) == 1
2245def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2246 user1, token1 = generate_user(complete_profile=True)
2247 user2, token2 = generate_user()
2248 user3, token3 = generate_user()
2249 user4, token4 = generate_user()
2250 user5, token5 = generate_user(is_superuser=True)
2252 with session_scope() as session:
2253 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2254 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2255 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2256 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2258 enforce_community_memberships()
2260 with events_session(token1) as api:
2261 res = api.CreateEvent(
2262 events_pb2.CreateEventReq(
2263 title="Dummy Title",
2264 content="Dummy content.",
2265 parent_community_id=c_id,
2266 location=events_pb2.EventLocation(
2267 address="Near Null Island",
2268 lat=0.1,
2269 lng=0.2,
2270 ),
2271 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2272 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2273 )
2274 )
2275 user_url = f"http://localhost:3000/user/{user1.username}"
2276 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2278 event_id = res.event_id
2280 moderator.approve_event_occurrence(event_id)
2282 with events_session(token1) as api:
2283 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2285 email = email_collector.pop_for_mods(last=True)
2287 assert user_url in email.plain
2288 assert event_url in email.plain
2290 # can't send another req
2291 with pytest.raises(grpc.RpcError) as err:
2292 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2293 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2294 assert err.value.details() == "You have already requested a community invite for this event."
2296 # another user can send one though
2297 with events_session(token3) as api:
2298 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2300 # but not a non-admin
2301 with events_session(token2) as api:
2302 with pytest.raises(grpc.RpcError) as err:
2303 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2304 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2305 assert err.value.details() == "You're not allowed to edit that event."
2307 with real_editor_session(token5) as editor:
2308 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2309 assert len(res.requests) == 2
2310 assert res.requests[0].user_id == user1.id
2311 assert res.requests[0].approx_users_to_notify == 3
2312 assert res.requests[1].user_id == user3.id
2313 assert res.requests[1].approx_users_to_notify == 3
2315 editor.DecideEventCommunityInviteRequest(
2316 editor_pb2.DecideEventCommunityInviteRequestReq(
2317 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2318 approve=False,
2319 )
2320 )
2322 editor.DecideEventCommunityInviteRequest(
2323 editor_pb2.DecideEventCommunityInviteRequestReq(
2324 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2325 approve=True,
2326 )
2327 )
2329 # not after approve
2330 with events_session(token4) as api:
2331 with pytest.raises(grpc.RpcError) as err:
2332 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2333 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2334 assert err.value.details() == "A community invite has already been sent out for this event."
2337def test_update_event_should_notify_queues_job():
2338 user, token = generate_user()
2339 start = now()
2341 with session_scope() as session:
2342 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2344 # create an event
2345 with events_session(token) as api:
2346 create_res = api.CreateEvent(
2347 events_pb2.CreateEventReq(
2348 title="Dummy Title",
2349 content="Dummy content.",
2350 parent_community_id=c_id,
2351 location=events_pb2.EventLocation(
2352 address="Near Null Island",
2353 lat=1.0,
2354 lng=2.0,
2355 ),
2356 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2357 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2358 )
2359 )
2361 event_id = create_res.event_id
2363 # measure initial background job queue length
2364 with session_scope() as session:
2365 jobs = session.query(BackgroundJob).all()
2366 job_length_before_update = len(jobs)
2368 # update with should_notify=False, expect no change in background job queue
2369 api.UpdateEvent(
2370 events_pb2.UpdateEventReq(
2371 event_id=event_id,
2372 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2373 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2374 ),
2375 should_notify=False,
2376 )
2377 )
2379 with session_scope() as session:
2380 jobs = session.query(BackgroundJob).all()
2381 assert len(jobs) == job_length_before_update
2383 # update with should_notify=True, expect one new background job added
2384 api.UpdateEvent(
2385 events_pb2.UpdateEventReq(
2386 event_id=event_id,
2387 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2388 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2389 ),
2390 should_notify=True,
2391 )
2392 )
2394 with session_scope() as session:
2395 jobs = session.query(BackgroundJob).all()
2396 assert len(jobs) == job_length_before_update + 1
2399def test_event_photo_key(db):
2400 """Test that events return the photo_key field when a photo is set."""
2401 user, token = generate_user()
2403 start_time = now() + timedelta(hours=2)
2404 end_time = start_time + timedelta(hours=3)
2406 # Create a community and an upload for the event photo
2407 with session_scope() as session:
2408 create_community(session, 0, 2, "Community", [user], [], None)
2409 upload = Upload(
2410 key="test_event_photo_key_123",
2411 filename="test_event_photo_key_123.jpg",
2412 creator_user_id=user.id,
2413 )
2414 session.add(upload)
2416 with events_session(token) as api:
2417 # Create event without photo
2418 res = api.CreateEvent(
2419 events_pb2.CreateEventReq(
2420 title="Event Without Photo",
2421 content="No photo content.",
2422 photo_key=None,
2423 location=events_pb2.EventLocation(
2424 address="Near Null Island",
2425 lat=0.1,
2426 lng=0.2,
2427 ),
2428 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2429 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2430 )
2431 )
2433 assert res.photo_key == ""
2434 assert res.photo_url == ""
2436 # Create event with photo
2437 res_with_photo = api.CreateEvent(
2438 events_pb2.CreateEventReq(
2439 title="Event With Photo",
2440 content="Has photo content.",
2441 photo_key="test_event_photo_key_123",
2442 location=events_pb2.EventLocation(
2443 address="Near Null Island",
2444 lat=0.1,
2445 lng=0.2,
2446 ),
2447 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2448 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2449 )
2450 )
2452 assert res_with_photo.photo_key == "test_event_photo_key_123"
2453 assert "test_event_photo_key_123" in res_with_photo.photo_url
2455 event_id = res_with_photo.event_id
2457 # Verify photo_key is returned when getting the event
2458 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2459 assert get_res.photo_key == "test_event_photo_key_123"
2460 assert "test_event_photo_key_123" in get_res.photo_url
2463def test_event_timezone(db):
2464 user, token = generate_user()
2466 with session_scope() as session:
2467 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2469 # Midnight future day, UTC timezone
2470 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2471 end_time = start_time + timedelta(days=1)
2473 with events_session(token) as api:
2474 create_res: events_pb2.Event = api.CreateEvent(
2475 events_pb2.CreateEventReq(
2476 title="Dummy Title",
2477 content="Dummy content.",
2478 photo_key=None,
2479 parent_community_id=c_id,
2480 # timezone_areas.sql-fake has a region for Europe/Helsinki
2481 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2482 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2483 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2484 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2485 )
2486 )
2488 # Backend should have deduced the helsinki timezone when creating the event,
2489 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2490 assert create_res.timezone == "Europe/Helsinki"
2491 assert to_aware_datetime(create_res.start_time).hour != 0
2492 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2494 # Now update its location such that it gets a new timezone
2495 update_res: events_pb2.Event = api.UpdateEvent(
2496 events_pb2.UpdateEventReq(
2497 event_id=create_res.event_id,
2498 # timezone_areas.sql-fake has a region for America/New_York
2499 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2500 )
2501 )
2503 # The user didn't touch the datetime components on the frontend,
2504 # so they expect the event to be at the same local time (midnight),
2505 # but now in the New York timezone.
2506 assert update_res.timezone == "America/New_York"
2507 assert update_res.start_time != create_res.start_time
2508 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2509 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2511 # Also validate GetEvent
2512 get_res: events_pb2.Event = api.GetEvent(
2513 events_pb2.GetEventReq(
2514 event_id=create_res.event_id,
2515 )
2516 )
2518 assert get_res.timezone == update_res.timezone
2519 assert get_res.start_time == update_res.start_time
2522def test_event_created_with_shadowed_visibility(db):
2523 """Events start in SHADOWED state when created."""
2524 user, token = generate_user()
2526 with session_scope() as session:
2527 create_community(session, 0, 2, "Community", [user], [], None)
2529 start_time = now() + timedelta(hours=2)
2530 end_time = start_time + timedelta(hours=3)
2532 with events_session(token) as api:
2533 res = api.CreateEvent(
2534 events_pb2.CreateEventReq(
2535 title="Test UMS Event",
2536 content="UMS content.",
2537 location=events_pb2.EventLocation(
2538 address="Near Null Island",
2539 lat=0.1,
2540 lng=0.2,
2541 ),
2542 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2543 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2544 )
2545 )
2546 event_id = res.event_id
2548 with session_scope() as session:
2549 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2550 mod_state = session.execute(
2551 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2552 ).scalar_one()
2553 assert mod_state.visibility == ModerationVisibility.shadowed
2556def test_shadowed_event_visible_to_creator_only(db):
2557 """SHADOWED events are visible to the creator but not to other users."""
2558 user1, token1 = generate_user()
2559 user2, token2 = generate_user()
2561 with session_scope() as session:
2562 create_community(session, 0, 2, "Community", [user1], [], None)
2564 start_time = now() + timedelta(hours=2)
2565 end_time = start_time + timedelta(hours=3)
2567 with events_session(token1) as api:
2568 res = api.CreateEvent(
2569 events_pb2.CreateEventReq(
2570 title="Shadowed Event",
2571 content="Content.",
2572 location=events_pb2.EventLocation(
2573 address="Near Null Island",
2574 lat=0.1,
2575 lng=0.2,
2576 ),
2577 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2578 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2579 )
2580 )
2581 event_id = res.event_id
2583 # Creator can see it
2584 with events_session(token1) as api:
2585 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2586 assert res.title == "Shadowed Event"
2588 # Other user cannot
2589 with events_session(token2) as api:
2590 with pytest.raises(grpc.RpcError) as e:
2591 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2592 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2595def test_event_visible_after_approval(db, moderator: Moderator):
2596 """Events become visible to all users after moderation approval."""
2597 user1, token1 = generate_user()
2598 user2, token2 = generate_user()
2600 with session_scope() as session:
2601 create_community(session, 0, 2, "Community", [user1], [], None)
2603 start_time = now() + timedelta(hours=2)
2604 end_time = start_time + timedelta(hours=3)
2606 with events_session(token1) as api:
2607 res = api.CreateEvent(
2608 events_pb2.CreateEventReq(
2609 title="Approved Event",
2610 content="Content.",
2611 location=events_pb2.EventLocation(
2612 address="Near Null Island",
2613 lat=0.1,
2614 lng=0.2,
2615 ),
2616 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2617 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2618 )
2619 )
2620 event_id = res.event_id
2622 # Other user cannot see it yet
2623 with events_session(token2) as api:
2624 with pytest.raises(grpc.RpcError) as e:
2625 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2626 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2628 # Approve the event
2629 moderator.approve_event_occurrence(event_id)
2631 # Now other user can see it
2632 with events_session(token2) as api:
2633 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2634 assert res.title == "Approved Event"
2637def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2638 """SHADOWED events appear in lists for the creator but not for other users."""
2639 user1, token1 = generate_user()
2640 user2, token2 = generate_user()
2642 with session_scope() as session:
2643 create_community(session, 0, 2, "Community", [user1], [], None)
2645 start_time = now() + timedelta(hours=2)
2646 end_time = start_time + timedelta(hours=3)
2648 with events_session(token1) as api:
2649 res = api.CreateEvent(
2650 events_pb2.CreateEventReq(
2651 title="List Test Event",
2652 content="Content.",
2653 location=events_pb2.EventLocation(
2654 address="Near Null Island",
2655 lat=0.1,
2656 lng=0.2,
2657 ),
2658 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2659 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2660 )
2661 )
2662 event_id = res.event_id
2664 # Creator can see their own SHADOWED event in lists
2665 with events_session(token1) as api:
2666 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2667 event_ids = [e.event_id for e in list_res.events]
2668 assert event_id in event_ids
2670 # Other user cannot see the SHADOWED event in lists
2671 with events_session(token2) as api:
2672 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2673 event_ids = [e.event_id for e in list_res.events]
2674 assert event_id not in event_ids
2676 # After approval, other user can see it
2677 moderator.approve_event_occurrence(event_id)
2679 with events_session(token2) as api:
2680 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2681 event_ids = [e.event_id for e in list_res.events]
2682 assert event_id in event_ids
2685def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2686 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2687 user1, token1 = generate_user()
2688 user2, token2 = generate_user()
2690 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2691 with session_scope() as session:
2692 world = create_community(session, 0, 10, "World", [user1], [], None)
2693 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2694 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2695 create_community(session, 0, 2, "Child", [user2], [], region)
2697 start_time = now() + timedelta(hours=2)
2698 end_time = start_time + timedelta(hours=3)
2700 with events_session(token1) as api:
2701 res = api.CreateEvent(
2702 events_pb2.CreateEventReq(
2703 title="Deferred Event",
2704 content="Content.",
2705 location=events_pb2.EventLocation(
2706 address="Near Null Island",
2707 lat=0.1,
2708 lng=0.2,
2709 ),
2710 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2711 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2712 )
2713 )
2714 event_id = res.event_id
2716 # Process all jobs — notification should be deferred (event is SHADOWED)
2717 process_jobs()
2719 with session_scope() as session:
2720 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2721 # Notification was created with moderation_state_id for deferral
2722 assert notif.moderation_state_id is not None
2723 # No delivery exists (deferred because event is SHADOWED)
2724 delivery_count = session.execute(
2725 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2726 ).scalar_one_or_none()
2727 assert delivery_count is None
2729 # Approve the event — handle_notification is re-queued for deferred notifications
2730 moderator.approve_event_occurrence(event_id)
2732 # Verify handle_notification job was queued
2733 with session_scope() as session:
2734 pending_jobs = (
2735 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2736 .scalars()
2737 .all()
2738 )
2739 assert any("handle_notification" in j.job_type for j in pending_jobs)
2742def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2743 """Event update notifications should carry the event's moderation_state_id for deferral."""
2744 user1, token1 = generate_user()
2745 user2, token2 = generate_user()
2747 with session_scope() as session:
2748 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2750 start_time = now() + timedelta(hours=2)
2751 end_time = start_time + timedelta(hours=3)
2753 with events_session(token1) as api:
2754 res = api.CreateEvent(
2755 events_pb2.CreateEventReq(
2756 title="Update Test",
2757 content="Content.",
2758 location=events_pb2.EventLocation(
2759 address="Near Null Island",
2760 lat=0.1,
2761 lng=0.2,
2762 ),
2763 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2764 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2765 )
2766 )
2767 event_id = res.event_id
2769 moderator.approve_event_occurrence(event_id)
2770 process_jobs()
2771 # Clear any create notifications
2772 while push_collector.count_for_user(user2.id): 2772 ↛ 2773line 2772 didn't jump to line 2773 because the condition on line 2772 was never true
2773 push_collector.pop_for_user(user2.id)
2775 # User2 subscribes to the event
2776 with events_session(token2) as api:
2777 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2779 # User1 updates the event with should_notify=True
2780 with events_session(token1) as api:
2781 api.UpdateEvent(
2782 events_pb2.UpdateEventReq(
2783 event_id=event_id,
2784 title=wrappers_pb2.StringValue(value="Updated Title"),
2785 should_notify=True,
2786 )
2787 )
2789 process_jobs()
2791 # Verify that the update notification for user2 has moderation_state_id set
2792 with session_scope() as session:
2793 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2795 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2796 # Find the update notification (most recent one)
2797 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2798 assert len(update_notifs) == 1
2799 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2802def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2803 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2804 user1, token1 = generate_user()
2805 user2, token2 = generate_user()
2807 with session_scope() as session:
2808 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2810 start_time = now() + timedelta(hours=2)
2811 end_time = start_time + timedelta(hours=3)
2813 with events_session(token1) as api:
2814 res = api.CreateEvent(
2815 events_pb2.CreateEventReq(
2816 title="Cancel Test",
2817 content="Content.",
2818 location=events_pb2.EventLocation(
2819 address="Near Null Island",
2820 lat=0.1,
2821 lng=0.2,
2822 ),
2823 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2824 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2825 )
2826 )
2827 event_id = res.event_id
2829 moderator.approve_event_occurrence(event_id)
2830 process_jobs()
2831 while push_collector.count_for_user(user2.id): 2831 ↛ 2832line 2831 didn't jump to line 2832 because the condition on line 2831 was never true
2832 push_collector.pop_for_user(user2.id)
2834 # User2 subscribes
2835 with events_session(token2) as api:
2836 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2838 # User1 cancels the event
2839 with events_session(token1) as api:
2840 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2842 process_jobs()
2844 # Verify that the cancel notification for user2 has moderation_state_id set
2845 with session_scope() as session:
2846 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2848 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2849 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2850 assert len(cancel_notifs) == 1
2851 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2854def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2855 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2856 user1, token1 = generate_user()
2857 user2, token2 = generate_user()
2859 with session_scope() as session:
2860 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2862 # Create event starting 23 hours from now (within 24h reminder window)
2863 start_time = now() + timedelta(hours=23)
2864 end_time = start_time + timedelta(hours=1)
2866 with events_session(token1) as api:
2867 res = api.CreateEvent(
2868 events_pb2.CreateEventReq(
2869 title="Reminder Test",
2870 content="Content.",
2871 location=events_pb2.EventLocation(
2872 address="Near Null Island",
2873 lat=0.1,
2874 lng=0.2,
2875 ),
2876 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2877 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2878 )
2879 )
2880 event_id = res.event_id
2882 moderator.approve_event_occurrence(event_id)
2883 process_jobs()
2884 while push_collector.count_for_user(user2.id): 2884 ↛ 2885line 2884 didn't jump to line 2885 because the condition on line 2884 was never true
2885 push_collector.pop_for_user(user2.id)
2887 # User2 marks attendance
2888 with events_session(token2) as api:
2889 api.SetEventAttendance(
2890 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2891 )
2893 # Run the event reminder handler
2894 send_event_reminders(empty_pb2.Empty())
2895 process_jobs()
2897 # Verify that the reminder notification for user2 has moderation_state_id set
2898 with session_scope() as session:
2899 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2901 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2902 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2903 assert len(reminder_notifs) == 1
2904 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2907def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2908 """Event reminders should not be sent for cancelled events."""
2909 user1, token1 = generate_user()
2910 user2, token2 = generate_user()
2912 with session_scope() as session:
2913 create_community(session, 0, 2, "Community", [user2], [], None)
2915 # Create event starting 23 hours from now (within 24h reminder window)
2916 start_time = now() + timedelta(hours=23)
2917 end_time = start_time + timedelta(hours=1)
2919 with events_session(token1) as api:
2920 res = api.CreateEvent(
2921 events_pb2.CreateEventReq(
2922 title="Cancelled Reminder Test",
2923 content="Content.",
2924 location=events_pb2.EventLocation(
2925 address="Near Null Island",
2926 lat=0.1,
2927 lng=0.2,
2928 ),
2929 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2930 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2931 )
2932 )
2933 event_id = res.event_id
2935 moderator.approve_event_occurrence(event_id)
2936 process_jobs()
2938 # User2 marks attendance
2939 with events_session(token2) as api:
2940 api.SetEventAttendance(
2941 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2942 )
2944 # User1 cancels the event
2945 with events_session(token1) as api:
2946 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2948 process_jobs()
2949 # Drain any cancellation-related notifications so we can cleanly assert on reminders
2950 while push_collector.count_for_user(user2.id):
2951 push_collector.pop_for_user(user2.id)
2953 # Run the event reminder handler
2954 send_event_reminders(empty_pb2.Empty())
2955 process_jobs()
2957 # Verify that no reminder notification was sent for user2
2958 with session_scope() as session:
2959 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2960 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
2961 assert len(reminder_notifs) == 0
2964@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
2965def test_event_reminder_not_sent_for_invisible_attendee(
2966 db, push_collector: PushCollector, moderator: Moderator, invisible_field
2967):
2968 user1, token1 = generate_user()
2969 user2, token2 = generate_user()
2971 with session_scope() as session:
2972 create_community(session, 0, 2, "Community", [user2], [], None)
2974 start_time = now() + timedelta(hours=23)
2975 end_time = start_time + timedelta(hours=1)
2977 with events_session(token1) as api:
2978 res = api.CreateEvent(
2979 events_pb2.CreateEventReq(
2980 title="Invisible Attendee Reminder Test",
2981 content="Content.",
2982 location=events_pb2.EventLocation(
2983 address="Near Null Island",
2984 lat=0.1,
2985 lng=0.2,
2986 ),
2987 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2988 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2989 )
2990 )
2991 event_id = res.event_id
2993 moderator.approve_event_occurrence(event_id)
2994 process_jobs()
2996 with events_session(token2) as api:
2997 api.SetEventAttendance(
2998 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2999 )
3001 with session_scope() as session:
3002 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3004 send_event_reminders(empty_pb2.Empty())
3005 process_jobs()
3007 with session_scope() as session:
3008 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3009 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3010 assert len(reminder_notifs) == 0
3013def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3014 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3015 user1, token1 = generate_user()
3016 user2, token2 = generate_user()
3018 with session_scope() as session:
3019 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3021 start = now()
3023 # User1 creates event A with 3 occurrences
3024 event_a_ids = []
3025 with events_session(token1) as api:
3026 res = api.CreateEvent(
3027 events_pb2.CreateEventReq(
3028 title="Event A",
3029 content="Content A.",
3030 parent_community_id=c_id,
3031 location=events_pb2.EventLocation(
3032 address="Near Null Island",
3033 lat=0.1,
3034 lng=0.2,
3035 ),
3036 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3037 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3038 )
3039 )
3040 event_a_ids.append(res.event_id)
3041 for i in range(2):
3042 res = api.ScheduleEvent(
3043 events_pb2.ScheduleEventReq(
3044 event_id=event_a_ids[-1],
3045 content=f"A occurrence {i}",
3046 location=events_pb2.EventLocation(
3047 address="Near Null Island",
3048 lat=0.1,
3049 lng=0.2,
3050 ),
3051 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3052 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3053 )
3054 )
3055 event_a_ids.append(res.event_id)
3057 # User2 creates event B with 2 occurrences
3058 event_b_ids = []
3059 with events_session(token2) as api:
3060 res = api.CreateEvent(
3061 events_pb2.CreateEventReq(
3062 title="Event B",
3063 content="Content B.",
3064 parent_community_id=c_id,
3065 location=events_pb2.EventLocation(
3066 address="Near Null Island",
3067 lat=0.1,
3068 lng=0.2,
3069 ),
3070 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3071 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3072 )
3073 )
3074 event_b_ids.append(res.event_id)
3075 res = api.ScheduleEvent(
3076 events_pb2.ScheduleEventReq(
3077 event_id=event_b_ids[-1],
3078 content="B occurrence 1",
3079 location=events_pb2.EventLocation(
3080 address="Near Null Island",
3081 lat=0.1,
3082 lng=0.2,
3083 ),
3084 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3085 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3086 )
3087 )
3088 event_b_ids.append(res.event_id)
3090 moderator.approve_event_occurrence(event_a_ids[0])
3091 moderator.approve_event_occurrence(event_b_ids[0])
3093 # List occurrences for event A — should only get event A's 3 occurrences
3094 with events_session(token1) as api:
3095 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3096 returned_ids = [e.event_id for e in res.events]
3097 assert sorted(returned_ids) == sorted(event_a_ids)
3099 # List occurrences for event B — should only get event B's 2 occurrences
3100 with events_session(token2) as api:
3101 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3102 returned_ids = [e.event_id for e in res.events]
3103 assert sorted(returned_ids) == sorted(event_b_ids)
3106def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3107 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3108 user1, token1 = generate_user()
3109 user2, token2 = generate_user()
3111 with session_scope() as session:
3112 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3114 start_time = now() + timedelta(hours=2)
3115 end_time = start_time + timedelta(hours=3)
3117 with events_session(token1) as api:
3118 res = api.CreateEvent(
3119 events_pb2.CreateEventReq(
3120 title="Comment Test",
3121 content="Content.",
3122 parent_community_id=c_id,
3123 location=events_pb2.EventLocation(
3124 address="Near Null Island",
3125 lat=0.1,
3126 lng=0.2,
3127 ),
3128 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3129 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3130 )
3131 )
3132 event_id = res.event_id
3133 thread_id = res.thread.thread_id
3135 moderator.approve_event_occurrence(event_id)
3136 process_jobs()
3137 while push_collector.count_for_user(user1.id): 3137 ↛ 3138line 3137 didn't jump to line 3138 because the condition on line 3137 was never true
3138 push_collector.pop_for_user(user1.id)
3140 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3141 with events_session(token1) as api:
3142 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3144 # User2 posts a top-level comment on the event thread
3145 with threads_session(token2) as api:
3146 comment_thread_id = api.PostReply(
3147 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3148 ).thread_id
3150 process_jobs()
3152 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3153 comment_db_id = comment_thread_id // 10
3154 with session_scope() as session:
3155 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3157 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3158 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3159 assert len(comment_notifs) == 1
3160 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3163def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3164 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3165 user1, token1 = generate_user()
3166 user2, token2 = generate_user()
3167 user3, token3 = generate_user()
3169 with session_scope() as session:
3170 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3172 start_time = now() + timedelta(hours=2)
3173 end_time = start_time + timedelta(hours=3)
3175 with events_session(token1) as api:
3176 res = api.CreateEvent(
3177 events_pb2.CreateEventReq(
3178 title="Reply Test",
3179 content="Content.",
3180 location=events_pb2.EventLocation(
3181 address="Near Null Island",
3182 lat=0.1,
3183 lng=0.2,
3184 ),
3185 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3186 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3187 )
3188 )
3189 event_id = res.event_id
3190 thread_id = res.thread.thread_id
3192 moderator.approve_event_occurrence(event_id)
3193 process_jobs()
3194 while push_collector.count_for_user(user1.id): 3194 ↛ 3195line 3194 didn't jump to line 3195 because the condition on line 3194 was never true
3195 push_collector.pop_for_user(user1.id)
3197 # User2 posts a top-level comment
3198 with threads_session(token2) as api:
3199 comment_thread_id = api.PostReply(
3200 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3201 ).thread_id
3203 process_jobs()
3204 while push_collector.count_for_user(user1.id): 3204 ↛ 3205line 3204 didn't jump to line 3205 because the condition on line 3204 was never true
3205 push_collector.pop_for_user(user1.id)
3207 # User3 replies to user2's comment (depth=2 reply)
3208 with threads_session(token3) as api:
3209 nested_reply_thread_id = api.PostReply(
3210 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3211 ).thread_id
3213 process_jobs()
3215 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3216 nested_reply_db_id = nested_reply_thread_id // 10
3217 with session_scope() as session:
3218 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3220 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3221 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3222 assert len(reply_notifs) == 1
3223 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id