Coverage for app/backend/src/tests/test_events.py: 99%

1402 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-19 00:32 +0000

1from datetime import timedelta 

2 

3import grpc 

4import pytest 

5from google.protobuf import empty_pb2, wrappers_pb2 

6from psycopg.types.range import TimestamptzRange 

7from sqlalchemy import select 

8from sqlalchemy.sql.expression import update 

9 

10from couchers.db import session_scope 

11from couchers.jobs.handlers import send_event_reminders 

12from couchers.models import ( 

13 BackgroundJob, 

14 BackgroundJobState, 

15 Comment, 

16 EventOccurrence, 

17 ModerationState, 

18 ModerationVisibility, 

19 Notification, 

20 NotificationDelivery, 

21 NotificationTopicAction, 

22 Reply, 

23 Upload, 

24 User, 

25) 

26from couchers.proto import editor_pb2, events_pb2, threads_pb2 

27from couchers.tasks import enforce_community_memberships 

28from couchers.utils import Timestamp_from_datetime, now, to_aware_datetime 

29from tests.fixtures.db import generate_user 

30from tests.fixtures.misc import EmailCollector, Moderator, PushCollector, process_jobs 

31from tests.fixtures.sessions import events_session, real_editor_session, threads_session 

32from tests.test_communities import create_community, create_group 

33 

34 

35@pytest.fixture(autouse=True) 

36def _(testconfig): 

37 pass 

38 

39 

40def test_CreateEvent(db, push_collector: PushCollector, moderator: Moderator): 

41 # test cases: 

42 # can create event 

43 # cannot create event with missing details 

44 # can't create event that starts in the past 

45 # can create in different timezones 

46 

47 # event creator 

48 user1, token1 = generate_user() 

49 # community moderator 

50 user2, token2 = generate_user() 

51 # third party 

52 user3, token3 = generate_user() 

53 

54 with session_scope() as session: 

55 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

56 

57 time_before = now() 

58 start_time = now() + timedelta(hours=2) 

59 end_time = start_time + timedelta(hours=3) 

60 

61 # Can create an event 

62 with events_session(token1) as api: 

63 res = api.CreateEvent( 

64 events_pb2.CreateEventReq( 

65 title="Dummy Title", 

66 content="Dummy content.", 

67 photo_key=None, 

68 location=events_pb2.EventLocation( 

69 address="Near Null Island", 

70 lat=0.1, 

71 lng=0.2, 

72 ), 

73 start_time=Timestamp_from_datetime(start_time), 

74 end_time=Timestamp_from_datetime(end_time), 

75 timezone="UTC", 

76 ) 

77 ) 

78 

79 assert res.is_next 

80 assert res.title == "Dummy Title" 

81 assert res.slug == "dummy-title" 

82 assert res.content == "Dummy content." 

83 assert not res.photo_url 

84 assert res.HasField("location") 

85 assert res.location.lat == 0.1 

86 assert res.location.lng == 0.2 

87 assert res.location.address == "Near Null Island" 

88 assert time_before <= to_aware_datetime(res.created) <= now() 

89 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

90 assert res.creator_user_id == user1.id 

91 assert to_aware_datetime(res.start_time) == start_time 

92 assert to_aware_datetime(res.end_time) == end_time 

93 # assert res.timezone == "UTC" 

94 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

95 assert res.organizer 

96 assert res.subscriber 

97 assert res.going_count == 1 

98 assert res.organizer_count == 1 

99 assert res.subscriber_count == 1 

100 assert res.owner_user_id == user1.id 

101 assert not res.owner_community_id 

102 assert not res.owner_group_id 

103 assert res.thread.thread_id 

104 assert res.can_edit 

105 assert not res.can_moderate 

106 

107 event_id = res.event_id 

108 

109 # Approve the event so other users can see it 

110 moderator.approve_event_occurrence(event_id) 

111 

112 with events_session(token2) as api: 

113 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

114 

115 assert res.is_next 

116 assert res.title == "Dummy Title" 

117 assert res.slug == "dummy-title" 

118 assert res.content == "Dummy content." 

119 assert not res.photo_url 

120 assert res.HasField("location") 

121 assert res.location.lat == 0.1 

122 assert res.location.lng == 0.2 

123 assert res.location.address == "Near Null Island" 

124 assert time_before <= to_aware_datetime(res.created) <= now() 

125 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

126 assert res.creator_user_id == user1.id 

127 assert to_aware_datetime(res.start_time) == start_time 

128 assert to_aware_datetime(res.end_time) == end_time 

129 # assert res.timezone == "UTC" 

130 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

131 assert not res.organizer 

132 assert not res.subscriber 

133 assert res.going_count == 1 

134 assert res.organizer_count == 1 

135 assert res.subscriber_count == 1 

136 assert res.owner_user_id == user1.id 

137 assert not res.owner_community_id 

138 assert not res.owner_group_id 

139 assert res.thread.thread_id 

140 assert res.can_edit 

141 assert res.can_moderate 

142 

143 with events_session(token3) as api: 

144 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

145 

146 assert res.is_next 

147 assert res.title == "Dummy Title" 

148 assert res.slug == "dummy-title" 

149 assert res.content == "Dummy content." 

150 assert not res.photo_url 

151 assert res.HasField("location") 

152 assert res.location.lat == 0.1 

153 assert res.location.lng == 0.2 

154 assert res.location.address == "Near Null Island" 

155 assert time_before <= to_aware_datetime(res.created) <= now() 

156 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

157 assert res.creator_user_id == user1.id 

158 assert to_aware_datetime(res.start_time) == start_time 

159 assert to_aware_datetime(res.end_time) == end_time 

160 # assert res.timezone == "UTC" 

161 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

162 assert not res.organizer 

163 assert not res.subscriber 

164 assert res.going_count == 1 

165 assert res.organizer_count == 1 

166 assert res.subscriber_count == 1 

167 assert res.owner_user_id == user1.id 

168 assert not res.owner_community_id 

169 assert not res.owner_group_id 

170 assert res.thread.thread_id 

171 assert not res.can_edit 

172 assert not res.can_moderate 

173 

174 # Failure cases 

175 with events_session(token1) as api: 

176 with pytest.raises(grpc.RpcError) as e: 

177 api.CreateEvent( 

178 events_pb2.CreateEventReq( 

179 # title="Dummy Title", 

180 content="Dummy content.", 

181 photo_key=None, 

182 location=events_pb2.EventLocation( 

183 address="Near Null Island", 

184 lat=0.1, 

185 lng=0.2, 

186 ), 

187 start_time=Timestamp_from_datetime(start_time), 

188 end_time=Timestamp_from_datetime(end_time), 

189 timezone="UTC", 

190 ) 

191 ) 

192 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

193 assert e.value.details() == "Missing event title." 

194 

195 with pytest.raises(grpc.RpcError) as e: 

196 api.CreateEvent( 

197 events_pb2.CreateEventReq( 

198 title="Dummy Title", 

199 # content="Dummy content.", 

200 photo_key=None, 

201 location=events_pb2.EventLocation( 

202 address="Near Null Island", 

203 lat=0.1, 

204 lng=0.2, 

205 ), 

206 start_time=Timestamp_from_datetime(start_time), 

207 end_time=Timestamp_from_datetime(end_time), 

208 timezone="UTC", 

209 ) 

210 ) 

211 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

212 assert e.value.details() == "Missing event content." 

213 

214 with pytest.raises(grpc.RpcError) as e: 

215 api.CreateEvent( 

216 events_pb2.CreateEventReq( 

217 title="Dummy Title", 

218 content="Dummy content.", 

219 photo_key="nonexistent", 

220 location=events_pb2.EventLocation( 

221 address="Near Null Island", 

222 lat=0.1, 

223 lng=0.2, 

224 ), 

225 start_time=Timestamp_from_datetime(start_time), 

226 end_time=Timestamp_from_datetime(end_time), 

227 timezone="UTC", 

228 ) 

229 ) 

230 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

231 assert e.value.details() == "Photo not found." 

232 

233 with pytest.raises(grpc.RpcError) as e: 

234 api.CreateEvent( 

235 events_pb2.CreateEventReq( 

236 title="Dummy Title", 

237 content="Dummy content.", 

238 location=events_pb2.EventLocation( 

239 address="Near Null Island", 

240 ), 

241 start_time=Timestamp_from_datetime(start_time), 

242 end_time=Timestamp_from_datetime(end_time), 

243 timezone="UTC", 

244 ) 

245 ) 

246 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

247 assert e.value.details() == "Missing event address or location." 

248 

249 with pytest.raises(grpc.RpcError) as e: 

250 api.CreateEvent( 

251 events_pb2.CreateEventReq( 

252 title="Dummy Title", 

253 content="Dummy content.", 

254 location=events_pb2.EventLocation( 

255 lat=0.1, 

256 lng=0.1, 

257 ), 

258 start_time=Timestamp_from_datetime(start_time), 

259 end_time=Timestamp_from_datetime(end_time), 

260 timezone="UTC", 

261 ) 

262 ) 

263 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

264 assert e.value.details() == "Missing event address or location." 

265 

266 with pytest.raises(grpc.RpcError) as e: 

267 api.CreateEvent( 

268 events_pb2.CreateEventReq( 

269 title="Dummy Title", 

270 content="Dummy content.", 

271 location=events_pb2.EventLocation( 

272 address="Near Null Island", 

273 lat=0.1, 

274 lng=0.2, 

275 ), 

276 start_time=Timestamp_from_datetime(now() - timedelta(hours=2)), 

277 end_time=Timestamp_from_datetime(end_time), 

278 timezone="UTC", 

279 ) 

280 ) 

281 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

282 assert e.value.details() == "The event must be in the future." 

283 

284 with pytest.raises(grpc.RpcError) as e: 

285 api.CreateEvent( 

286 events_pb2.CreateEventReq( 

287 title="Dummy Title", 

288 content="Dummy content.", 

289 location=events_pb2.EventLocation( 

290 address="Near Null Island", 

291 lat=0.1, 

292 lng=0.2, 

293 ), 

294 start_time=Timestamp_from_datetime(end_time), 

295 end_time=Timestamp_from_datetime(start_time), 

296 timezone="UTC", 

297 ) 

298 ) 

299 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

300 assert e.value.details() == "The event must end after it starts." 

301 

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_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=2)), 

313 end_time=Timestamp_from_datetime(now() + timedelta(days=500, hours=5)), 

314 timezone="UTC", 

315 ) 

316 ) 

317 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

318 assert e.value.details() == "The event needs to start within the next year." 

319 

320 with pytest.raises(grpc.RpcError) as e: 

321 api.CreateEvent( 

322 events_pb2.CreateEventReq( 

323 title="Dummy Title", 

324 content="Dummy content.", 

325 location=events_pb2.EventLocation( 

326 address="Near Null Island", 

327 lat=0.1, 

328 lng=0.2, 

329 ), 

330 start_time=Timestamp_from_datetime(start_time), 

331 end_time=Timestamp_from_datetime(now() + timedelta(days=100)), 

332 timezone="UTC", 

333 ) 

334 ) 

335 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

336 assert e.value.details() == "Events cannot last longer than 7 days." 

337 

338 

339def test_CreateEvent_incomplete_profile(db): 

340 user1, token1 = generate_user(complete_profile=False) 

341 user2, token2 = generate_user() 

342 

343 with session_scope() as session: 

344 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

345 

346 start_time = now() + timedelta(hours=2) 

347 end_time = start_time + timedelta(hours=3) 

348 

349 with events_session(token1) as api: 

350 with pytest.raises(grpc.RpcError) as e: 

351 api.CreateEvent( 

352 events_pb2.CreateEventReq( 

353 title="Dummy Title", 

354 content="Dummy content.", 

355 photo_key=None, 

356 location=events_pb2.EventLocation( 

357 address="Near Null Island", 

358 lat=0.1, 

359 lng=0.2, 

360 ), 

361 start_time=Timestamp_from_datetime(start_time), 

362 end_time=Timestamp_from_datetime(end_time), 

363 timezone="UTC", 

364 ) 

365 ) 

366 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

367 assert e.value.details() == "You have to complete your profile before you can create an event." 

368 

369 

370def test_ScheduleEvent(db): 

371 # test cases: 

372 # can schedule a new event occurrence 

373 

374 user, token = generate_user() 

375 

376 with session_scope() as session: 

377 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

378 

379 time_before = now() 

380 start_time = now() + timedelta(hours=2) 

381 end_time = start_time + timedelta(hours=3) 

382 

383 with events_session(token) as api: 

384 res = api.CreateEvent( 

385 events_pb2.CreateEventReq( 

386 title="Dummy Title", 

387 content="Dummy content.", 

388 parent_community_id=c_id, 

389 location=events_pb2.EventLocation( 

390 address="Near Null Island", 

391 lat=0.1, 

392 lng=0.2, 

393 ), 

394 start_time=Timestamp_from_datetime(start_time), 

395 end_time=Timestamp_from_datetime(end_time), 

396 timezone="UTC", 

397 ) 

398 ) 

399 

400 new_start_time = now() + timedelta(hours=6) 

401 new_end_time = new_start_time + timedelta(hours=2) 

402 

403 res = api.ScheduleEvent( 

404 events_pb2.ScheduleEventReq( 

405 event_id=res.event_id, 

406 content="New event occurrence", 

407 location=events_pb2.EventLocation( 

408 address="A bit further but still near Null Island", 

409 lat=0.3, 

410 lng=0.2, 

411 ), 

412 start_time=Timestamp_from_datetime(new_start_time), 

413 end_time=Timestamp_from_datetime(new_end_time), 

414 timezone="UTC", 

415 ) 

416 ) 

417 

418 res = api.GetEvent(events_pb2.GetEventReq(event_id=res.event_id)) 

419 

420 assert not res.is_next 

421 assert res.title == "Dummy Title" 

422 assert res.slug == "dummy-title" 

423 assert res.content == "New event occurrence" 

424 assert not res.photo_url 

425 assert res.HasField("location") 

426 assert res.location.lat == 0.3 

427 assert res.location.lng == 0.2 

428 assert res.location.address == "A bit further but still near Null Island" 

429 assert time_before <= to_aware_datetime(res.created) <= now() 

430 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

431 assert res.creator_user_id == user.id 

432 assert to_aware_datetime(res.start_time) == new_start_time 

433 assert to_aware_datetime(res.end_time) == new_end_time 

434 # assert res.timezone == "UTC" 

435 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

436 assert res.organizer 

437 assert res.subscriber 

438 assert res.going_count == 1 

439 assert res.organizer_count == 1 

440 assert res.subscriber_count == 1 

441 assert res.owner_user_id == user.id 

442 assert not res.owner_community_id 

443 assert not res.owner_group_id 

444 assert res.thread.thread_id 

445 assert res.can_edit 

446 assert res.can_moderate 

447 

448 

449def test_cannot_overlap_occurrences_schedule(db): 

450 user, token = generate_user() 

451 

452 with session_scope() as session: 

453 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

454 

455 start = now() 

456 

457 with events_session(token) as api: 

458 res = api.CreateEvent( 

459 events_pb2.CreateEventReq( 

460 title="Dummy Title", 

461 content="Dummy content.", 

462 parent_community_id=c_id, 

463 location=events_pb2.EventLocation( 

464 address="Near Null Island", 

465 lat=0.1, 

466 lng=0.2, 

467 ), 

468 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

469 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

470 timezone="UTC", 

471 ) 

472 ) 

473 

474 with pytest.raises(grpc.RpcError) as e: 

475 api.ScheduleEvent( 

476 events_pb2.ScheduleEventReq( 

477 event_id=res.event_id, 

478 content="New event occurrence", 

479 location=events_pb2.EventLocation( 

480 address="A bit further but still near Null Island", 

481 lat=0.3, 

482 lng=0.2, 

483 ), 

484 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

485 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

486 timezone="UTC", 

487 ) 

488 ) 

489 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

490 assert e.value.details() == "An event cannot have overlapping occurrences." 

491 

492 

493def test_cannot_overlap_occurrences_update(db): 

494 user, token = generate_user() 

495 

496 with session_scope() as session: 

497 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

498 

499 start = now() 

500 

501 with events_session(token) as api: 

502 res = api.CreateEvent( 

503 events_pb2.CreateEventReq( 

504 title="Dummy Title", 

505 content="Dummy content.", 

506 parent_community_id=c_id, 

507 location=events_pb2.EventLocation( 

508 address="Near Null Island", 

509 lat=0.1, 

510 lng=0.2, 

511 ), 

512 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

513 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

514 timezone="UTC", 

515 ) 

516 ) 

517 

518 event_id = api.ScheduleEvent( 

519 events_pb2.ScheduleEventReq( 

520 event_id=res.event_id, 

521 content="New event occurrence", 

522 location=events_pb2.EventLocation( 

523 address="A bit further but still near Null Island", 

524 lat=0.3, 

525 lng=0.2, 

526 ), 

527 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

528 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

529 timezone="UTC", 

530 ) 

531 ).event_id 

532 

533 # can overlap with this current existing occurrence 

534 api.UpdateEvent( 

535 events_pb2.UpdateEventReq( 

536 event_id=event_id, 

537 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

538 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

539 ) 

540 ) 

541 

542 with pytest.raises(grpc.RpcError) as e: 

543 api.UpdateEvent( 

544 events_pb2.UpdateEventReq( 

545 event_id=event_id, 

546 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

547 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

548 ) 

549 ) 

550 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

551 assert e.value.details() == "An event cannot have overlapping occurrences." 

552 

553 

554def test_UpdateEvent_single(db, moderator: Moderator): 

555 # test cases: 

556 # owner can update 

557 # community owner can update 

558 # notifies attendees 

559 

560 # event creator 

561 user1, token1 = generate_user() 

562 # community moderator 

563 user2, token2 = generate_user() 

564 # third parties 

565 user3, token3 = generate_user() 

566 user4, token4 = generate_user() 

567 user5, token5 = generate_user() 

568 user6, token6 = generate_user() 

569 

570 with session_scope() as session: 

571 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

572 

573 time_before = now() 

574 start_time = now() + timedelta(hours=2) 

575 end_time = start_time + timedelta(hours=3) 

576 

577 with events_session(token1) as api: 

578 res = api.CreateEvent( 

579 events_pb2.CreateEventReq( 

580 title="Dummy Title", 

581 content="Dummy content.", 

582 parent_community_id=c_id, 

583 location=events_pb2.EventLocation( 

584 address="Near Null Island", 

585 lat=0.1, 

586 lng=0.2, 

587 ), 

588 start_time=Timestamp_from_datetime(start_time), 

589 end_time=Timestamp_from_datetime(end_time), 

590 timezone="UTC", 

591 ) 

592 ) 

593 

594 event_id = res.event_id 

595 

596 moderator.approve_event_occurrence(event_id) 

597 

598 with events_session(token4) as api: 

599 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

600 

601 with events_session(token5) as api: 

602 api.SetEventAttendance( 

603 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

604 ) 

605 

606 with events_session(token6) as api: 

607 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

608 

609 time_before_update = now() 

610 

611 with events_session(token1) as api: 

612 res = api.UpdateEvent( 

613 events_pb2.UpdateEventReq( 

614 event_id=event_id, 

615 ) 

616 ) 

617 

618 with events_session(token1) as api: 

619 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

620 

621 assert res.is_next 

622 assert res.title == "Dummy Title" 

623 assert res.slug == "dummy-title" 

624 assert res.content == "Dummy content." 

625 assert not res.photo_url 

626 assert res.HasField("location") 

627 assert res.location.lat == 0.1 

628 assert res.location.lng == 0.2 

629 assert res.location.address == "Near Null Island" 

630 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

631 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

632 assert res.creator_user_id == user1.id 

633 assert to_aware_datetime(res.start_time) == start_time 

634 assert to_aware_datetime(res.end_time) == end_time 

635 # assert res.timezone == "UTC" 

636 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

637 assert res.organizer 

638 assert res.subscriber 

639 assert res.going_count == 2 

640 assert res.organizer_count == 1 

641 assert res.subscriber_count == 3 

642 assert res.owner_user_id == user1.id 

643 assert not res.owner_community_id 

644 assert not res.owner_group_id 

645 assert res.thread.thread_id 

646 assert res.can_edit 

647 assert not res.can_moderate 

648 

649 with events_session(token2) as api: 

650 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

651 

652 assert res.is_next 

653 assert res.title == "Dummy Title" 

654 assert res.slug == "dummy-title" 

655 assert res.content == "Dummy content." 

656 assert not res.photo_url 

657 assert res.HasField("location") 

658 assert res.location.lat == 0.1 

659 assert res.location.lng == 0.2 

660 assert res.location.address == "Near Null Island" 

661 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

662 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

663 assert res.creator_user_id == user1.id 

664 assert to_aware_datetime(res.start_time) == start_time 

665 assert to_aware_datetime(res.end_time) == end_time 

666 # assert res.timezone == "UTC" 

667 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

668 assert not res.organizer 

669 assert not res.subscriber 

670 assert res.going_count == 2 

671 assert res.organizer_count == 1 

672 assert res.subscriber_count == 3 

673 assert res.owner_user_id == user1.id 

674 assert not res.owner_community_id 

675 assert not res.owner_group_id 

676 assert res.thread.thread_id 

677 assert res.can_edit 

678 assert res.can_moderate 

679 

680 with events_session(token3) as api: 

681 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

682 

683 assert res.is_next 

684 assert res.title == "Dummy Title" 

685 assert res.slug == "dummy-title" 

686 assert res.content == "Dummy content." 

687 assert not res.photo_url 

688 assert res.HasField("location") 

689 assert res.location.lat == 0.1 

690 assert res.location.lng == 0.2 

691 assert res.location.address == "Near Null Island" 

692 assert time_before <= to_aware_datetime(res.created) <= time_before_update 

693 assert time_before_update <= to_aware_datetime(res.last_edited) <= now() 

694 assert res.creator_user_id == user1.id 

695 assert to_aware_datetime(res.start_time) == start_time 

696 assert to_aware_datetime(res.end_time) == end_time 

697 # assert res.timezone == "UTC" 

698 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

699 assert not res.organizer 

700 assert not res.subscriber 

701 assert res.going_count == 2 

702 assert res.organizer_count == 1 

703 assert res.subscriber_count == 3 

704 assert res.owner_user_id == user1.id 

705 assert not res.owner_community_id 

706 assert not res.owner_group_id 

707 assert res.thread.thread_id 

708 assert not res.can_edit 

709 assert not res.can_moderate 

710 

711 with events_session(token1) as api: 

712 res = api.UpdateEvent( 

713 events_pb2.UpdateEventReq( 

714 event_id=event_id, 

715 location=events_pb2.EventLocation( 

716 address="Nearer Null Island", 

717 lat=0.01, 

718 lng=0.02, 

719 ), 

720 ) 

721 ) 

722 

723 with events_session(token3) as api: 

724 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

725 

726 assert res.HasField("location") 

727 assert res.location.address == "Nearer Null Island" 

728 assert res.location.lat == 0.01 

729 assert res.location.lng == 0.02 

730 

731 

732def test_GetEvent_online(db, moderator: Moderator): 

733 """Validate that legacy online events are surfaced as offline events through the API.""" 

734 user1, token1 = generate_user() 

735 

736 with session_scope() as session: 

737 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

738 

739 start_time = now() + timedelta(hours=2) 

740 end_time = start_time + timedelta(hours=3) 

741 

742 # Create as an offline event since the API doesn't support online events anymore. 

743 with events_session(token1) as api: 

744 create_res: events_pb2.Event = api.CreateEvent( 

745 events_pb2.CreateEventReq( 

746 title="Dummy Title", 

747 content="Dummy content.", 

748 parent_community_id=c_id, 

749 location=events_pb2.EventLocation(address="Near Null Island", lat=0.1, lng=0.2), 

750 start_time=Timestamp_from_datetime(start_time), 

751 end_time=Timestamp_from_datetime(end_time), 

752 timezone="UTC", 

753 ) 

754 ) 

755 

756 event_id = create_res.event_id 

757 

758 moderator.approve_event_occurrence(event_id) 

759 

760 # Tweak the DB object to turn it into a legacy online event 

761 with session_scope() as session: 

762 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

763 occurrence.geom = None 

764 occurrence.address = None 

765 occurrence.link = "https://couchers.org/meet/" 

766 

767 # Backend should surface it as an offline event 

768 with events_session(token1) as api: 

769 get_res: events_pb2.Event = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

770 

771 assert get_res.title == "Dummy Title" 

772 assert get_res.HasField("location") 

773 assert get_res.location.address == "https://couchers.org/meet/" 

774 assert get_res.location.lng == 0 

775 assert get_res.location.lat == 0 

776 

777 

778def test_UpdateEvent_all(db, moderator: Moderator): 

779 # event creator 

780 user1, token1 = generate_user() 

781 # community moderator 

782 user2, token2 = generate_user() 

783 # third parties 

784 user3, token3 = generate_user() 

785 user4, token4 = generate_user() 

786 user5, token5 = generate_user() 

787 user6, token6 = generate_user() 

788 

789 with session_scope() as session: 

790 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

791 

792 time_before = now() 

793 start_time = now() + timedelta(hours=1) 

794 end_time = start_time + timedelta(hours=1.5) 

795 

796 event_ids = [] 

797 

798 with events_session(token1) as api: 

799 res = api.CreateEvent( 

800 events_pb2.CreateEventReq( 

801 title="Dummy Title", 

802 content="0th occurrence", 

803 location=events_pb2.EventLocation( 

804 address="Near Null Island", 

805 lat=0.1, 

806 lng=0.2, 

807 ), 

808 start_time=Timestamp_from_datetime(start_time), 

809 end_time=Timestamp_from_datetime(end_time), 

810 timezone="UTC", 

811 ) 

812 ) 

813 

814 event_id = res.event_id 

815 event_ids.append(event_id) 

816 

817 moderator.approve_event_occurrence(event_id) 

818 

819 with events_session(token4) as api: 

820 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

821 

822 with events_session(token5) as api: 

823 api.SetEventAttendance( 

824 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

825 ) 

826 

827 with events_session(token6) as api: 

828 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

829 

830 with events_session(token1) as api: 

831 for i in range(5): 

832 res = api.ScheduleEvent( 

833 events_pb2.ScheduleEventReq( 

834 event_id=event_ids[-1], 

835 content=f"{i + 1}th occurrence", 

836 location=events_pb2.EventLocation( 

837 address="Near Null Island", 

838 lat=0.1, 

839 lng=0.2, 

840 ), 

841 start_time=Timestamp_from_datetime(start_time + timedelta(hours=2 + i)), 

842 end_time=Timestamp_from_datetime(start_time + timedelta(hours=2.5 + i)), 

843 timezone="UTC", 

844 ) 

845 ) 

846 

847 event_ids.append(res.event_id) 

848 

849 # Approve all scheduled occurrences 

850 for eid in event_ids[1:]: 

851 moderator.approve_event_occurrence(eid) 

852 

853 updated_event_id = event_ids[3] 

854 

855 time_before_update = now() 

856 

857 with events_session(token1) as api: 

858 res = api.UpdateEvent( 

859 events_pb2.UpdateEventReq( 

860 event_id=updated_event_id, 

861 title=wrappers_pb2.StringValue(value="New Title"), 

862 content=wrappers_pb2.StringValue(value="New content."), 

863 location=events_pb2.EventLocation( 

864 lat=0.2, 

865 lng=0.2, 

866 ), 

867 update_all_future=True, 

868 ) 

869 ) 

870 

871 time_after_update = now() 

872 

873 with events_session(token2) as api: 

874 for i in range(3): 

875 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i])) 

876 assert res.content == f"{i}th occurrence" 

877 assert time_before <= to_aware_datetime(res.last_edited) <= time_before_update 

878 

879 for i in range(3, 6): 

880 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_ids[i])) 

881 assert res.content == "New content." 

882 assert time_before_update <= to_aware_datetime(res.last_edited) <= time_after_update 

883 

884 

885def test_GetEvent(db, moderator: Moderator): 

886 # event creator 

887 user1, token1 = generate_user() 

888 # community moderator 

889 user2, token2 = generate_user() 

890 # third parties 

891 user3, token3 = generate_user() 

892 user4, token4 = generate_user() 

893 user5, token5 = generate_user() 

894 user6, token6 = generate_user() 

895 

896 with session_scope() as session: 

897 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

898 

899 time_before = now() 

900 start_time = now() + timedelta(hours=2) 

901 end_time = start_time + timedelta(hours=3) 

902 

903 with events_session(token1) as api: 

904 # in person event 

905 res = api.CreateEvent( 

906 events_pb2.CreateEventReq( 

907 title="Dummy Title", 

908 content="Dummy content.", 

909 location=events_pb2.EventLocation( 

910 address="Near Null Island", 

911 lat=0.1, 

912 lng=0.2, 

913 ), 

914 start_time=Timestamp_from_datetime(start_time), 

915 end_time=Timestamp_from_datetime(end_time), 

916 timezone="UTC", 

917 ) 

918 ) 

919 

920 event_id = res.event_id 

921 

922 moderator.approve_event_occurrence(event_id) 

923 

924 with events_session(token4) as api: 

925 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

926 

927 with events_session(token5) as api: 

928 api.SetEventAttendance( 

929 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

930 ) 

931 

932 with events_session(token6) as api: 

933 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

934 

935 with events_session(token1) as api: 

936 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

937 

938 assert res.is_next 

939 assert res.title == "Dummy Title" 

940 assert res.slug == "dummy-title" 

941 assert res.content == "Dummy content." 

942 assert not res.photo_url 

943 assert res.HasField("location") 

944 assert res.location.lat == 0.1 

945 assert res.location.lng == 0.2 

946 assert res.location.address == "Near Null Island" 

947 assert time_before <= to_aware_datetime(res.created) <= now() 

948 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

949 assert res.creator_user_id == user1.id 

950 assert to_aware_datetime(res.start_time) == start_time 

951 assert to_aware_datetime(res.end_time) == end_time 

952 # assert res.timezone == "UTC" 

953 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_GOING 

954 assert res.organizer 

955 assert res.subscriber 

956 assert res.going_count == 2 

957 assert res.organizer_count == 1 

958 assert res.subscriber_count == 3 

959 assert res.owner_user_id == user1.id 

960 assert not res.owner_community_id 

961 assert not res.owner_group_id 

962 assert res.thread.thread_id 

963 assert res.can_edit 

964 assert not res.can_moderate 

965 

966 with events_session(token2) as api: 

967 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

968 

969 assert res.is_next 

970 assert res.title == "Dummy Title" 

971 assert res.slug == "dummy-title" 

972 assert res.content == "Dummy content." 

973 assert not res.photo_url 

974 assert res.HasField("location") 

975 assert res.location.lat == 0.1 

976 assert res.location.lng == 0.2 

977 assert res.location.address == "Near Null Island" 

978 assert time_before <= to_aware_datetime(res.created) <= now() 

979 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

980 assert res.creator_user_id == user1.id 

981 assert to_aware_datetime(res.start_time) == start_time 

982 assert to_aware_datetime(res.end_time) == end_time 

983 # assert res.timezone == "UTC" 

984 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

985 assert not res.organizer 

986 assert not res.subscriber 

987 assert res.going_count == 2 

988 assert res.organizer_count == 1 

989 assert res.subscriber_count == 3 

990 assert res.owner_user_id == user1.id 

991 assert not res.owner_community_id 

992 assert not res.owner_group_id 

993 assert res.thread.thread_id 

994 assert res.can_edit 

995 assert res.can_moderate 

996 

997 with events_session(token3) as api: 

998 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

999 

1000 assert res.is_next 

1001 assert res.title == "Dummy Title" 

1002 assert res.slug == "dummy-title" 

1003 assert res.content == "Dummy content." 

1004 assert not res.photo_url 

1005 assert res.HasField("location") 

1006 assert res.location.lat == 0.1 

1007 assert res.location.lng == 0.2 

1008 assert res.location.address == "Near Null Island" 

1009 assert time_before <= to_aware_datetime(res.created) <= now() 

1010 assert time_before <= to_aware_datetime(res.last_edited) <= now() 

1011 assert res.creator_user_id == user1.id 

1012 assert to_aware_datetime(res.start_time) == start_time 

1013 assert to_aware_datetime(res.end_time) == end_time 

1014 # assert res.timezone == "UTC" 

1015 assert res.attendance_state == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1016 assert not res.organizer 

1017 assert not res.subscriber 

1018 assert res.going_count == 2 

1019 assert res.organizer_count == 1 

1020 assert res.subscriber_count == 3 

1021 assert res.owner_user_id == user1.id 

1022 assert not res.owner_community_id 

1023 assert not res.owner_group_id 

1024 assert res.thread.thread_id 

1025 assert not res.can_edit 

1026 assert not res.can_moderate 

1027 

1028 

1029def test_CancelEvent(db, moderator: Moderator): 

1030 # event creator 

1031 user1, token1 = generate_user() 

1032 # community moderator 

1033 user2, token2 = generate_user() 

1034 # third parties 

1035 user3, token3 = generate_user() 

1036 user4, token4 = generate_user() 

1037 user5, token5 = generate_user() 

1038 user6, token6 = generate_user() 

1039 

1040 with session_scope() as session: 

1041 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

1042 

1043 start_time = now() + timedelta(hours=2) 

1044 end_time = start_time + timedelta(hours=3) 

1045 

1046 with events_session(token1) as api: 

1047 res = api.CreateEvent( 

1048 events_pb2.CreateEventReq( 

1049 title="Dummy Title", 

1050 content="Dummy content.", 

1051 location=events_pb2.EventLocation( 

1052 address="Near Null Island", 

1053 lat=0.1, 

1054 lng=0.2, 

1055 ), 

1056 start_time=Timestamp_from_datetime(start_time), 

1057 end_time=Timestamp_from_datetime(end_time), 

1058 timezone="UTC", 

1059 ) 

1060 ) 

1061 

1062 event_id = res.event_id 

1063 

1064 moderator.approve_event_occurrence(event_id) 

1065 

1066 with events_session(token4) as api: 

1067 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1068 

1069 with events_session(token5) as api: 

1070 api.SetEventAttendance( 

1071 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1072 ) 

1073 

1074 with events_session(token6) as api: 

1075 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1076 

1077 with events_session(token1) as api: 

1078 res = api.CancelEvent( 

1079 events_pb2.CancelEventReq( 

1080 event_id=event_id, 

1081 ) 

1082 ) 

1083 

1084 with events_session(token1) as api: 

1085 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1086 assert res.is_cancelled 

1087 

1088 with events_session(token1) as api: 

1089 with pytest.raises(grpc.RpcError) as e: 

1090 api.UpdateEvent( 

1091 events_pb2.UpdateEventReq( 

1092 event_id=event_id, 

1093 title=wrappers_pb2.StringValue(value="New Title"), 

1094 ) 

1095 ) 

1096 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1097 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1098 

1099 with pytest.raises(grpc.RpcError) as e: 

1100 api.InviteEventOrganizer( 

1101 events_pb2.InviteEventOrganizerReq( 

1102 event_id=event_id, 

1103 user_id=user3.id, 

1104 ) 

1105 ) 

1106 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1107 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1108 

1109 with pytest.raises(grpc.RpcError) as e: 

1110 api.TransferEvent(events_pb2.TransferEventReq(event_id=event_id, new_owner_community_id=c_id)) 

1111 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1112 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1113 

1114 with events_session(token3) as api: 

1115 with pytest.raises(grpc.RpcError) as e: 

1116 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1117 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1118 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1119 

1120 with pytest.raises(grpc.RpcError) as e: 

1121 api.SetEventAttendance( 

1122 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1123 ) 

1124 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1125 assert e.value.details() == "You can't modify, subscribe to, or attend to an event that's been cancelled." 

1126 

1127 with events_session(token1) as api: 

1128 for include_cancelled in [True, False]: 

1129 res = api.ListEventOccurrences( 

1130 events_pb2.ListEventOccurrencesReq( 

1131 event_id=event_id, 

1132 include_cancelled=include_cancelled, 

1133 ) 

1134 ) 

1135 if include_cancelled: 

1136 assert len(res.events) > 0 

1137 else: 

1138 assert len(res.events) == 0 

1139 

1140 res = api.ListMyEvents( 

1141 events_pb2.ListMyEventsReq( 

1142 include_cancelled=include_cancelled, 

1143 ) 

1144 ) 

1145 if include_cancelled: 

1146 assert len(res.events) > 0 

1147 else: 

1148 assert len(res.events) == 0 

1149 

1150 

1151def test_ListEventAttendees(db, moderator: Moderator): 

1152 # event creator 

1153 user1, token1 = generate_user() 

1154 # others 

1155 user2, token2 = generate_user() 

1156 user3, token3 = generate_user() 

1157 user4, token4 = generate_user() 

1158 user5, token5 = generate_user() 

1159 user6, token6 = generate_user() 

1160 

1161 with session_scope() as session: 

1162 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1163 

1164 with events_session(token1) as api: 

1165 event_id = api.CreateEvent( 

1166 events_pb2.CreateEventReq( 

1167 title="Dummy Title", 

1168 content="Dummy content.", 

1169 location=events_pb2.EventLocation( 

1170 address="Near Null Island", 

1171 lat=0.1, 

1172 lng=0.2, 

1173 ), 

1174 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1175 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1176 timezone="UTC", 

1177 ) 

1178 ).event_id 

1179 

1180 moderator.approve_event_occurrence(event_id) 

1181 

1182 for token in [token2, token3, token4, token5]: 

1183 with events_session(token) as api: 

1184 api.SetEventAttendance( 

1185 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1186 ) 

1187 

1188 with events_session(token6) as api: 

1189 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).going_count == 5 

1190 

1191 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2)) 

1192 assert res.attendee_user_ids == [user1.id, user2.id] 

1193 

1194 res = api.ListEventAttendees( 

1195 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1196 ) 

1197 assert res.attendee_user_ids == [user3.id, user4.id] 

1198 

1199 res = api.ListEventAttendees( 

1200 events_pb2.ListEventAttendeesReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1201 ) 

1202 assert res.attendee_user_ids == [user5.id] 

1203 assert not res.next_page_token 

1204 

1205 

1206def test_ListEventSubscribers(db, moderator: Moderator): 

1207 # event creator 

1208 user1, token1 = generate_user() 

1209 # others 

1210 user2, token2 = generate_user() 

1211 user3, token3 = generate_user() 

1212 user4, token4 = generate_user() 

1213 user5, token5 = generate_user() 

1214 user6, token6 = generate_user() 

1215 

1216 with session_scope() as session: 

1217 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1218 

1219 with events_session(token1) as api: 

1220 event_id = api.CreateEvent( 

1221 events_pb2.CreateEventReq( 

1222 title="Dummy Title", 

1223 content="Dummy content.", 

1224 location=events_pb2.EventLocation( 

1225 address="Near Null Island", 

1226 lat=0.1, 

1227 lng=0.2, 

1228 ), 

1229 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1230 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1231 timezone="UTC", 

1232 ) 

1233 ).event_id 

1234 

1235 moderator.approve_event_occurrence(event_id) 

1236 

1237 for token in [token2, token3, token4, token5]: 

1238 with events_session(token) as api: 

1239 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1240 

1241 with events_session(token6) as api: 

1242 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber_count == 5 

1243 

1244 res = api.ListEventSubscribers(events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2)) 

1245 assert res.subscriber_user_ids == [user1.id, user2.id] 

1246 

1247 res = api.ListEventSubscribers( 

1248 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1249 ) 

1250 assert res.subscriber_user_ids == [user3.id, user4.id] 

1251 

1252 res = api.ListEventSubscribers( 

1253 events_pb2.ListEventSubscribersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1254 ) 

1255 assert res.subscriber_user_ids == [user5.id] 

1256 assert not res.next_page_token 

1257 

1258 

1259def test_ListEventOrganizers(db, moderator: Moderator): 

1260 # event creator 

1261 user1, token1 = generate_user() 

1262 # others 

1263 user2, token2 = generate_user() 

1264 user3, token3 = generate_user() 

1265 user4, token4 = generate_user() 

1266 user5, token5 = generate_user() 

1267 user6, token6 = generate_user() 

1268 

1269 with session_scope() as session: 

1270 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1271 

1272 with events_session(token1) as api: 

1273 event_id = api.CreateEvent( 

1274 events_pb2.CreateEventReq( 

1275 title="Dummy Title", 

1276 content="Dummy content.", 

1277 location=events_pb2.EventLocation( 

1278 address="Near Null Island", 

1279 lat=0.1, 

1280 lng=0.2, 

1281 ), 

1282 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1283 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1284 timezone="UTC", 

1285 ) 

1286 ).event_id 

1287 

1288 moderator.approve_event_occurrence(event_id) 

1289 

1290 with events_session(token1) as api: 

1291 for user_id in [user2.id, user3.id, user4.id, user5.id]: 

1292 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user_id)) 

1293 

1294 with events_session(token6) as api: 

1295 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer_count == 5 

1296 

1297 res = api.ListEventOrganizers(events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2)) 

1298 assert res.organizer_user_ids == [user1.id, user2.id] 

1299 

1300 res = api.ListEventOrganizers( 

1301 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1302 ) 

1303 assert res.organizer_user_ids == [user3.id, user4.id] 

1304 

1305 res = api.ListEventOrganizers( 

1306 events_pb2.ListEventOrganizersReq(event_id=event_id, page_size=2, page_token=res.next_page_token) 

1307 ) 

1308 assert res.organizer_user_ids == [user5.id] 

1309 assert not res.next_page_token 

1310 

1311 

1312def test_TransferEvent(db): 

1313 user1, token1 = generate_user() 

1314 user2, token2 = generate_user() 

1315 user3, token3 = generate_user() 

1316 user4, token4 = generate_user() 

1317 

1318 with session_scope() as session: 

1319 c = create_community(session, 0, 2, "Community", [user3], [], None) 

1320 h = create_group(session, "Group", [user4], [], c) 

1321 c_id = c.id 

1322 h_id = h.id 

1323 

1324 with events_session(token1) as api: 

1325 event_id = api.CreateEvent( 

1326 events_pb2.CreateEventReq( 

1327 title="Dummy Title", 

1328 content="Dummy content.", 

1329 location=events_pb2.EventLocation( 

1330 address="Near Null Island", 

1331 lat=0.1, 

1332 lng=0.2, 

1333 ), 

1334 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1335 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1336 timezone="UTC", 

1337 ) 

1338 ).event_id 

1339 

1340 api.TransferEvent( 

1341 events_pb2.TransferEventReq( 

1342 event_id=event_id, 

1343 new_owner_community_id=c_id, 

1344 ) 

1345 ) 

1346 

1347 # remove ourselves as organizer, otherwise we can still edit it 

1348 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1349 

1350 with pytest.raises(grpc.RpcError) as e: 

1351 api.TransferEvent( 

1352 events_pb2.TransferEventReq( 

1353 event_id=event_id, 

1354 new_owner_group_id=h_id, 

1355 ) 

1356 ) 

1357 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1358 assert e.value.details() == "You're not allowed to transfer that event." 

1359 

1360 event_id = api.CreateEvent( 

1361 events_pb2.CreateEventReq( 

1362 title="Dummy Title", 

1363 content="Dummy content.", 

1364 location=events_pb2.EventLocation( 

1365 address="Near Null Island", 

1366 lat=0.1, 

1367 lng=0.2, 

1368 ), 

1369 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1370 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1371 timezone="UTC", 

1372 ) 

1373 ).event_id 

1374 

1375 api.TransferEvent( 

1376 events_pb2.TransferEventReq( 

1377 event_id=event_id, 

1378 new_owner_group_id=h_id, 

1379 ) 

1380 ) 

1381 

1382 # remove ourselves as organizer, otherwise we can still edit it 

1383 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1384 

1385 with pytest.raises(grpc.RpcError) as e: 

1386 api.TransferEvent( 

1387 events_pb2.TransferEventReq( 

1388 event_id=event_id, 

1389 new_owner_community_id=c_id, 

1390 ) 

1391 ) 

1392 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1393 assert e.value.details() == "You're not allowed to transfer that event." 

1394 

1395 

1396def test_SetEventSubscription(db, moderator: Moderator): 

1397 user1, token1 = generate_user() 

1398 user2, token2 = generate_user() 

1399 

1400 with session_scope() as session: 

1401 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1402 

1403 with events_session(token1) as api: 

1404 event_id = api.CreateEvent( 

1405 events_pb2.CreateEventReq( 

1406 title="Dummy Title", 

1407 content="Dummy content.", 

1408 location=events_pb2.EventLocation( 

1409 address="Near Null Island", 

1410 lat=0.1, 

1411 lng=0.2, 

1412 ), 

1413 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1414 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1415 timezone="UTC", 

1416 ) 

1417 ).event_id 

1418 

1419 moderator.approve_event_occurrence(event_id) 

1420 

1421 with events_session(token2) as api: 

1422 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1423 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

1424 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1425 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=False)) 

1426 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).subscriber 

1427 

1428 

1429def test_SetEventAttendance(db, moderator: Moderator): 

1430 user1, token1 = generate_user() 

1431 user2, token2 = generate_user() 

1432 

1433 with session_scope() as session: 

1434 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1435 

1436 with events_session(token1) as api: 

1437 event_id = api.CreateEvent( 

1438 events_pb2.CreateEventReq( 

1439 title="Dummy Title", 

1440 content="Dummy content.", 

1441 location=events_pb2.EventLocation( 

1442 address="Near Null Island", 

1443 lat=0.1, 

1444 lng=0.2, 

1445 ), 

1446 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1447 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1448 timezone="UTC", 

1449 ) 

1450 ).event_id 

1451 

1452 moderator.approve_event_occurrence(event_id) 

1453 

1454 with events_session(token2) as api: 

1455 assert ( 

1456 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1457 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1458 ) 

1459 api.SetEventAttendance( 

1460 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1461 ) 

1462 assert ( 

1463 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1464 == events_pb2.ATTENDANCE_STATE_GOING 

1465 ) 

1466 api.SetEventAttendance( 

1467 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING) 

1468 ) 

1469 assert ( 

1470 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).attendance_state 

1471 == events_pb2.ATTENDANCE_STATE_NOT_GOING 

1472 ) 

1473 

1474 

1475def test_InviteEventOrganizer(db, moderator: Moderator): 

1476 user1, token1 = generate_user() 

1477 user2, token2 = generate_user() 

1478 

1479 with session_scope() as session: 

1480 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1481 

1482 with events_session(token1) as api: 

1483 event_id = api.CreateEvent( 

1484 events_pb2.CreateEventReq( 

1485 title="Dummy Title", 

1486 content="Dummy content.", 

1487 location=events_pb2.EventLocation( 

1488 address="Near Null Island", 

1489 lat=0.1, 

1490 lng=0.2, 

1491 ), 

1492 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1493 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1494 timezone="UTC", 

1495 ) 

1496 ).event_id 

1497 

1498 moderator.approve_event_occurrence(event_id) 

1499 

1500 with events_session(token2) as api: 

1501 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1502 

1503 with pytest.raises(grpc.RpcError) as e: 

1504 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user1.id)) 

1505 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1506 assert e.value.details() == "You're not allowed to edit that event." 

1507 

1508 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1509 

1510 with events_session(token1) as api: 

1511 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1512 

1513 with events_session(token2) as api: 

1514 assert api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1515 

1516 

1517def test_ListEventOccurrences(db): 

1518 user1, token1 = generate_user() 

1519 user2, token2 = generate_user() 

1520 user3, token3 = generate_user() 

1521 

1522 with session_scope() as session: 

1523 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

1524 

1525 start = now() 

1526 

1527 event_ids = [] 

1528 

1529 with events_session(token1) as api: 

1530 res = api.CreateEvent( 

1531 events_pb2.CreateEventReq( 

1532 title="First occurrence", 

1533 content="Dummy content.", 

1534 parent_community_id=c_id, 

1535 location=events_pb2.EventLocation( 

1536 address="Near Null Island", 

1537 lat=0.1, 

1538 lng=0.2, 

1539 ), 

1540 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

1541 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

1542 timezone="UTC", 

1543 ) 

1544 ) 

1545 

1546 event_ids.append(res.event_id) 

1547 

1548 for i in range(5): 

1549 res = api.ScheduleEvent( 

1550 events_pb2.ScheduleEventReq( 

1551 event_id=event_ids[-1], 

1552 content=f"{i}th occurrence", 

1553 location=events_pb2.EventLocation( 

1554 address="Near Null Island", 

1555 lat=0.1, 

1556 lng=0.2, 

1557 ), 

1558 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

1559 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

1560 timezone="UTC", 

1561 ) 

1562 ) 

1563 

1564 event_ids.append(res.event_id) 

1565 

1566 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2)) 

1567 assert [event.event_id for event in res.events] == event_ids[:2] 

1568 

1569 res = api.ListEventOccurrences( 

1570 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token) 

1571 ) 

1572 assert [event.event_id for event in res.events] == event_ids[2:4] 

1573 

1574 res = api.ListEventOccurrences( 

1575 events_pb2.ListEventOccurrencesReq(event_id=event_ids[-1], page_size=2, page_token=res.next_page_token) 

1576 ) 

1577 assert [event.event_id for event in res.events] == event_ids[4:6] 

1578 assert not res.next_page_token 

1579 

1580 

1581def test_ListMyEvents(db, moderator: Moderator): 

1582 user1, token1 = generate_user() 

1583 user2, token2 = generate_user() 

1584 user3, token3 = generate_user() 

1585 user4, token4 = generate_user() 

1586 user5, token5 = generate_user() 

1587 

1588 with session_scope() as session: 

1589 # Create global (world) -> macroregion -> region -> subregion hierarchy 

1590 # my_communities_exclude_global filters out world, macroregion, and region level communities 

1591 global_community = create_community(session, 0, 100, "Global", [user3], [], None) 

1592 c_id = global_community.id 

1593 macroregion_community = create_community( 

1594 session, 0, 75, "Macroregion Community", [user3, user4], [], global_community 

1595 ) 

1596 region_community = create_community( 

1597 session, 0, 50, "Region Community", [user3, user4], [], macroregion_community 

1598 ) 

1599 subregion_community = create_community( 

1600 session, 0, 25, "Subregion Community", [user3, user4], [], region_community 

1601 ) 

1602 c2_id = subregion_community.id 

1603 

1604 start = now() 

1605 

1606 def new_event(hours_from_now: int, community_id: int) -> events_pb2.CreateEventReq: 

1607 return events_pb2.CreateEventReq( 

1608 title="Dummy Title", 

1609 content="Dummy content.", 

1610 location=events_pb2.EventLocation( 

1611 address="Near Null Island", 

1612 lat=0.1, 

1613 lng=0.2, 

1614 ), 

1615 parent_community_id=community_id, 

1616 timezone="UTC", 

1617 start_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now)), 

1618 end_time=Timestamp_from_datetime(start + timedelta(hours=hours_from_now + 0.5)), 

1619 ) 

1620 

1621 with events_session(token1) as api: 

1622 e2 = api.CreateEvent(new_event(2, c_id)).event_id 

1623 

1624 moderator.approve_event_occurrence(e2) 

1625 

1626 with events_session(token2) as api: 

1627 e1 = api.CreateEvent(new_event(1, c_id)).event_id 

1628 

1629 moderator.approve_event_occurrence(e1) 

1630 

1631 with events_session(token1) as api: 

1632 e3 = api.CreateEvent(new_event(3, c_id)).event_id 

1633 

1634 moderator.approve_event_occurrence(e3) 

1635 

1636 with events_session(token2) as api: 

1637 e5 = api.CreateEvent(new_event(5, c_id)).event_id 

1638 

1639 moderator.approve_event_occurrence(e5) 

1640 

1641 with events_session(token3) as api: 

1642 e4 = api.CreateEvent(new_event(4, c_id)).event_id 

1643 

1644 moderator.approve_event_occurrence(e4) 

1645 

1646 with events_session(token4) as api: 

1647 e6 = api.CreateEvent(new_event(6, c2_id)).event_id 

1648 

1649 moderator.approve_event_occurrence(e6) 

1650 

1651 with events_session(token1) as api: 

1652 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e3, user_id=user3.id)) 

1653 

1654 with events_session(token1) as api: 

1655 api.SetEventAttendance( 

1656 events_pb2.SetEventAttendanceReq(event_id=e1, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1657 ) 

1658 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e4, subscribe=True)) 

1659 

1660 with events_session(token2) as api: 

1661 api.SetEventAttendance( 

1662 events_pb2.SetEventAttendanceReq(event_id=e3, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1663 ) 

1664 

1665 with events_session(token3) as api: 

1666 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=e2, subscribe=True)) 

1667 

1668 with events_session(token1) as api: 

1669 # test pagination with token first 

1670 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2)) 

1671 assert [event.event_id for event in res.events] == [e1, e2] 

1672 res = api.ListMyEvents(events_pb2.ListMyEventsReq(page_size=2, page_token=res.next_page_token)) 

1673 assert [event.event_id for event in res.events] == [e3, e4] 

1674 assert not res.next_page_token 

1675 

1676 res = api.ListMyEvents( 

1677 events_pb2.ListMyEventsReq( 

1678 subscribed=True, 

1679 attending=True, 

1680 organizing=True, 

1681 ) 

1682 ) 

1683 assert [event.event_id for event in res.events] == [e1, e2, e3, e4] 

1684 

1685 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1686 assert [event.event_id for event in res.events] == [e1, e2, e3, e4] 

1687 

1688 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1689 assert [event.event_id for event in res.events] == [e2, e3, e4] 

1690 

1691 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1692 assert [event.event_id for event in res.events] == [e1, e2, e3] 

1693 

1694 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1695 assert [event.event_id for event in res.events] == [e2, e3] 

1696 

1697 with events_session(token1) as api: 

1698 # Test pagination with page_number and verify total_items 

1699 res = api.ListMyEvents( 

1700 events_pb2.ListMyEventsReq(page_size=2, page_number=1, subscribed=True, attending=True, organizing=True) 

1701 ) 

1702 assert [event.event_id for event in res.events] == [e1, e2] 

1703 assert res.total_items == 4 

1704 

1705 res = api.ListMyEvents( 

1706 events_pb2.ListMyEventsReq(page_size=2, page_number=2, subscribed=True, attending=True, organizing=True) 

1707 ) 

1708 assert [event.event_id for event in res.events] == [e3, e4] 

1709 assert res.total_items == 4 

1710 

1711 # Verify no more pages 

1712 res = api.ListMyEvents( 

1713 events_pb2.ListMyEventsReq(page_size=2, page_number=3, subscribed=True, attending=True, organizing=True) 

1714 ) 

1715 assert not res.events 

1716 assert res.total_items == 4 

1717 

1718 with events_session(token2) as api: 

1719 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1720 assert [event.event_id for event in res.events] == [e1, e3, e5] 

1721 

1722 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1723 assert [event.event_id for event in res.events] == [e1, e5] 

1724 

1725 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1726 assert [event.event_id for event in res.events] == [e1, e3, e5] 

1727 

1728 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1729 assert [event.event_id for event in res.events] == [e1, e5] 

1730 

1731 with events_session(token3) as api: 

1732 # user3 is member of both global (c_id) and child (c2_id) communities 

1733 res = api.ListMyEvents(events_pb2.ListMyEventsReq()) 

1734 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1735 

1736 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True)) 

1737 assert [event.event_id for event in res.events] == [e2, e4] 

1738 

1739 res = api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True)) 

1740 assert [event.event_id for event in res.events] == [e4] 

1741 

1742 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True)) 

1743 assert [event.event_id for event in res.events] == [e3, e4] 

1744 

1745 # my_communities returns events from both communities user3 is a member of 

1746 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True)) 

1747 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1748 

1749 # my_communities_exclude_global filters out events from global community (node_id=1) 

1750 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, my_communities_exclude_global=True)) 

1751 assert [event.event_id for event in res.events] == [e6] 

1752 

1753 # my_communities_exclude_global works independently of my_communities flag 

1754 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities_exclude_global=True)) 

1755 assert [event.event_id for event in res.events] == [e6] 

1756 

1757 # my_communities_exclude_global filters organizing results too 

1758 res = api.ListMyEvents(events_pb2.ListMyEventsReq(organizing=True, my_communities_exclude_global=True)) 

1759 assert [event.event_id for event in res.events] == [] 

1760 

1761 # my_communities_exclude_global filters subscribed results too 

1762 res = api.ListMyEvents(events_pb2.ListMyEventsReq(subscribed=True, my_communities_exclude_global=True)) 

1763 assert [event.event_id for event in res.events] == [] 

1764 

1765 with events_session(token5) as api: 

1766 res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

1767 assert [event.event_id for event in res.events] == [e1, e2, e3, e4, e5, e6] 

1768 

1769 

1770def test_list_my_events_exclude_attending(db, moderator: Moderator): 

1771 user1, token1 = generate_user() 

1772 user2, token2 = generate_user() 

1773 

1774 with session_scope() as session: 

1775 c = create_community(session, 0, 100, "Community", [user1, user2], [], None) 

1776 c_id = c.id 

1777 

1778 start = now() 

1779 

1780 def make_event(hours): 

1781 return events_pb2.CreateEventReq( 

1782 title="Test Event", 

1783 content="Test content.", 

1784 location=events_pb2.EventLocation( 

1785 address="Near Null Island", 

1786 lat=0.1, 

1787 lng=0.2, 

1788 ), 

1789 parent_community_id=c_id, 

1790 timezone="UTC", 

1791 start_time=Timestamp_from_datetime(start + timedelta(hours=hours)), 

1792 end_time=Timestamp_from_datetime(start + timedelta(hours=hours + 1)), 

1793 ) 

1794 

1795 # user1 organizes e_own; user2 organizes e_attending and e_community_only 

1796 with events_session(token1) as api: 

1797 e_own = api.CreateEvent(make_event(1)).event_id 

1798 

1799 with events_session(token2) as api: 

1800 e_attending = api.CreateEvent(make_event(2)).event_id 

1801 e_community_only = api.CreateEvent(make_event(3)).event_id 

1802 # e_both: user1 will be both organizer and attendee 

1803 e_both = api.CreateEvent(make_event(4)).event_id 

1804 

1805 moderator.approve_event_occurrence(e_own) 

1806 moderator.approve_event_occurrence(e_attending) 

1807 moderator.approve_event_occurrence(e_community_only) 

1808 moderator.approve_event_occurrence(e_both) 

1809 

1810 # invite user1 as organizer of e_both 

1811 with events_session(token2) as api: 

1812 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=e_both, user_id=user1.id)) 

1813 

1814 # user1 RSVPs to e_attending and e_both 

1815 with events_session(token1) as api: 

1816 api.SetEventAttendance( 

1817 events_pb2.SetEventAttendanceReq(event_id=e_attending, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1818 ) 

1819 api.SetEventAttendance( 

1820 events_pb2.SetEventAttendanceReq(event_id=e_both, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1821 ) 

1822 

1823 with events_session(token1) as api: 

1824 # baseline: all four community events visible 

1825 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True)) 

1826 assert {e.event_id for e in res.events} == {e_own, e_attending, e_community_only, e_both} 

1827 

1828 # exclude_attending removes events user1 is attending (e_attending, e_both) 

1829 # and events user1 is organizing (e_own, e_both) — leaving only e_community_only 

1830 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True)) 

1831 assert [e.event_id for e in res.events] == [e_community_only] 

1832 

1833 # exclude_attending with attending=True: invalid combination 

1834 with pytest.raises(grpc.RpcError) as e: 

1835 api.ListMyEvents(events_pb2.ListMyEventsReq(attending=True, exclude_attending=True)) 

1836 assert e.value.code() == grpc.StatusCode.INVALID_ARGUMENT 

1837 

1838 # user2 has no attendance/organizing relationship with e_community_only, so exclude_attending has no effect on it 

1839 with events_session(token2) as api: 

1840 res = api.ListMyEvents(events_pb2.ListMyEventsReq(my_communities=True, exclude_attending=True)) 

1841 # user2 organizes e_attending, e_community_only, e_both — all excluded except e_own (user2 has no relation) 

1842 assert [e.event_id for e in res.events] == [e_own] 

1843 

1844 

1845def test_RemoveEventOrganizer(db, moderator: Moderator): 

1846 user1, token1 = generate_user() 

1847 user2, token2 = generate_user() 

1848 

1849 with session_scope() as session: 

1850 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1851 

1852 with events_session(token1) as api: 

1853 event_id = api.CreateEvent( 

1854 events_pb2.CreateEventReq( 

1855 title="Dummy Title", 

1856 content="Dummy content.", 

1857 location=events_pb2.EventLocation( 

1858 address="Near Null Island", 

1859 lat=0.1, 

1860 lng=0.2, 

1861 ), 

1862 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

1863 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

1864 timezone="UTC", 

1865 ) 

1866 ).event_id 

1867 

1868 moderator.approve_event_occurrence(event_id) 

1869 

1870 with events_session(token2) as api: 

1871 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1872 

1873 with pytest.raises(grpc.RpcError) as e: 

1874 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1875 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1876 assert e.value.details() == "You're not allowed to edit that event." 

1877 

1878 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1879 

1880 with events_session(token1) as api: 

1881 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1882 

1883 with pytest.raises(grpc.RpcError) as e: 

1884 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1885 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1886 assert e.value.details() == "You cannot remove the event owner as an organizer." 

1887 

1888 with events_session(token2) as api: 

1889 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1890 assert res.organizer 

1891 assert res.organizer_count == 2 

1892 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1893 assert not api.GetEvent(events_pb2.GetEventReq(event_id=event_id)).organizer 

1894 

1895 with pytest.raises(grpc.RpcError) as e: 

1896 api.RemoveEventOrganizer(events_pb2.RemoveEventOrganizerReq(event_id=event_id)) 

1897 assert e.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

1898 assert e.value.details() == "You're not allowed to edit that event." 

1899 

1900 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1901 assert not res.organizer 

1902 assert res.organizer_count == 1 

1903 

1904 # Test that event owner can remove co-organizers 

1905 with events_session(token1) as api: 

1906 # Add user2 back as organizer 

1907 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1908 

1909 # Verify user2 is now an organizer 

1910 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1911 assert res.organizer_count == 2 

1912 

1913 # Event owner can remove co-organizer 

1914 api.RemoveEventOrganizer( 

1915 events_pb2.RemoveEventOrganizerReq(event_id=event_id, user_id=wrappers_pb2.Int64Value(value=user2.id)) 

1916 ) 

1917 

1918 # Verify user2 is no longer an organizer 

1919 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

1920 assert res.organizer_count == 1 

1921 

1922 # Test that non-organizers cannot remove other organizers 

1923 with events_session(token2) as api: 

1924 # User2 cannot invite themselves as organizer (not the owner) 

1925 with pytest.raises(grpc.RpcError) as e: 

1926 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1927 assert e.value.code() == grpc.StatusCode.PERMISSION_DENIED 

1928 assert e.value.details() == "You're not allowed to edit that event." 

1929 

1930 # Test that non-organizers cannot remove other organizers (user1 adds user2 back first) 

1931 with events_session(token1) as api: 

1932 # Add user2 back as organizer 

1933 api.InviteEventOrganizer(events_pb2.InviteEventOrganizerReq(event_id=event_id, user_id=user2.id)) 

1934 

1935 

1936def test_ListEventAttendees_regression(db): 

1937 # see issue #1617: 

1938 # 

1939 # 1. Create an event 

1940 # 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` 

1941 # 3. Change the current user's attendance state to "not going" (with `SetEventAttendance`) 

1942 # 4. Change the current user's attendance state to "going" again 

1943 # 

1944 # **Expected behaviour** 

1945 # `ListEventAttendees` should return the current user's ID 

1946 # 

1947 # **Actual/current behaviour** 

1948 # `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 

1949 

1950 user1, token1 = generate_user() 

1951 user2, token2 = generate_user() 

1952 user3, token3 = generate_user() 

1953 user4, token4 = generate_user() 

1954 user5, token5 = generate_user() 

1955 

1956 with session_scope() as session: 

1957 c_id = create_community(session, 0, 2, "Community", [user1], [], None).id 

1958 

1959 start_time = now() + timedelta(hours=2) 

1960 end_time = start_time + timedelta(hours=3) 

1961 

1962 with events_session(token1) as api: 

1963 res = api.CreateEvent( 

1964 events_pb2.CreateEventReq( 

1965 title="Dummy Title", 

1966 content="Dummy content.", 

1967 location=events_pb2.EventLocation( 

1968 address="Near Null Island", 

1969 lat=0.1, 

1970 lng=0.2, 

1971 ), 

1972 parent_community_id=c_id, 

1973 start_time=Timestamp_from_datetime(start_time), 

1974 end_time=Timestamp_from_datetime(end_time), 

1975 timezone="UTC", 

1976 ) 

1977 ) 

1978 

1979 res = api.TransferEvent( 

1980 events_pb2.TransferEventReq( 

1981 event_id=res.event_id, 

1982 new_owner_community_id=c_id, 

1983 ) 

1984 ) 

1985 

1986 event_id = res.event_id 

1987 

1988 api.SetEventAttendance( 

1989 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_NOT_GOING) 

1990 ) 

1991 api.SetEventAttendance( 

1992 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

1993 ) 

1994 

1995 res = api.ListEventAttendees(events_pb2.ListEventAttendeesReq(event_id=event_id)) 

1996 assert len(res.attendee_user_ids) == 1 

1997 assert res.attendee_user_ids[0] == user1.id 

1998 

1999 

2000def test_event_threads(db, push_collector: PushCollector, moderator: Moderator): 

2001 user1, token1 = generate_user() 

2002 user2, token2 = generate_user() 

2003 user3, token3 = generate_user() 

2004 user4, token4 = generate_user() 

2005 

2006 with session_scope() as session: 

2007 c = create_community(session, 0, 2, "Community", [user3], [], None) 

2008 h = create_group(session, "Group", [user4], [], c) 

2009 c_id = c.id 

2010 h_id = h.id 

2011 user4_id = user4.id 

2012 

2013 with events_session(token1) as api: 

2014 event = api.CreateEvent( 

2015 events_pb2.CreateEventReq( 

2016 title="Dummy Title", 

2017 content="Dummy content.", 

2018 location=events_pb2.EventLocation( 

2019 address="Near Null Island", 

2020 lat=0.1, 

2021 lng=0.2, 

2022 ), 

2023 start_time=Timestamp_from_datetime(now() + timedelta(hours=2)), 

2024 end_time=Timestamp_from_datetime(now() + timedelta(hours=5)), 

2025 timezone="UTC", 

2026 ) 

2027 ) 

2028 

2029 moderator.approve_event_occurrence(event.event_id) 

2030 

2031 with threads_session(token2) as api: 

2032 reply_id = api.PostReply(threads_pb2.PostReplyReq(thread_id=event.thread.thread_id, content="hi")).thread_id 

2033 

2034 moderator.approve_thread_post(reply_id) 

2035 

2036 with events_session(token3) as api: 

2037 res = api.GetEvent(events_pb2.GetEventReq(event_id=event.event_id)) 

2038 assert res.thread.num_responses == 1 

2039 

2040 with threads_session(token3) as api: 

2041 ret = api.GetThread(threads_pb2.GetThreadReq(thread_id=res.thread.thread_id)) 

2042 assert len(ret.replies) == 1 

2043 assert not ret.next_page_token 

2044 assert ret.replies[0].thread_id == reply_id 

2045 assert ret.replies[0].content == "hi" 

2046 assert ret.replies[0].author_user_id == user2.id 

2047 assert ret.replies[0].num_replies == 0 

2048 

2049 nested_reply_id = api.PostReply( 

2050 threads_pb2.PostReplyReq(thread_id=reply_id, content="what a silly comment") 

2051 ).thread_id 

2052 

2053 moderator.approve_thread_post(nested_reply_id) 

2054 

2055 process_jobs() 

2056 

2057 push = push_collector.pop_for_user(user1.id, last=True) 

2058 assert push.topic_action == NotificationTopicAction.event__comment.display 

2059 assert push.content.title == f"{user2.name} • Dummy Title" 

2060 assert push.content.ios_title == user2.name 

2061 assert push.content.ios_subtitle == "Commented on Dummy Title" 

2062 assert push.content.body == "hi" 

2063 

2064 push = push_collector.pop_for_user(user2.id, last=True) 

2065 assert push.content.title == f"{user3.name} • Dummy Title" 

2066 

2067 assert push_collector.count_for_user(user4_id) == 0 

2068 

2069 

2070def test_can_overlap_other_events_schedule_regression(db): 

2071 # we had a bug where we were checking overlapping for *all* occurrences of *all* events, not just the ones for this event 

2072 user, token = generate_user() 

2073 

2074 with session_scope() as session: 

2075 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2076 

2077 start = now() 

2078 

2079 with events_session(token) as api: 

2080 # create another event, should be able to overlap with this one 

2081 api.CreateEvent( 

2082 events_pb2.CreateEventReq( 

2083 title="Dummy Title", 

2084 content="Dummy content.", 

2085 parent_community_id=c_id, 

2086 location=events_pb2.EventLocation( 

2087 address="Near Null Island", 

2088 lat=0.1, 

2089 lng=0.2, 

2090 ), 

2091 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2092 end_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2093 timezone="UTC", 

2094 ) 

2095 ) 

2096 

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_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2109 end_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2110 timezone="UTC", 

2111 ) 

2112 ) 

2113 

2114 # this doesn't overlap with the just created event, but does overlap with the occurrence from earlier; which should be no problem 

2115 api.ScheduleEvent( 

2116 events_pb2.ScheduleEventReq( 

2117 event_id=res.event_id, 

2118 content="New event occurrence", 

2119 location=events_pb2.EventLocation( 

2120 address="A bit further but still near Null Island", 

2121 lat=0.3, 

2122 lng=0.2, 

2123 ), 

2124 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2125 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2126 timezone="UTC", 

2127 ) 

2128 ) 

2129 

2130 

2131def test_can_overlap_other_events_update_regression(db): 

2132 user, token = generate_user() 

2133 

2134 with session_scope() as session: 

2135 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2136 

2137 start = now() 

2138 

2139 with events_session(token) as api: 

2140 # create another event, should be able to overlap with this one 

2141 api.CreateEvent( 

2142 events_pb2.CreateEventReq( 

2143 title="Dummy Title", 

2144 content="Dummy content.", 

2145 parent_community_id=c_id, 

2146 location=events_pb2.EventLocation( 

2147 address="Near Null Island", 

2148 lat=0.1, 

2149 lng=0.2, 

2150 ), 

2151 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2152 end_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2153 timezone="UTC", 

2154 ) 

2155 ) 

2156 

2157 res = api.CreateEvent( 

2158 events_pb2.CreateEventReq( 

2159 title="Dummy Title", 

2160 content="Dummy content.", 

2161 parent_community_id=c_id, 

2162 location=events_pb2.EventLocation( 

2163 address="Near Null Island", 

2164 lat=0.1, 

2165 lng=0.2, 

2166 ), 

2167 start_time=Timestamp_from_datetime(start + timedelta(hours=7)), 

2168 end_time=Timestamp_from_datetime(start + timedelta(hours=8)), 

2169 timezone="UTC", 

2170 ) 

2171 ) 

2172 

2173 event_id = api.ScheduleEvent( 

2174 events_pb2.ScheduleEventReq( 

2175 event_id=res.event_id, 

2176 content="New event occurrence", 

2177 location=events_pb2.EventLocation( 

2178 address="A bit further but still near Null Island", 

2179 lat=0.3, 

2180 lng=0.2, 

2181 ), 

2182 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2183 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2184 timezone="UTC", 

2185 ) 

2186 ).event_id 

2187 

2188 # can overlap with this current existing occurrence 

2189 api.UpdateEvent( 

2190 events_pb2.UpdateEventReq( 

2191 event_id=event_id, 

2192 start_time=Timestamp_from_datetime(start + timedelta(hours=5)), 

2193 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2194 ) 

2195 ) 

2196 

2197 api.UpdateEvent( 

2198 events_pb2.UpdateEventReq( 

2199 event_id=event_id, 

2200 start_time=Timestamp_from_datetime(start + timedelta(hours=2)), 

2201 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2202 ) 

2203 ) 

2204 

2205 

2206def test_list_past_events_regression(db): 

2207 # test for a bug where listing past events didn't work if they didn't have a future occurrence 

2208 user, token = generate_user() 

2209 

2210 with session_scope() as session: 

2211 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2212 

2213 start = now() 

2214 

2215 with events_session(token) as api: 

2216 api.CreateEvent( 

2217 events_pb2.CreateEventReq( 

2218 title="Dummy Title", 

2219 content="Dummy content.", 

2220 parent_community_id=c_id, 

2221 location=events_pb2.EventLocation( 

2222 address="Near Null Island", 

2223 lat=0.1, 

2224 lng=0.2, 

2225 ), 

2226 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2227 end_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2228 timezone="UTC", 

2229 ) 

2230 ) 

2231 

2232 with session_scope() as session: 

2233 session.execute( 

2234 update(EventOccurrence).values( 

2235 during=TimestamptzRange(start + timedelta(hours=-5), start + timedelta(hours=-4)) 

2236 ) 

2237 ) 

2238 

2239 with events_session(token) as api: 

2240 res = api.ListAllEvents(events_pb2.ListAllEventsReq(past=True)) 

2241 assert len(res.events) == 1 

2242 

2243 

2244def test_community_invite_requests(db, email_collector: EmailCollector, moderator: Moderator): 

2245 user1, token1 = generate_user(complete_profile=True) 

2246 user2, token2 = generate_user() 

2247 user3, token3 = generate_user() 

2248 user4, token4 = generate_user() 

2249 user5, token5 = generate_user(is_superuser=True) 

2250 

2251 with session_scope() as session: 

2252 w = create_community(session, 0, 2, "World Community", [user5], [], None) 

2253 mr = create_community(session, 0, 2, "Macroregion", [user5], [], w) 

2254 r = create_community(session, 0, 2, "Region", [user5], [], mr) 

2255 c_id = create_community(session, 0, 2, "Community", [user1, user3, user4], [], r).id 

2256 

2257 enforce_community_memberships() 

2258 

2259 with events_session(token1) as api: 

2260 res = api.CreateEvent( 

2261 events_pb2.CreateEventReq( 

2262 title="Dummy Title", 

2263 content="Dummy content.", 

2264 parent_community_id=c_id, 

2265 location=events_pb2.EventLocation( 

2266 address="Near Null Island", 

2267 lat=0.1, 

2268 lng=0.2, 

2269 ), 

2270 start_time=Timestamp_from_datetime(now() + timedelta(hours=3)), 

2271 end_time=Timestamp_from_datetime(now() + timedelta(hours=4)), 

2272 timezone="UTC", 

2273 ) 

2274 ) 

2275 user_url = f"http://localhost:3000/user/{user1.username}" 

2276 event_url = f"http://localhost:3000/event/{res.event_id}/{res.slug}" 

2277 

2278 event_id = res.event_id 

2279 

2280 moderator.approve_event_occurrence(event_id) 

2281 

2282 with events_session(token1) as api: 

2283 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2284 

2285 email = email_collector.pop_for_mods(last=True) 

2286 

2287 assert user_url in email.plain 

2288 assert event_url in email.plain 

2289 

2290 # can't send another req 

2291 with pytest.raises(grpc.RpcError) as err: 

2292 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2293 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2294 assert err.value.details() == "You have already requested a community invite for this event." 

2295 

2296 # another user can send one though 

2297 with events_session(token3) as api: 

2298 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2299 

2300 # but not a non-admin 

2301 with events_session(token2) as api: 

2302 with pytest.raises(grpc.RpcError) as err: 

2303 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2304 assert err.value.code() == grpc.StatusCode.PERMISSION_DENIED 

2305 assert err.value.details() == "You're not allowed to edit that event." 

2306 

2307 with real_editor_session(token5) as editor: 

2308 res = editor.ListEventCommunityInviteRequests(editor_pb2.ListEventCommunityInviteRequestsReq()) 

2309 assert len(res.requests) == 2 

2310 assert res.requests[0].user_id == user1.id 

2311 assert res.requests[0].approx_users_to_notify == 3 

2312 assert res.requests[1].user_id == user3.id 

2313 assert res.requests[1].approx_users_to_notify == 3 

2314 

2315 editor.DecideEventCommunityInviteRequest( 

2316 editor_pb2.DecideEventCommunityInviteRequestReq( 

2317 event_community_invite_request_id=res.requests[0].event_community_invite_request_id, 

2318 approve=False, 

2319 ) 

2320 ) 

2321 

2322 editor.DecideEventCommunityInviteRequest( 

2323 editor_pb2.DecideEventCommunityInviteRequestReq( 

2324 event_community_invite_request_id=res.requests[1].event_community_invite_request_id, 

2325 approve=True, 

2326 ) 

2327 ) 

2328 

2329 # not after approve 

2330 with events_session(token4) as api: 

2331 with pytest.raises(grpc.RpcError) as err: 

2332 api.RequestCommunityInvite(events_pb2.RequestCommunityInviteReq(event_id=event_id)) 

2333 assert err.value.code() == grpc.StatusCode.FAILED_PRECONDITION 

2334 assert err.value.details() == "A community invite has already been sent out for this event." 

2335 

2336 

2337def test_update_event_should_notify_queues_job(): 

2338 user, token = generate_user() 

2339 start = now() 

2340 

2341 with session_scope() as session: 

2342 c_id = create_community(session, 0, 2, "Community", [user], [], None).id 

2343 

2344 # create an event 

2345 with events_session(token) as api: 

2346 create_res = api.CreateEvent( 

2347 events_pb2.CreateEventReq( 

2348 title="Dummy Title", 

2349 content="Dummy content.", 

2350 parent_community_id=c_id, 

2351 location=events_pb2.EventLocation( 

2352 address="Near Null Island", 

2353 lat=1.0, 

2354 lng=2.0, 

2355 ), 

2356 start_time=Timestamp_from_datetime(start + timedelta(hours=3)), 

2357 end_time=Timestamp_from_datetime(start + timedelta(hours=6)), 

2358 timezone="UTC", 

2359 ) 

2360 ) 

2361 

2362 event_id = create_res.event_id 

2363 

2364 # measure initial background job queue length 

2365 with session_scope() as session: 

2366 jobs = session.query(BackgroundJob).all() 

2367 job_length_before_update = len(jobs) 

2368 

2369 # update with should_notify=False, expect no change in background job queue 

2370 api.UpdateEvent( 

2371 events_pb2.UpdateEventReq( 

2372 event_id=event_id, 

2373 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2374 should_notify=False, 

2375 ) 

2376 ) 

2377 

2378 with session_scope() as session: 

2379 jobs = session.query(BackgroundJob).all() 

2380 assert len(jobs) == job_length_before_update 

2381 

2382 # update with should_notify=True, expect one new background job added 

2383 api.UpdateEvent( 

2384 events_pb2.UpdateEventReq( 

2385 event_id=event_id, 

2386 start_time=Timestamp_from_datetime(start + timedelta(hours=4)), 

2387 should_notify=True, 

2388 ) 

2389 ) 

2390 

2391 with session_scope() as session: 

2392 jobs = session.query(BackgroundJob).all() 

2393 assert len(jobs) == job_length_before_update + 1 

2394 

2395 

2396def test_event_photo_key(db): 

2397 """Test that events return the photo_key field when a photo is set.""" 

2398 user, token = generate_user() 

2399 

2400 start_time = now() + timedelta(hours=2) 

2401 end_time = start_time + timedelta(hours=3) 

2402 

2403 # Create a community and an upload for the event photo 

2404 with session_scope() as session: 

2405 create_community(session, 0, 2, "Community", [user], [], None) 

2406 upload = Upload( 

2407 key="test_event_photo_key_123", 

2408 filename="test_event_photo_key_123.jpg", 

2409 creator_user_id=user.id, 

2410 ) 

2411 session.add(upload) 

2412 

2413 with events_session(token) as api: 

2414 # Create event without photo 

2415 res = api.CreateEvent( 

2416 events_pb2.CreateEventReq( 

2417 title="Event Without Photo", 

2418 content="No photo content.", 

2419 photo_key=None, 

2420 location=events_pb2.EventLocation( 

2421 address="Near Null Island", 

2422 lat=0.1, 

2423 lng=0.2, 

2424 ), 

2425 start_time=Timestamp_from_datetime(start_time), 

2426 end_time=Timestamp_from_datetime(end_time), 

2427 timezone="UTC", 

2428 ) 

2429 ) 

2430 

2431 assert res.photo_key == "" 

2432 assert res.photo_url == "" 

2433 

2434 # Create event with photo 

2435 res_with_photo = api.CreateEvent( 

2436 events_pb2.CreateEventReq( 

2437 title="Event With Photo", 

2438 content="Has photo content.", 

2439 photo_key="test_event_photo_key_123", 

2440 location=events_pb2.EventLocation( 

2441 address="Near Null Island", 

2442 lat=0.1, 

2443 lng=0.2, 

2444 ), 

2445 start_time=Timestamp_from_datetime(start_time + timedelta(days=1)), 

2446 end_time=Timestamp_from_datetime(end_time + timedelta(days=1)), 

2447 timezone="UTC", 

2448 ) 

2449 ) 

2450 

2451 assert res_with_photo.photo_key == "test_event_photo_key_123" 

2452 assert "test_event_photo_key_123" in res_with_photo.photo_url 

2453 

2454 event_id = res_with_photo.event_id 

2455 

2456 # Verify photo_key is returned when getting the event 

2457 get_res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2458 assert get_res.photo_key == "test_event_photo_key_123" 

2459 assert "test_event_photo_key_123" in get_res.photo_url 

2460 

2461 

2462def test_event_created_with_shadowed_visibility(db): 

2463 """Events start in SHADOWED state when created.""" 

2464 user, token = generate_user() 

2465 

2466 with session_scope() as session: 

2467 create_community(session, 0, 2, "Community", [user], [], None) 

2468 

2469 start_time = now() + timedelta(hours=2) 

2470 end_time = start_time + timedelta(hours=3) 

2471 

2472 with events_session(token) as api: 

2473 res = api.CreateEvent( 

2474 events_pb2.CreateEventReq( 

2475 title="Test UMS Event", 

2476 content="UMS content.", 

2477 location=events_pb2.EventLocation( 

2478 address="Near Null Island", 

2479 lat=0.1, 

2480 lng=0.2, 

2481 ), 

2482 start_time=Timestamp_from_datetime(start_time), 

2483 end_time=Timestamp_from_datetime(end_time), 

2484 timezone="UTC", 

2485 ) 

2486 ) 

2487 event_id = res.event_id 

2488 

2489 with session_scope() as session: 

2490 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2491 mod_state = session.execute( 

2492 select(ModerationState).where(ModerationState.id == occurrence.moderation_state_id) 

2493 ).scalar_one() 

2494 assert mod_state.visibility == ModerationVisibility.shadowed 

2495 

2496 

2497def test_shadowed_event_visible_to_creator_only(db): 

2498 """SHADOWED events are visible to the creator but not to other users.""" 

2499 user1, token1 = generate_user() 

2500 user2, token2 = generate_user() 

2501 

2502 with session_scope() as session: 

2503 create_community(session, 0, 2, "Community", [user1], [], None) 

2504 

2505 start_time = now() + timedelta(hours=2) 

2506 end_time = start_time + timedelta(hours=3) 

2507 

2508 with events_session(token1) as api: 

2509 res = api.CreateEvent( 

2510 events_pb2.CreateEventReq( 

2511 title="Shadowed Event", 

2512 content="Content.", 

2513 location=events_pb2.EventLocation( 

2514 address="Near Null Island", 

2515 lat=0.1, 

2516 lng=0.2, 

2517 ), 

2518 start_time=Timestamp_from_datetime(start_time), 

2519 end_time=Timestamp_from_datetime(end_time), 

2520 timezone="UTC", 

2521 ) 

2522 ) 

2523 event_id = res.event_id 

2524 

2525 # Creator can see it 

2526 with events_session(token1) as api: 

2527 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2528 assert res.title == "Shadowed Event" 

2529 

2530 # Other user cannot 

2531 with events_session(token2) as api: 

2532 with pytest.raises(grpc.RpcError) as e: 

2533 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2534 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2535 

2536 

2537def test_event_visible_after_approval(db, moderator: Moderator): 

2538 """Events become visible to all users after moderation approval.""" 

2539 user1, token1 = generate_user() 

2540 user2, token2 = generate_user() 

2541 

2542 with session_scope() as session: 

2543 create_community(session, 0, 2, "Community", [user1], [], None) 

2544 

2545 start_time = now() + timedelta(hours=2) 

2546 end_time = start_time + timedelta(hours=3) 

2547 

2548 with events_session(token1) as api: 

2549 res = api.CreateEvent( 

2550 events_pb2.CreateEventReq( 

2551 title="Approved Event", 

2552 content="Content.", 

2553 location=events_pb2.EventLocation( 

2554 address="Near Null Island", 

2555 lat=0.1, 

2556 lng=0.2, 

2557 ), 

2558 start_time=Timestamp_from_datetime(start_time), 

2559 end_time=Timestamp_from_datetime(end_time), 

2560 timezone="UTC", 

2561 ) 

2562 ) 

2563 event_id = res.event_id 

2564 

2565 # Other user cannot see it yet 

2566 with events_session(token2) as api: 

2567 with pytest.raises(grpc.RpcError) as e: 

2568 api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2569 assert e.value.code() == grpc.StatusCode.NOT_FOUND 

2570 

2571 # Approve the event 

2572 moderator.approve_event_occurrence(event_id) 

2573 

2574 # Now other user can see it 

2575 with events_session(token2) as api: 

2576 res = api.GetEvent(events_pb2.GetEventReq(event_id=event_id)) 

2577 assert res.title == "Approved Event" 

2578 

2579 

2580def test_shadowed_event_hidden_from_list_for_non_creator(db, moderator: Moderator): 

2581 """SHADOWED events appear in lists for the creator but not for other users.""" 

2582 user1, token1 = generate_user() 

2583 user2, token2 = generate_user() 

2584 

2585 with session_scope() as session: 

2586 create_community(session, 0, 2, "Community", [user1], [], None) 

2587 

2588 start_time = now() + timedelta(hours=2) 

2589 end_time = start_time + timedelta(hours=3) 

2590 

2591 with events_session(token1) as api: 

2592 res = api.CreateEvent( 

2593 events_pb2.CreateEventReq( 

2594 title="List Test Event", 

2595 content="Content.", 

2596 location=events_pb2.EventLocation( 

2597 address="Near Null Island", 

2598 lat=0.1, 

2599 lng=0.2, 

2600 ), 

2601 start_time=Timestamp_from_datetime(start_time), 

2602 end_time=Timestamp_from_datetime(end_time), 

2603 timezone="UTC", 

2604 ) 

2605 ) 

2606 event_id = res.event_id 

2607 

2608 # Creator can see their own SHADOWED event in lists 

2609 with events_session(token1) as api: 

2610 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2611 event_ids = [e.event_id for e in list_res.events] 

2612 assert event_id in event_ids 

2613 

2614 # Other user cannot see the SHADOWED event in lists 

2615 with events_session(token2) as api: 

2616 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2617 event_ids = [e.event_id for e in list_res.events] 

2618 assert event_id not in event_ids 

2619 

2620 # After approval, other user can see it 

2621 moderator.approve_event_occurrence(event_id) 

2622 

2623 with events_session(token2) as api: 

2624 list_res = api.ListAllEvents(events_pb2.ListAllEventsReq()) 

2625 event_ids = [e.event_id for e in list_res.events] 

2626 assert event_id in event_ids 

2627 

2628 

2629def test_event_create_notification_deferred_until_approval(db, push_collector: PushCollector, moderator: Moderator): 

2630 """Event create notifications are deferred while SHADOWED, then unblocked after approval.""" 

2631 user1, token1 = generate_user() 

2632 user2, token2 = generate_user() 

2633 

2634 # Need world -> macroregion -> region -> subregion so the subregion community gets notifications 

2635 with session_scope() as session: 

2636 world = create_community(session, 0, 10, "World", [user1], [], None) 

2637 macroregion = create_community(session, 0, 7, "Macroregion", [user1], [], world) 

2638 region = create_community(session, 0, 5, "Region", [user1], [], macroregion) 

2639 create_community(session, 0, 2, "Child", [user2], [], region) 

2640 

2641 start_time = now() + timedelta(hours=2) 

2642 end_time = start_time + timedelta(hours=3) 

2643 

2644 with events_session(token1) as api: 

2645 res = api.CreateEvent( 

2646 events_pb2.CreateEventReq( 

2647 title="Deferred Event", 

2648 content="Content.", 

2649 location=events_pb2.EventLocation( 

2650 address="Near Null Island", 

2651 lat=0.1, 

2652 lng=0.2, 

2653 ), 

2654 start_time=Timestamp_from_datetime(start_time), 

2655 end_time=Timestamp_from_datetime(end_time), 

2656 timezone="UTC", 

2657 ) 

2658 ) 

2659 event_id = res.event_id 

2660 

2661 # Process all jobs — notification should be deferred (event is SHADOWED) 

2662 process_jobs() 

2663 

2664 with session_scope() as session: 

2665 notif = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalar_one() 

2666 # Notification was created with moderation_state_id for deferral 

2667 assert notif.moderation_state_id is not None 

2668 # No delivery exists (deferred because event is SHADOWED) 

2669 delivery_count = session.execute( 

2670 select(NotificationDelivery).where(NotificationDelivery.notification_id == notif.id) 

2671 ).scalar_one_or_none() 

2672 assert delivery_count is None 

2673 

2674 # Approve the event — handle_notification is re-queued for deferred notifications 

2675 moderator.approve_event_occurrence(event_id) 

2676 

2677 # Verify handle_notification job was queued 

2678 with session_scope() as session: 

2679 pending_jobs = ( 

2680 session.execute(select(BackgroundJob).where(BackgroundJob.state == BackgroundJobState.pending)) 

2681 .scalars() 

2682 .all() 

2683 ) 

2684 assert any("handle_notification" in j.job_type for j in pending_jobs) 

2685 

2686 

2687def test_event_update_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2688 """Event update notifications should carry the event's moderation_state_id for deferral.""" 

2689 user1, token1 = generate_user() 

2690 user2, token2 = generate_user() 

2691 

2692 with session_scope() as session: 

2693 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2694 

2695 start_time = now() + timedelta(hours=2) 

2696 end_time = start_time + timedelta(hours=3) 

2697 

2698 with events_session(token1) as api: 

2699 res = api.CreateEvent( 

2700 events_pb2.CreateEventReq( 

2701 title="Update Test", 

2702 content="Content.", 

2703 location=events_pb2.EventLocation( 

2704 address="Near Null Island", 

2705 lat=0.1, 

2706 lng=0.2, 

2707 ), 

2708 start_time=Timestamp_from_datetime(start_time), 

2709 end_time=Timestamp_from_datetime(end_time), 

2710 timezone="UTC", 

2711 ) 

2712 ) 

2713 event_id = res.event_id 

2714 

2715 moderator.approve_event_occurrence(event_id) 

2716 process_jobs() 

2717 # Clear any create notifications 

2718 while push_collector.count_for_user(user2.id): 2718 ↛ 2719line 2718 didn't jump to line 2719 because the condition on line 2718 was never true

2719 push_collector.pop_for_user(user2.id) 

2720 

2721 # User2 subscribes to the event 

2722 with events_session(token2) as api: 

2723 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

2724 

2725 # User1 updates the event with should_notify=True 

2726 with events_session(token1) as api: 

2727 api.UpdateEvent( 

2728 events_pb2.UpdateEventReq( 

2729 event_id=event_id, 

2730 title=wrappers_pb2.StringValue(value="Updated Title"), 

2731 should_notify=True, 

2732 ) 

2733 ) 

2734 

2735 process_jobs() 

2736 

2737 # Verify that the update notification for user2 has moderation_state_id set 

2738 with session_scope() as session: 

2739 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2740 

2741 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2742 # Find the update notification (most recent one) 

2743 update_notifs = [n for n in notifications if n.topic_action.action == "update"] 

2744 assert len(update_notifs) == 1 

2745 assert update_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2746 

2747 

2748def test_event_cancel_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2749 """Event cancel notifications should carry the event's moderation_state_id for deferral.""" 

2750 user1, token1 = generate_user() 

2751 user2, token2 = generate_user() 

2752 

2753 with session_scope() as session: 

2754 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2755 

2756 start_time = now() + timedelta(hours=2) 

2757 end_time = start_time + timedelta(hours=3) 

2758 

2759 with events_session(token1) as api: 

2760 res = api.CreateEvent( 

2761 events_pb2.CreateEventReq( 

2762 title="Cancel Test", 

2763 content="Content.", 

2764 location=events_pb2.EventLocation( 

2765 address="Near Null Island", 

2766 lat=0.1, 

2767 lng=0.2, 

2768 ), 

2769 start_time=Timestamp_from_datetime(start_time), 

2770 end_time=Timestamp_from_datetime(end_time), 

2771 timezone="UTC", 

2772 ) 

2773 ) 

2774 event_id = res.event_id 

2775 

2776 moderator.approve_event_occurrence(event_id) 

2777 process_jobs() 

2778 while push_collector.count_for_user(user2.id): 2778 ↛ 2779line 2778 didn't jump to line 2779 because the condition on line 2778 was never true

2779 push_collector.pop_for_user(user2.id) 

2780 

2781 # User2 subscribes 

2782 with events_session(token2) as api: 

2783 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

2784 

2785 # User1 cancels the event 

2786 with events_session(token1) as api: 

2787 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id)) 

2788 

2789 process_jobs() 

2790 

2791 # Verify that the cancel notification for user2 has moderation_state_id set 

2792 with session_scope() as session: 

2793 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2794 

2795 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2796 cancel_notifs = [n for n in notifications if n.topic_action.action == "cancel"] 

2797 assert len(cancel_notifs) == 1 

2798 assert cancel_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2799 

2800 

2801def test_event_reminder_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

2802 """Event reminder notifications should carry the event's moderation_state_id for deferral.""" 

2803 user1, token1 = generate_user() 

2804 user2, token2 = generate_user() 

2805 

2806 with session_scope() as session: 

2807 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

2808 

2809 # Create event starting 23 hours from now (within 24h reminder window) 

2810 start_time = now() + timedelta(hours=23) 

2811 end_time = start_time + timedelta(hours=1) 

2812 

2813 with events_session(token1) as api: 

2814 res = api.CreateEvent( 

2815 events_pb2.CreateEventReq( 

2816 title="Reminder Test", 

2817 content="Content.", 

2818 location=events_pb2.EventLocation( 

2819 address="Near Null Island", 

2820 lat=0.1, 

2821 lng=0.2, 

2822 ), 

2823 start_time=Timestamp_from_datetime(start_time), 

2824 end_time=Timestamp_from_datetime(end_time), 

2825 timezone="UTC", 

2826 ) 

2827 ) 

2828 event_id = res.event_id 

2829 

2830 moderator.approve_event_occurrence(event_id) 

2831 process_jobs() 

2832 while push_collector.count_for_user(user2.id): 2832 ↛ 2833line 2832 didn't jump to line 2833 because the condition on line 2832 was never true

2833 push_collector.pop_for_user(user2.id) 

2834 

2835 # User2 marks attendance 

2836 with events_session(token2) as api: 

2837 api.SetEventAttendance( 

2838 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2839 ) 

2840 

2841 # Run the event reminder handler 

2842 send_event_reminders(empty_pb2.Empty()) 

2843 process_jobs() 

2844 

2845 # Verify that the reminder notification for user2 has moderation_state_id set 

2846 with session_scope() as session: 

2847 occurrence = session.execute(select(EventOccurrence).where(EventOccurrence.id == event_id)).scalar_one() 

2848 

2849 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2850 reminder_notifs = [n for n in notifications if n.topic_action.action == "reminder"] 

2851 assert len(reminder_notifs) == 1 

2852 assert reminder_notifs[0].moderation_state_id == occurrence.moderation_state_id 

2853 

2854 

2855def test_event_reminder_not_sent_for_cancelled_event(db, push_collector: PushCollector, moderator: Moderator): 

2856 """Event reminders should not be sent for cancelled events.""" 

2857 user1, token1 = generate_user() 

2858 user2, token2 = generate_user() 

2859 

2860 with session_scope() as session: 

2861 create_community(session, 0, 2, "Community", [user2], [], None) 

2862 

2863 # Create event starting 23 hours from now (within 24h reminder window) 

2864 start_time = now() + timedelta(hours=23) 

2865 end_time = start_time + timedelta(hours=1) 

2866 

2867 with events_session(token1) as api: 

2868 res = api.CreateEvent( 

2869 events_pb2.CreateEventReq( 

2870 title="Cancelled Reminder Test", 

2871 content="Content.", 

2872 location=events_pb2.EventLocation( 

2873 address="Near Null Island", 

2874 lat=0.1, 

2875 lng=0.2, 

2876 ), 

2877 start_time=Timestamp_from_datetime(start_time), 

2878 end_time=Timestamp_from_datetime(end_time), 

2879 timezone="UTC", 

2880 ) 

2881 ) 

2882 event_id = res.event_id 

2883 

2884 moderator.approve_event_occurrence(event_id) 

2885 process_jobs() 

2886 

2887 # User2 marks attendance 

2888 with events_session(token2) as api: 

2889 api.SetEventAttendance( 

2890 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2891 ) 

2892 

2893 # User1 cancels the event 

2894 with events_session(token1) as api: 

2895 api.CancelEvent(events_pb2.CancelEventReq(event_id=event_id)) 

2896 

2897 process_jobs() 

2898 # Drain any cancellation-related notifications so we can cleanly assert on reminders 

2899 while push_collector.count_for_user(user2.id): 

2900 push_collector.pop_for_user(user2.id) 

2901 

2902 # Run the event reminder handler 

2903 send_event_reminders(empty_pb2.Empty()) 

2904 process_jobs() 

2905 

2906 # Verify that no reminder notification was sent for user2 

2907 with session_scope() as session: 

2908 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2909 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder] 

2910 assert len(reminder_notifs) == 0 

2911 

2912 

2913@pytest.mark.parametrize("invisible_field", ["deleted_at", "banned_at", "shadowed_at"]) 

2914def test_event_reminder_not_sent_for_invisible_attendee( 

2915 db, push_collector: PushCollector, moderator: Moderator, invisible_field 

2916): 

2917 user1, token1 = generate_user() 

2918 user2, token2 = generate_user() 

2919 

2920 with session_scope() as session: 

2921 create_community(session, 0, 2, "Community", [user2], [], None) 

2922 

2923 start_time = now() + timedelta(hours=23) 

2924 end_time = start_time + timedelta(hours=1) 

2925 

2926 with events_session(token1) as api: 

2927 res = api.CreateEvent( 

2928 events_pb2.CreateEventReq( 

2929 title="Invisible Attendee Reminder Test", 

2930 content="Content.", 

2931 location=events_pb2.EventLocation( 

2932 address="Near Null Island", 

2933 lat=0.1, 

2934 lng=0.2, 

2935 ), 

2936 start_time=Timestamp_from_datetime(start_time), 

2937 end_time=Timestamp_from_datetime(end_time), 

2938 timezone="UTC", 

2939 ) 

2940 ) 

2941 event_id = res.event_id 

2942 

2943 moderator.approve_event_occurrence(event_id) 

2944 process_jobs() 

2945 

2946 with events_session(token2) as api: 

2947 api.SetEventAttendance( 

2948 events_pb2.SetEventAttendanceReq(event_id=event_id, attendance_state=events_pb2.ATTENDANCE_STATE_GOING) 

2949 ) 

2950 

2951 with session_scope() as session: 

2952 session.execute(update(User).where(User.id == user2.id).values({invisible_field: now()})) 

2953 

2954 send_event_reminders(empty_pb2.Empty()) 

2955 process_jobs() 

2956 

2957 with session_scope() as session: 

2958 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

2959 reminder_notifs = [n for n in notifications if n.topic_action == NotificationTopicAction.event__reminder] 

2960 assert len(reminder_notifs) == 0 

2961 

2962 

2963def test_ListEventOccurrences_does_not_leak_other_events(db, moderator: Moderator): 

2964 """ListEventOccurrences should only return occurrences for the requested event, not other events.""" 

2965 user1, token1 = generate_user() 

2966 user2, token2 = generate_user() 

2967 

2968 with session_scope() as session: 

2969 c_id = create_community(session, 0, 2, "Community", [user1, user2], [], None).id 

2970 

2971 start = now() 

2972 

2973 # User1 creates event A with 3 occurrences 

2974 event_a_ids = [] 

2975 with events_session(token1) as api: 

2976 res = api.CreateEvent( 

2977 events_pb2.CreateEventReq( 

2978 title="Event A", 

2979 content="Content A.", 

2980 parent_community_id=c_id, 

2981 location=events_pb2.EventLocation( 

2982 address="Near Null Island", 

2983 lat=0.1, 

2984 lng=0.2, 

2985 ), 

2986 start_time=Timestamp_from_datetime(start + timedelta(hours=1)), 

2987 end_time=Timestamp_from_datetime(start + timedelta(hours=1.5)), 

2988 timezone="UTC", 

2989 ) 

2990 ) 

2991 event_a_ids.append(res.event_id) 

2992 for i in range(2): 

2993 res = api.ScheduleEvent( 

2994 events_pb2.ScheduleEventReq( 

2995 event_id=event_a_ids[-1], 

2996 content=f"A occurrence {i}", 

2997 location=events_pb2.EventLocation( 

2998 address="Near Null Island", 

2999 lat=0.1, 

3000 lng=0.2, 

3001 ), 

3002 start_time=Timestamp_from_datetime(start + timedelta(hours=2 + i)), 

3003 end_time=Timestamp_from_datetime(start + timedelta(hours=2.5 + i)), 

3004 timezone="UTC", 

3005 ) 

3006 ) 

3007 event_a_ids.append(res.event_id) 

3008 

3009 # User2 creates event B with 2 occurrences 

3010 event_b_ids = [] 

3011 with events_session(token2) as api: 

3012 res = api.CreateEvent( 

3013 events_pb2.CreateEventReq( 

3014 title="Event B", 

3015 content="Content B.", 

3016 parent_community_id=c_id, 

3017 location=events_pb2.EventLocation( 

3018 address="Near Null Island", 

3019 lat=0.1, 

3020 lng=0.2, 

3021 ), 

3022 start_time=Timestamp_from_datetime(start + timedelta(hours=10)), 

3023 end_time=Timestamp_from_datetime(start + timedelta(hours=10.5)), 

3024 timezone="UTC", 

3025 ) 

3026 ) 

3027 event_b_ids.append(res.event_id) 

3028 res = api.ScheduleEvent( 

3029 events_pb2.ScheduleEventReq( 

3030 event_id=event_b_ids[-1], 

3031 content="B occurrence 1", 

3032 location=events_pb2.EventLocation( 

3033 address="Near Null Island", 

3034 lat=0.1, 

3035 lng=0.2, 

3036 ), 

3037 start_time=Timestamp_from_datetime(start + timedelta(hours=11)), 

3038 end_time=Timestamp_from_datetime(start + timedelta(hours=11.5)), 

3039 timezone="UTC", 

3040 ) 

3041 ) 

3042 event_b_ids.append(res.event_id) 

3043 

3044 moderator.approve_event_occurrence(event_a_ids[0]) 

3045 moderator.approve_event_occurrence(event_b_ids[0]) 

3046 

3047 # List occurrences for event A — should only get event A's 3 occurrences 

3048 with events_session(token1) as api: 

3049 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_a_ids[-1])) 

3050 returned_ids = [e.event_id for e in res.events] 

3051 assert sorted(returned_ids) == sorted(event_a_ids) 

3052 

3053 # List occurrences for event B — should only get event B's 2 occurrences 

3054 with events_session(token2) as api: 

3055 res = api.ListEventOccurrences(events_pb2.ListEventOccurrencesReq(event_id=event_b_ids[-1])) 

3056 returned_ids = [e.event_id for e in res.events] 

3057 assert sorted(returned_ids) == sorted(event_b_ids) 

3058 

3059 

3060def test_event_comment_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

3061 """Event comment notifications should carry the comment's moderation_state_id for deferral.""" 

3062 user1, token1 = generate_user() 

3063 user2, token2 = generate_user() 

3064 

3065 with session_scope() as session: 

3066 c_id = create_community(session, 0, 2, "Community", [user2], [], None).id 

3067 

3068 start_time = now() + timedelta(hours=2) 

3069 end_time = start_time + timedelta(hours=3) 

3070 

3071 with events_session(token1) as api: 

3072 res = api.CreateEvent( 

3073 events_pb2.CreateEventReq( 

3074 title="Comment Test", 

3075 content="Content.", 

3076 parent_community_id=c_id, 

3077 location=events_pb2.EventLocation( 

3078 address="Near Null Island", 

3079 lat=0.1, 

3080 lng=0.2, 

3081 ), 

3082 start_time=Timestamp_from_datetime(start_time), 

3083 end_time=Timestamp_from_datetime(end_time), 

3084 timezone="UTC", 

3085 ) 

3086 ) 

3087 event_id = res.event_id 

3088 thread_id = res.thread.thread_id 

3089 

3090 moderator.approve_event_occurrence(event_id) 

3091 process_jobs() 

3092 while push_collector.count_for_user(user1.id): 3092 ↛ 3093line 3092 didn't jump to line 3093 because the condition on line 3092 was never true

3093 push_collector.pop_for_user(user1.id) 

3094 

3095 # User1 subscribes (creator is auto-subscribed, but let's be explicit) 

3096 with events_session(token1) as api: 

3097 api.SetEventSubscription(events_pb2.SetEventSubscriptionReq(event_id=event_id, subscribe=True)) 

3098 

3099 # User2 posts a top-level comment on the event thread 

3100 with threads_session(token2) as api: 

3101 comment_thread_id = api.PostReply( 

3102 threads_pb2.PostReplyReq(thread_id=thread_id, content="Hello event!") 

3103 ).thread_id 

3104 

3105 process_jobs() 

3106 

3107 # The comment notification for user1 should be gated on the comment's own moderation_state_id 

3108 comment_db_id = comment_thread_id // 10 

3109 with session_scope() as session: 

3110 comment = session.execute(select(Comment).where(Comment.id == comment_db_id)).scalar_one() 

3111 

3112 notifications = session.execute(select(Notification).where(Notification.user_id == user1.id)).scalars().all() 

3113 comment_notifs = [n for n in notifications if n.topic_action.action == "comment"] 

3114 assert len(comment_notifs) == 1 

3115 assert comment_notifs[0].moderation_state_id == comment.moderation_state_id 

3116 

3117 

3118def test_event_thread_reply_notification_has_moderation_state(db, push_collector: PushCollector, moderator: Moderator): 

3119 """Event thread reply notifications should carry the reply's moderation_state_id for deferral.""" 

3120 user1, token1 = generate_user() 

3121 user2, token2 = generate_user() 

3122 user3, token3 = generate_user() 

3123 

3124 with session_scope() as session: 

3125 c_id = create_community(session, 0, 2, "Community", [user2, user3], [], None).id 

3126 

3127 start_time = now() + timedelta(hours=2) 

3128 end_time = start_time + timedelta(hours=3) 

3129 

3130 with events_session(token1) as api: 

3131 res = api.CreateEvent( 

3132 events_pb2.CreateEventReq( 

3133 title="Reply Test", 

3134 content="Content.", 

3135 location=events_pb2.EventLocation( 

3136 address="Near Null Island", 

3137 lat=0.1, 

3138 lng=0.2, 

3139 ), 

3140 start_time=Timestamp_from_datetime(start_time), 

3141 end_time=Timestamp_from_datetime(end_time), 

3142 timezone="UTC", 

3143 ) 

3144 ) 

3145 event_id = res.event_id 

3146 thread_id = res.thread.thread_id 

3147 

3148 moderator.approve_event_occurrence(event_id) 

3149 process_jobs() 

3150 while push_collector.count_for_user(user1.id): 3150 ↛ 3151line 3150 didn't jump to line 3151 because the condition on line 3150 was never true

3151 push_collector.pop_for_user(user1.id) 

3152 

3153 # User2 posts a top-level comment 

3154 with threads_session(token2) as api: 

3155 comment_thread_id = api.PostReply( 

3156 threads_pb2.PostReplyReq(thread_id=thread_id, content="Top-level comment") 

3157 ).thread_id 

3158 

3159 process_jobs() 

3160 while push_collector.count_for_user(user1.id): 3160 ↛ 3161line 3160 didn't jump to line 3161 because the condition on line 3160 was never true

3161 push_collector.pop_for_user(user1.id) 

3162 

3163 # User3 replies to user2's comment (depth=2 reply) 

3164 with threads_session(token3) as api: 

3165 nested_reply_thread_id = api.PostReply( 

3166 threads_pb2.PostReplyReq(thread_id=comment_thread_id, content="Nested reply") 

3167 ).thread_id 

3168 

3169 process_jobs() 

3170 

3171 # The nested reply notification for user2 should be gated on the reply's own moderation_state_id 

3172 nested_reply_db_id = nested_reply_thread_id // 10 

3173 with session_scope() as session: 

3174 nested_reply = session.execute(select(Reply).where(Reply.id == nested_reply_db_id)).scalar_one() 

3175 

3176 notifications = session.execute(select(Notification).where(Notification.user_id == user2.id)).scalars().all() 

3177 reply_notifs = [n for n in notifications if n.topic_action.action == "reply"] 

3178 assert len(reply_notifs) == 1 

3179 assert reply_notifs[0].moderation_state_id == nested_reply.moderation_state_id