Coverage for app/backend/src/tests/test_events.py: 99%
1475 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 11:50 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-26 11:50 +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 assert file_res.content_type == "text/calendar"
1984 ics_string = file_res.data.decode("utf-8")
1985 assert "SUMMARY:Dummy Title" in ics_string
1986 assert "DESCRIPTION:Dummy content." in ics_string
1987 assert "LOCATION:Near Null Island" in ics_string
1988 assert "STATUS:CANCELLED" not in ics_string
1989 pre_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1))
1991 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
1993 file_res = api.GetEventCalendarFile(events_pb2.GetEventCalendarFileReq(event_id=event_id))
1994 ics_string = file_res.data.decode("utf-8")
1995 assert "SUMMARY:Cancelled: Dummy Title" in ics_string
1996 assert "STATUS:CANCELLED" in ics_string
1997 post_cancel_sequence = int(re.search(r"SEQUENCE:(\d+)", ics_string).group(1))
1998 # Ideally the sequence number are strictly ascending, but they are based on timestamps so in tests they could be equal.
1999 assert post_cancel_sequence >= pre_cancel_sequence
2002def test_event_threads(db, push_collector: PushCollector, moderator: Moderator):
2003 user1, token1 = generate_user()
2004 user2, token2 = generate_user()
2005 user3, token3 = generate_user()
2006 user4, token4 = generate_user()
2008 with session_scope() as session:
2009 c = create_community(session, 0, 2, "Community", [user3], [], None)
2010 h = create_group(session, "Group", [user4], [], c)
2011 c_id = c.id
2012 h_id = h.id
2013 user4_id = user4.id
2015 with events_session(token1) as api:
2016 event = api.CreateEvent(
2017 events_pb2.CreateEventReq(
2018 title="Dummy Title",
2019 content="Dummy content.",
2020 location=events_pb2.EventLocation(
2021 address="Near Null Island",
2022 lat=0.1,
2023 lng=0.2,
2024 ),
2025 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=2)),
2026 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=5)),
2027 )
2028 )
2030 moderator.approve_event_occurrence(event.event_id)
2032 with threads_session(token2) as api:
2033 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id
2035 moderator.approve_thread_post(reply_id)
2037 with events_session(token3) as api:
2038 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id))
2039 assert res.thread.num_responses == 1
2041 with threads_session(token3) as api:
2042 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id))
2043 assert len(ret.replies) == 1
2044 assert not ret.next_page_token
2045 assert ret.replies[0].thread_id == reply_id
2046 assert ret.replies[0].content == "hi"
2047 assert ret.replies[0].author_user_id == user2.id
2048 assert ret.replies[0].num_replies == 0
2050 nested_reply_id = api.PostReply(
2051 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment")
2052 ).thread_id
2054 moderator.approve_thread_post(nested_reply_id)
2056 process_jobs()
2058 push = push_collector.pop_for_user(user1.id, last=True)
2059 assert push.topic_action == NotificationTopicAction.event__comment.display
2060 assert push.content.title == f"{user2.name} • Dummy Title"
2061 assert push.content.ios_title == user2.name
2062 assert push.content.ios_subtitle == "Commented on Dummy Title"
2063 assert push.content.body == "hi"
2065 push = push_collector.pop_for_user(user2.id, last=True)
2066 assert push.content.title == f"{user3.name} • Dummy Title"
2068 assert push_collector.count_for_user(user4_id) == 0
2071def test_can_overlap_other_events_schedule_regression(db):
2072 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event
2073 user, token = generate_user()
2075 with session_scope() as session:
2076 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2078 start = now()
2080 with events_session(token) as api:
2081 # create another event, should be able to overlap with this one
2082 api.CreateEvent(
2083 events_pb2.CreateEventReq(
2084 title="Dummy Title",
2085 content="Dummy content.",
2086 parent_community_id=c_id,
2087 location=events_pb2.EventLocation(
2088 address="Near Null Island",
2089 lat=0.1,
2090 lng=0.2,
2091 ),
2092 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2093 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=5)),
2094 )
2095 )
2097 # this event
2098 res = api.CreateEvent(
2099 events_pb2.CreateEventReq(
2100 title="Dummy Title",
2101 content="Dummy content.",
2102 parent_community_id=c_id,
2103 location=events_pb2.EventLocation(
2104 address="Near Null Island",
2105 lat=0.1,
2106 lng=0.2,
2107 ),
2108 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2109 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2)),
2110 )
2111 )
2113 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem
2114 api.ScheduleEvent(
2115 events_pb2.ScheduleEventReq(
2116 event_id=res.event_id,
2117 content="New event occurrence",
2118 location=events_pb2.EventLocation(
2119 address="A bit further but still near Null Island",
2120 lat=0.3,
2121 lng=0.2,
2122 ),
2123 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2124 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2125 )
2126 )
2129def test_can_overlap_other_events_update_regression(db):
2130 user, token = generate_user()
2132 with session_scope() as session:
2133 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2135 start = now()
2137 with events_session(token) as api:
2138 # create another event, should be able to overlap with this one
2139 api.CreateEvent(
2140 events_pb2.CreateEventReq(
2141 title="Dummy Title",
2142 content="Dummy content.",
2143 parent_community_id=c_id,
2144 location=events_pb2.EventLocation(
2145 address="Near Null Island",
2146 lat=0.1,
2147 lng=0.2,
2148 ),
2149 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
2150 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2151 )
2152 )
2154 res = api.CreateEvent(
2155 events_pb2.CreateEventReq(
2156 title="Dummy Title",
2157 content="Dummy content.",
2158 parent_community_id=c_id,
2159 location=events_pb2.EventLocation(
2160 address="Near Null Island",
2161 lat=0.1,
2162 lng=0.2,
2163 ),
2164 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=7)),
2165 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=8)),
2166 )
2167 )
2169 event_id = api.ScheduleEvent(
2170 events_pb2.ScheduleEventReq(
2171 event_id=res.event_id,
2172 content="New event occurrence",
2173 location=events_pb2.EventLocation(
2174 address="A bit further but still near Null Island",
2175 lat=0.3,
2176 lng=0.2,
2177 ),
2178 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2179 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2180 )
2181 ).event_id
2183 # can overlap with this current existing occurrence
2184 api.UpdateEvent(
2185 events_pb2.UpdateEventReq(
2186 event_id=event_id,
2187 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2188 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2189 ),
2190 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2191 value=datetime_to_iso8601_local(start + timedelta(hours=6))
2192 ),
2193 )
2194 )
2196 api.UpdateEvent(
2197 events_pb2.UpdateEventReq(
2198 event_id=event_id,
2199 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2200 value=datetime_to_iso8601_local(start + timedelta(hours=2))
2201 ),
2202 end_datetime_iso8601_local=wrappers_pb2.StringValue(
2203 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2204 ),
2205 )
2206 )
2209def test_list_past_events_regression(db):
2210 # test for a bug where listing past events didn't work if they didn't have a future occurrence
2211 user, token = generate_user()
2213 with session_scope() as session:
2214 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2216 start = now()
2218 with events_session(token) as api:
2219 api.CreateEvent(
2220 events_pb2.CreateEventReq(
2221 title="Dummy Title",
2222 content="Dummy content.",
2223 parent_community_id=c_id,
2224 location=events_pb2.EventLocation(
2225 address="Near Null Island",
2226 lat=0.1,
2227 lng=0.2,
2228 ),
2229 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2230 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=4)),
2231 )
2232 )
2234 with session_scope() as session:
2235 session.execute(
2236 update(EventOccurrence).values(
2237 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4))
2238 )
2239 )
2241 with events_session(token) as api:
2242 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True))
2243 assert len(res.events) == 1
2246def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator):
2247 user1, token1 = generate_user(complete_profile=True)
2248 user2, token2 = generate_user()
2249 user3, token3 = generate_user()
2250 user4, token4 = generate_user()
2251 user5, token5 = generate_user(is_superuser=True)
2253 with session_scope() as session:
2254 w = create_community(session, 0, 2, "World Community", [user5], [], None)
2255 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w)
2256 r = create_community(session, 0, 2, "Region", [user5], [], mr)
2257 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id
2259 enforce_community_memberships()
2261 with events_session(token1) as api:
2262 res = api.CreateEvent(
2263 events_pb2.CreateEventReq(
2264 title="Dummy Title",
2265 content="Dummy content.",
2266 parent_community_id=c_id,
2267 location=events_pb2.EventLocation(
2268 address="Near Null Island",
2269 lat=0.1,
2270 lng=0.2,
2271 ),
2272 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2273 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2274 )
2275 )
2276 user_url = f"http://localhost:3000/user/{user1.username}"
2277 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}"
2279 event_id = res.event_id
2281 moderator.approve_event_occurrence(event_id)
2283 with events_session(token1) as api:
2284 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2286 email = email_collector.pop_for_mods(last=True)
2288 assert user_url in email.plain
2289 assert event_url in email.plain
2291 # can't send another req
2292 with pytest.raises(grpc.RpcError) as err:
2293 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2294 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2295 assert err.value.details() == "You have already requested a community invite for this event."
2297 # another user can send one though
2298 with events_session(token3) as api:
2299 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2301 # but not a non-admin
2302 with events_session(token2) as api:
2303 with pytest.raises(grpc.RpcError) as err:
2304 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2305 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED
2306 assert err.value.details() == "You're not allowed to edit that event."
2308 with real_editor_session(token5) as editor:
2309 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2310 assert len(res.requests) == 2
2311 assert res.requests[0].user_id == user1.id
2312 # user1 is the event organizer, so they're excluded from the notify count (only user3 and user4 remain)
2313 assert res.requests[0].approx_users_to_notify == 2
2314 assert res.requests[1].user_id == user3.id
2315 assert res.requests[1].approx_users_to_notify == 2
2317 editor.DecideEventCommunityInviteRequest(
2318 editor_pb2.DecideEventCommunityInviteRequestReq(
2319 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2320 approve=False,
2321 )
2322 )
2324 editor.DecideEventCommunityInviteRequest(
2325 editor_pb2.DecideEventCommunityInviteRequestReq(
2326 event_community_invite_request_id=res.requests[1].event_community_invite_request_id,
2327 approve=True,
2328 )
2329 )
2331 # not after approve
2332 with events_session(token4) as api:
2333 with pytest.raises(grpc.RpcError) as err:
2334 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2335 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION
2336 assert err.value.details() == "A community invite has already been sent out for this event."
2339def test_community_invite_not_sent_to_attendees_or_organizers(db, moderator: Moderator):
2340 # Regression: users who already RSVP'd (or organize the event) must not get the
2341 # community invite notification when it is approved.
2342 organizer, organizer_token = generate_user()
2343 attendee, attendee_token = generate_user()
2344 member, _ = generate_user()
2345 superuser, superuser_token = generate_user(is_superuser=True)
2347 with session_scope() as session:
2348 w = create_community(session, 0, 2, "World Community", [superuser], [], None)
2349 mr = create_community(session, 0, 2, "Macroregion", [superuser], [], w)
2350 r = create_community(session, 0, 2, "Region", [superuser], [], mr)
2351 c_id = create_community(session, 0, 2, "Community", [organizer, attendee, member], [], r).id
2353 enforce_community_memberships()
2355 with events_session(organizer_token) as api:
2356 event_id = api.CreateEvent(
2357 events_pb2.CreateEventReq(
2358 title="Dummy Title",
2359 content="Dummy content.",
2360 parent_community_id=c_id,
2361 location=events_pb2.EventLocation(
2362 address="Near Null Island",
2363 lat=0.1,
2364 lng=0.2,
2365 ),
2366 start_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=3)),
2367 end_datetime_iso8601_local=datetime_to_iso8601_local(now() + timedelta(hours=4)),
2368 )
2369 ).event_id
2371 moderator.approve_event_occurrence(event_id)
2373 # the attendee RSVPs before the community invite is approved
2374 with events_session(attendee_token) as api:
2375 api.SetEventAttendance(
2376 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2377 )
2379 with events_session(organizer_token) as api:
2380 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id))
2382 with real_editor_session(superuser_token) as editor:
2383 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq())
2384 editor.DecideEventCommunityInviteRequest(
2385 editor_pb2.DecideEventCommunityInviteRequestReq(
2386 event_community_invite_request_id=res.requests[0].event_community_invite_request_id,
2387 approve=True,
2388 )
2389 )
2391 process_jobs()
2393 with session_scope() as session:
2395 def invite_notification_count(user_id: int) -> int:
2396 notifications = session.execute(select(Notification).where(Notification.user_id == user_id)).scalars().all()
2397 return len([n for n in notifications if n.topic_action == NotificationTopicAction.event__create_approved])
2399 # a plain community member gets the invite...
2400 assert invite_notification_count(member.id) == 1
2401 # ...but the attendee and the organizer don't
2402 assert invite_notification_count(attendee.id) == 0
2403 assert invite_notification_count(organizer.id) == 0
2406def test_update_event_should_notify_queues_job():
2407 user, token = generate_user()
2408 start = now()
2410 with session_scope() as session:
2411 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2413 # create an event
2414 with events_session(token) as api:
2415 create_res = api.CreateEvent(
2416 events_pb2.CreateEventReq(
2417 title="Dummy Title",
2418 content="Dummy content.",
2419 parent_community_id=c_id,
2420 location=events_pb2.EventLocation(
2421 address="Near Null Island",
2422 lat=1.0,
2423 lng=2.0,
2424 ),
2425 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=3)),
2426 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=6)),
2427 )
2428 )
2430 event_id = create_res.event_id
2432 # measure initial background job queue length
2433 with session_scope() as session:
2434 jobs = session.query(BackgroundJob).all()
2435 job_length_before_update = len(jobs)
2437 # update with should_notify=False, expect no change in background job queue
2438 api.UpdateEvent(
2439 events_pb2.UpdateEventReq(
2440 event_id=event_id,
2441 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2442 value=datetime_to_iso8601_local(start + timedelta(hours=4))
2443 ),
2444 should_notify=False,
2445 )
2446 )
2448 with session_scope() as session:
2449 jobs = session.query(BackgroundJob).all()
2450 assert len(jobs) == job_length_before_update
2452 # update with should_notify=True, expect one new background job added
2453 api.UpdateEvent(
2454 events_pb2.UpdateEventReq(
2455 event_id=event_id,
2456 start_datetime_iso8601_local=wrappers_pb2.StringValue(
2457 value=datetime_to_iso8601_local(start + timedelta(hours=5))
2458 ),
2459 should_notify=True,
2460 )
2461 )
2463 with session_scope() as session:
2464 jobs = session.query(BackgroundJob).all()
2465 assert len(jobs) == job_length_before_update + 1
2468def test_event_photo_key(db):
2469 """Test that events return the photo_key field when a photo is set."""
2470 user, token = generate_user()
2472 start_time = now() + timedelta(hours=2)
2473 end_time = start_time + timedelta(hours=3)
2475 # Create a community and an upload for the event photo
2476 with session_scope() as session:
2477 create_community(session, 0, 2, "Community", [user], [], None)
2478 upload = Upload(
2479 key="test_event_photo_key_123",
2480 filename="test_event_photo_key_123.jpg",
2481 creator_user_id=user.id,
2482 )
2483 session.add(upload)
2485 with events_session(token) as api:
2486 # Create event without photo
2487 res = api.CreateEvent(
2488 events_pb2.CreateEventReq(
2489 title="Event Without Photo",
2490 content="No photo content.",
2491 photo_key=None,
2492 location=events_pb2.EventLocation(
2493 address="Near Null Island",
2494 lat=0.1,
2495 lng=0.2,
2496 ),
2497 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2498 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2499 )
2500 )
2502 assert res.photo_key == ""
2503 assert res.photo_url == ""
2505 # Create event with photo
2506 res_with_photo = api.CreateEvent(
2507 events_pb2.CreateEventReq(
2508 title="Event With Photo",
2509 content="Has photo content.",
2510 photo_key="test_event_photo_key_123",
2511 location=events_pb2.EventLocation(
2512 address="Near Null Island",
2513 lat=0.1,
2514 lng=0.2,
2515 ),
2516 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time + timedelta(days=1)),
2517 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time + timedelta(days=1)),
2518 )
2519 )
2521 assert res_with_photo.photo_key == "test_event_photo_key_123"
2522 assert "test_event_photo_key_123" in res_with_photo.photo_url
2524 event_id = res_with_photo.event_id
2526 # Verify photo_key is returned when getting the event
2527 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2528 assert get_res.photo_key == "test_event_photo_key_123"
2529 assert "test_event_photo_key_123" in get_res.photo_url
2532def test_event_timezone(db):
2533 user, token = generate_user()
2535 with session_scope() as session:
2536 c_id = create_community(session, 0, 2, "Community", [user], [], None).id
2538 # Midnight future day, UTC timezone
2539 start_time = (now() + timedelta(days=2)).replace(hour=0, minute=0, second=0, microsecond=0)
2540 end_time = start_time + timedelta(days=1)
2542 with events_session(token) as api:
2543 create_res: events_pb2.Event = api.CreateEvent(
2544 events_pb2.CreateEventReq(
2545 title="Dummy Title",
2546 content="Dummy content.",
2547 photo_key=None,
2548 parent_community_id=c_id,
2549 # timezone_areas.sql-fake has a region for Europe/Helsinki
2550 location=events_pb2.EventLocation(address="Helsinki", lat=60.192059, lng=24.945831),
2551 # Should result in YYYY-MM-DDT00:00 (midnight local time)
2552 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2553 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2554 )
2555 )
2557 # Backend should have deduced the helsinki timezone when creating the event,
2558 # so the datetime in Helsinki should be at midnight, but it shouldn't in UTC.
2559 assert create_res.timezone == "Europe/Helsinki"
2560 assert to_aware_datetime(create_res.start_time).hour != 0
2561 assert create_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour == 0
2563 # Now update its location such that it gets a new timezone
2564 update_res: events_pb2.Event = api.UpdateEvent(
2565 events_pb2.UpdateEventReq(
2566 event_id=create_res.event_id,
2567 # timezone_areas.sql-fake has a region for America/New_York
2568 location=events_pb2.EventLocation(address="New York", lat=40.712776, lng=-74.005974),
2569 )
2570 )
2572 # The user didn't touch the datetime components on the frontend,
2573 # so they expect the event to be at the same local time (midnight),
2574 # but now in the New York timezone.
2575 assert update_res.timezone == "America/New_York"
2576 assert update_res.start_time != create_res.start_time
2577 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("Europe/Helsinki")).hour != 0
2578 assert update_res.start_time.ToDatetime(tzinfo=ZoneInfo("America/New_York")).hour == 0
2580 # Also validate GetEvent
2581 get_res: events_pb2.Event = api.GetEvent(
2582 events_pb2.GetEventReq(
2583 event_id=create_res.event_id,
2584 )
2585 )
2587 assert get_res.timezone == update_res.timezone
2588 assert get_res.start_time == update_res.start_time
2591def test_event_created_with_shadowed_visibility(db):
2592 """Events start in SHADOWED state when created."""
2593 user, token = generate_user()
2595 with session_scope() as session:
2596 create_community(session, 0, 2, "Community", [user], [], None)
2598 start_time = now() + timedelta(hours=2)
2599 end_time = start_time + timedelta(hours=3)
2601 with events_session(token) as api:
2602 res = api.CreateEvent(
2603 events_pb2.CreateEventReq(
2604 title="Test UMS Event",
2605 content="UMS content.",
2606 location=events_pb2.EventLocation(
2607 address="Near Null Island",
2608 lat=0.1,
2609 lng=0.2,
2610 ),
2611 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2612 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2613 )
2614 )
2615 event_id = res.event_id
2617 with session_scope() as session:
2618 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2619 mod_state = session.execute(
2620 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id)
2621 ).scalar_one()
2622 assert mod_state.visibility == ModerationVisibility.shadowed
2625def test_shadowed_event_visible_to_creator_only(db):
2626 """SHADOWED events are visible to the creator but not to other users."""
2627 user1, token1 = generate_user()
2628 user2, token2 = generate_user()
2630 with session_scope() as session:
2631 create_community(session, 0, 2, "Community", [user1], [], None)
2633 start_time = now() + timedelta(hours=2)
2634 end_time = start_time + timedelta(hours=3)
2636 with events_session(token1) as api:
2637 res = api.CreateEvent(
2638 events_pb2.CreateEventReq(
2639 title="Shadowed Event",
2640 content="Content.",
2641 location=events_pb2.EventLocation(
2642 address="Near Null Island",
2643 lat=0.1,
2644 lng=0.2,
2645 ),
2646 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2647 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2648 )
2649 )
2650 event_id = res.event_id
2652 # Creator can see it
2653 with events_session(token1) as api:
2654 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2655 assert res.title == "Shadowed Event"
2657 # Other user cannot
2658 with events_session(token2) as api:
2659 with pytest.raises(grpc.RpcError) as e:
2660 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2661 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2664def test_event_visible_after_approval(db, moderator: Moderator):
2665 """Events become visible to all users after moderation approval."""
2666 user1, token1 = generate_user()
2667 user2, token2 = generate_user()
2669 with session_scope() as session:
2670 create_community(session, 0, 2, "Community", [user1], [], None)
2672 start_time = now() + timedelta(hours=2)
2673 end_time = start_time + timedelta(hours=3)
2675 with events_session(token1) as api:
2676 res = api.CreateEvent(
2677 events_pb2.CreateEventReq(
2678 title="Approved Event",
2679 content="Content.",
2680 location=events_pb2.EventLocation(
2681 address="Near Null Island",
2682 lat=0.1,
2683 lng=0.2,
2684 ),
2685 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2686 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2687 )
2688 )
2689 event_id = res.event_id
2691 # Other user cannot see it yet
2692 with events_session(token2) as api:
2693 with pytest.raises(grpc.RpcError) as e:
2694 api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2695 assert e.value.code() == grpc.StatusCode.NOT_FOUND
2697 # Approve the event
2698 moderator.approve_event_occurrence(event_id)
2700 # Now other user can see it
2701 with events_session(token2) as api:
2702 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id))
2703 assert res.title == "Approved Event"
2706def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator):
2707 """SHADOWED events appear in lists for the creator but not for other users."""
2708 user1, token1 = generate_user()
2709 user2, token2 = generate_user()
2711 with session_scope() as session:
2712 create_community(session, 0, 2, "Community", [user1], [], None)
2714 start_time = now() + timedelta(hours=2)
2715 end_time = start_time + timedelta(hours=3)
2717 with events_session(token1) as api:
2718 res = api.CreateEvent(
2719 events_pb2.CreateEventReq(
2720 title="List Test Event",
2721 content="Content.",
2722 location=events_pb2.EventLocation(
2723 address="Near Null Island",
2724 lat=0.1,
2725 lng=0.2,
2726 ),
2727 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2728 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2729 )
2730 )
2731 event_id = res.event_id
2733 # Creator can see their own SHADOWED event in lists
2734 with events_session(token1) as api:
2735 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2736 event_ids = [e.event_id for e in list_res.events]
2737 assert event_id in event_ids
2739 # Other user cannot see the SHADOWED event in lists
2740 with events_session(token2) as api:
2741 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2742 event_ids = [e.event_id for e in list_res.events]
2743 assert event_id not in event_ids
2745 # After approval, other user can see it
2746 moderator.approve_event_occurrence(event_id)
2748 with events_session(token2) as api:
2749 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq())
2750 event_ids = [e.event_id for e in list_res.events]
2751 assert event_id in event_ids
2754def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator):
2755 """Event create notifications are deferred while SHADOWED, then unblocked after approval."""
2756 user1, token1 = generate_user()
2757 user2, token2 = generate_user()
2759 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications
2760 with session_scope() as session:
2761 world = create_community(session, 0, 10, "World", [user1], [], None)
2762 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world)
2763 region = create_community(session, 0, 5, "Region", [user1], [], macroregion)
2764 create_community(session, 0, 2, "Child", [user2], [], region)
2766 start_time = now() + timedelta(hours=2)
2767 end_time = start_time + timedelta(hours=3)
2769 with events_session(token1) as api:
2770 res = api.CreateEvent(
2771 events_pb2.CreateEventReq(
2772 title="Deferred Event",
2773 content="Content.",
2774 location=events_pb2.EventLocation(
2775 address="Near Null Island",
2776 lat=0.1,
2777 lng=0.2,
2778 ),
2779 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2780 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2781 )
2782 )
2783 event_id = res.event_id
2785 # Process all jobs — notification should be deferred (event is SHADOWED)
2786 process_jobs()
2788 with session_scope() as session:
2789 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one()
2790 # Notification was created with moderation_state_id for deferral
2791 assert notif.moderation_state_id is not None
2792 # No delivery exists (deferred because event is SHADOWED)
2793 delivery_count = session.execute(
2794 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id)
2795 ).scalar_one_or_none()
2796 assert delivery_count is None
2798 # Approve the event — handle_notification is re-queued for deferred notifications
2799 moderator.approve_event_occurrence(event_id)
2801 # Verify handle_notification job was queued
2802 with session_scope() as session:
2803 pending_jobs = (
2804 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending))
2805 .scalars()
2806 .all()
2807 )
2808 assert any("handle_notification" in j.job_type for j in pending_jobs)
2811def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2812 """Event update notifications should carry the event's moderation_state_id for deferral."""
2813 user1, token1 = generate_user()
2814 user2, token2 = generate_user()
2816 with session_scope() as session:
2817 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2819 start_time = now() + timedelta(hours=2)
2820 end_time = start_time + timedelta(hours=3)
2822 with events_session(token1) as api:
2823 res = api.CreateEvent(
2824 events_pb2.CreateEventReq(
2825 title="Update Test",
2826 content="Content.",
2827 location=events_pb2.EventLocation(
2828 address="Near Null Island",
2829 lat=0.1,
2830 lng=0.2,
2831 ),
2832 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2833 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2834 )
2835 )
2836 event_id = res.event_id
2838 moderator.approve_event_occurrence(event_id)
2839 process_jobs()
2840 # Clear any create notifications
2841 while push_collector.count_for_user(user2.id): 2841 ↛ 2842line 2841 didn't jump to line 2842 because the condition on line 2841 was never true
2842 push_collector.pop_for_user(user2.id)
2844 # User2 subscribes to the event
2845 with events_session(token2) as api:
2846 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2848 # User1 updates the event with should_notify=True
2849 with events_session(token1) as api:
2850 api.UpdateEvent(
2851 events_pb2.UpdateEventReq(
2852 event_id=event_id,
2853 title=wrappers_pb2.StringValue(value="Updated Title"),
2854 should_notify=True,
2855 )
2856 )
2858 process_jobs()
2860 # Verify that the update notification for user2 has moderation_state_id set
2861 with session_scope() as session:
2862 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2864 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2865 # Find the update notification (most recent one)
2866 update_notifs = [n for n in notifications if n.topic_action.action == "update"]
2867 assert len(update_notifs) == 1
2868 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id
2871def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2872 """Event cancel notifications should carry the event's moderation_state_id for deferral."""
2873 user1, token1 = generate_user()
2874 user2, token2 = generate_user()
2876 with session_scope() as session:
2877 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2879 start_time = now() + timedelta(hours=2)
2880 end_time = start_time + timedelta(hours=3)
2882 with events_session(token1) as api:
2883 res = api.CreateEvent(
2884 events_pb2.CreateEventReq(
2885 title="Cancel Test",
2886 content="Content.",
2887 location=events_pb2.EventLocation(
2888 address="Near Null Island",
2889 lat=0.1,
2890 lng=0.2,
2891 ),
2892 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2893 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2894 )
2895 )
2896 event_id = res.event_id
2898 moderator.approve_event_occurrence(event_id)
2899 process_jobs()
2900 while push_collector.count_for_user(user2.id): 2900 ↛ 2901line 2900 didn't jump to line 2901 because the condition on line 2900 was never true
2901 push_collector.pop_for_user(user2.id)
2903 # User2 subscribes
2904 with events_session(token2) as api:
2905 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
2907 # User1 cancels the event
2908 with events_session(token1) as api:
2909 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
2911 process_jobs()
2913 # Verify that the cancel notification for user2 has moderation_state_id set
2914 with session_scope() as session:
2915 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2917 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2918 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"]
2919 assert len(cancel_notifs) == 1
2920 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id
2923def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
2924 """Event reminder notifications should carry the event's moderation_state_id for deferral."""
2925 user1, token1 = generate_user()
2926 user2, token2 = generate_user()
2928 with session_scope() as session:
2929 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
2931 # Create event starting 23 hours from now (within 24h reminder window)
2932 start_time = now() + timedelta(hours=23)
2933 end_time = start_time + timedelta(hours=1)
2935 with events_session(token1) as api:
2936 res = api.CreateEvent(
2937 events_pb2.CreateEventReq(
2938 title="Reminder Test",
2939 content="Content.",
2940 location=events_pb2.EventLocation(
2941 address="Near Null Island",
2942 lat=0.1,
2943 lng=0.2,
2944 ),
2945 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2946 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
2947 )
2948 )
2949 event_id = res.event_id
2951 moderator.approve_event_occurrence(event_id)
2952 process_jobs()
2953 while push_collector.count_for_user(user2.id): 2953 ↛ 2954line 2953 didn't jump to line 2954 because the condition on line 2953 was never true
2954 push_collector.pop_for_user(user2.id)
2956 # User2 marks attendance
2957 with events_session(token2) as api:
2958 api.SetEventAttendance(
2959 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
2960 )
2962 # Run the event reminder handler
2963 send_event_reminders(empty_pb2.Empty())
2964 process_jobs()
2966 # Verify that the reminder notification for user2 has moderation_state_id set
2967 with session_scope() as session:
2968 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one()
2970 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
2971 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"]
2972 assert len(reminder_notifs) == 1
2973 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id
2976def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator):
2977 """Event reminders should not be sent for cancelled events."""
2978 user1, token1 = generate_user()
2979 user2, token2 = generate_user()
2981 with session_scope() as session:
2982 create_community(session, 0, 2, "Community", [user2], [], None)
2984 # Create event starting 23 hours from now (within 24h reminder window)
2985 start_time = now() + timedelta(hours=23)
2986 end_time = start_time + timedelta(hours=1)
2988 with events_session(token1) as api:
2989 res = api.CreateEvent(
2990 events_pb2.CreateEventReq(
2991 title="Cancelled Reminder Test",
2992 content="Content.",
2993 location=events_pb2.EventLocation(
2994 address="Near Null Island",
2995 lat=0.1,
2996 lng=0.2,
2997 ),
2998 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
2999 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3000 )
3001 )
3002 event_id = res.event_id
3004 moderator.approve_event_occurrence(event_id)
3005 process_jobs()
3007 # User2 marks attendance
3008 with events_session(token2) as api:
3009 api.SetEventAttendance(
3010 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3011 )
3013 # User1 cancels the event
3014 with events_session(token1) as api:
3015 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id))
3017 process_jobs()
3018 # Drain any cancellation-related notifications so we can cleanly assert on reminders
3019 while push_collector.count_for_user(user2.id):
3020 push_collector.pop_for_user(user2.id)
3022 # Run the event reminder handler
3023 send_event_reminders(empty_pb2.Empty())
3024 process_jobs()
3026 # Verify that no reminder notification was sent for user2
3027 with session_scope() as session:
3028 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3029 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3030 assert len(reminder_notifs) == 0
3033@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"])
3034def test_event_reminder_not_sent_for_invisible_attendee(
3035 db, push_collector: PushCollector, moderator: Moderator, invisible_field
3036):
3037 user1, token1 = generate_user()
3038 user2, token2 = generate_user()
3040 with session_scope() as session:
3041 create_community(session, 0, 2, "Community", [user2], [], None)
3043 start_time = now() + timedelta(hours=23)
3044 end_time = start_time + timedelta(hours=1)
3046 with events_session(token1) as api:
3047 res = api.CreateEvent(
3048 events_pb2.CreateEventReq(
3049 title="Invisible Attendee Reminder Test",
3050 content="Content.",
3051 location=events_pb2.EventLocation(
3052 address="Near Null Island",
3053 lat=0.1,
3054 lng=0.2,
3055 ),
3056 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3057 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3058 )
3059 )
3060 event_id = res.event_id
3062 moderator.approve_event_occurrence(event_id)
3063 process_jobs()
3065 with events_session(token2) as api:
3066 api.SetEventAttendance(
3067 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING)
3068 )
3070 with session_scope() as session:
3071 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()}))
3073 send_event_reminders(empty_pb2.Empty())
3074 process_jobs()
3076 with session_scope() as session:
3077 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3078 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder]
3079 assert len(reminder_notifs) == 0
3082def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator):
3083 """ListEventOccurrences should only return occurrences for the requested event, not other events."""
3084 user1, token1 = generate_user()
3085 user2, token2 = generate_user()
3087 with session_scope() as session:
3088 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id
3090 start = now()
3092 # User1 creates event A with 3 occurrences
3093 event_a_ids = []
3094 with events_session(token1) as api:
3095 res = api.CreateEvent(
3096 events_pb2.CreateEventReq(
3097 title="Event A",
3098 content="Content A.",
3099 parent_community_id=c_id,
3100 location=events_pb2.EventLocation(
3101 address="Near Null Island",
3102 lat=0.1,
3103 lng=0.2,
3104 ),
3105 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1)),
3106 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=1.5)),
3107 )
3108 )
3109 event_a_ids.append(res.event_id)
3110 for i in range(2):
3111 res = api.ScheduleEvent(
3112 events_pb2.ScheduleEventReq(
3113 event_id=event_a_ids[-1],
3114 content=f"A occurrence {i}",
3115 location=events_pb2.EventLocation(
3116 address="Near Null Island",
3117 lat=0.1,
3118 lng=0.2,
3119 ),
3120 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2 + i)),
3121 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=2.5 + i)),
3122 )
3123 )
3124 event_a_ids.append(res.event_id)
3126 # User2 creates event B with 2 occurrences
3127 event_b_ids = []
3128 with events_session(token2) as api:
3129 res = api.CreateEvent(
3130 events_pb2.CreateEventReq(
3131 title="Event B",
3132 content="Content B.",
3133 parent_community_id=c_id,
3134 location=events_pb2.EventLocation(
3135 address="Near Null Island",
3136 lat=0.1,
3137 lng=0.2,
3138 ),
3139 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10)),
3140 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=10.5)),
3141 )
3142 )
3143 event_b_ids.append(res.event_id)
3144 res = api.ScheduleEvent(
3145 events_pb2.ScheduleEventReq(
3146 event_id=event_b_ids[-1],
3147 content="B occurrence 1",
3148 location=events_pb2.EventLocation(
3149 address="Near Null Island",
3150 lat=0.1,
3151 lng=0.2,
3152 ),
3153 start_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11)),
3154 end_datetime_iso8601_local=datetime_to_iso8601_local(start + timedelta(hours=11.5)),
3155 )
3156 )
3157 event_b_ids.append(res.event_id)
3159 moderator.approve_event_occurrence(event_a_ids[0])
3160 moderator.approve_event_occurrence(event_b_ids[0])
3162 # List occurrences for event A — should only get event A's 3 occurrences
3163 with events_session(token1) as api:
3164 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1]))
3165 returned_ids = [e.event_id for e in res.events]
3166 assert sorted(returned_ids) == sorted(event_a_ids)
3168 # List occurrences for event B — should only get event B's 2 occurrences
3169 with events_session(token2) as api:
3170 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1]))
3171 returned_ids = [e.event_id for e in res.events]
3172 assert sorted(returned_ids) == sorted(event_b_ids)
3175def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3176 """Event comment notifications should carry the comment's moderation_state_id for deferral."""
3177 user1, token1 = generate_user()
3178 user2, token2 = generate_user()
3180 with session_scope() as session:
3181 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id
3183 start_time = now() + timedelta(hours=2)
3184 end_time = start_time + timedelta(hours=3)
3186 with events_session(token1) as api:
3187 res = api.CreateEvent(
3188 events_pb2.CreateEventReq(
3189 title="Comment Test",
3190 content="Content.",
3191 parent_community_id=c_id,
3192 location=events_pb2.EventLocation(
3193 address="Near Null Island",
3194 lat=0.1,
3195 lng=0.2,
3196 ),
3197 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3198 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3199 )
3200 )
3201 event_id = res.event_id
3202 thread_id = res.thread.thread_id
3204 moderator.approve_event_occurrence(event_id)
3205 process_jobs()
3206 while push_collector.count_for_user(user1.id): 3206 ↛ 3207line 3206 didn't jump to line 3207 because the condition on line 3206 was never true
3207 push_collector.pop_for_user(user1.id)
3209 # User1 subscribes (creator is auto-subscribed, but let's be explicit)
3210 with events_session(token1) as api:
3211 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True))
3213 # User2 posts a top-level comment on the event thread
3214 with threads_session(token2) as api:
3215 comment_thread_id = api.PostReply(
3216 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!")
3217 ).thread_id
3219 process_jobs()
3221 # The comment notification for user1 should be gated on the comment's own moderation_state_id
3222 comment_db_id = comment_thread_id // 10
3223 with session_scope() as session:
3224 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one()
3226 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all()
3227 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"]
3228 assert len(comment_notifs) == 1
3229 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id
3232def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator):
3233 """Event thread reply notifications should carry the reply's moderation_state_id for deferral."""
3234 user1, token1 = generate_user()
3235 user2, token2 = generate_user()
3236 user3, token3 = generate_user()
3238 with session_scope() as session:
3239 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id
3241 start_time = now() + timedelta(hours=2)
3242 end_time = start_time + timedelta(hours=3)
3244 with events_session(token1) as api:
3245 res = api.CreateEvent(
3246 events_pb2.CreateEventReq(
3247 title="Reply Test",
3248 content="Content.",
3249 location=events_pb2.EventLocation(
3250 address="Near Null Island",
3251 lat=0.1,
3252 lng=0.2,
3253 ),
3254 start_datetime_iso8601_local=datetime_to_iso8601_local(start_time),
3255 end_datetime_iso8601_local=datetime_to_iso8601_local(end_time),
3256 )
3257 )
3258 event_id = res.event_id
3259 thread_id = res.thread.thread_id
3261 moderator.approve_event_occurrence(event_id)
3262 process_jobs()
3263 while push_collector.count_for_user(user1.id): 3263 ↛ 3264line 3263 didn't jump to line 3264 because the condition on line 3263 was never true
3264 push_collector.pop_for_user(user1.id)
3266 # User2 posts a top-level comment
3267 with threads_session(token2) as api:
3268 comment_thread_id = api.PostReply(
3269 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment")
3270 ).thread_id
3272 process_jobs()
3273 while push_collector.count_for_user(user1.id): 3273 ↛ 3274line 3273 didn't jump to line 3274 because the condition on line 3273 was never true
3274 push_collector.pop_for_user(user1.id)
3276 # User3 replies to user2's comment (depth=2 reply)
3277 with threads_session(token3) as api:
3278 nested_reply_thread_id = api.PostReply(
3279 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply")
3280 ).thread_id
3282 process_jobs()
3284 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id
3285 nested_reply_db_id = nested_reply_thread_id // 10
3286 with session_scope() as session:
3287 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one()
3289 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all()
3290 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"]
3291 assert len(reply_notifs) == 1
3292 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id