From 6e438dbfdc03768ca75b45040883fd8c998aecce Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 22 Feb 2026 22:28:18 +0000 Subject: [PATCH] Add upcoming_entries_for_container to CalendarService New paginated query for upcoming confirmed entries across all calendars belonging to a container (page). Used by the events page summary view. Co-Authored-By: Claude Opus 4.6 --- contracts/protocols.py | 5 +++++ services/calendar_impl.py | 29 +++++++++++++++++++++++++++++ services/stubs.py | 3 +++ 3 files changed, 37 insertions(+) diff --git a/contracts/protocols.py b/contracts/protocols.py index b27b870..0f6b7c7 100644 --- a/contracts/protocols.py +++ b/contracts/protocols.py @@ -129,6 +129,11 @@ class CalendarService(Protocol): self, session: AsyncSession, content_type: str, content_id: int, ) -> set[int]: ... + async def upcoming_entries_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[CalendarEntryDTO], bool]: ... + async def visible_entries_for_period( self, session: AsyncSession, calendar_id: int, period_start: datetime, period_end: datetime, diff --git a/services/calendar_impl.py b/services/calendar_impl.py index f79e1ab..b6159b8 100644 --- a/services/calendar_impl.py +++ b/services/calendar_impl.py @@ -239,6 +239,35 @@ class SqlCalendarService: merged = sorted(entries_by_id.values(), key=lambda e: e.start_at or period_start) return [_entry_to_dto(e) for e in merged] + async def upcoming_entries_for_container( + self, session: AsyncSession, container_type: str, container_id: int, + *, page: int = 1, per_page: int = 20, + ) -> tuple[list[CalendarEntryDTO], bool]: + """Upcoming confirmed entries across all calendars for a container.""" + cal_ids = select(Calendar.id).where( + Calendar.container_type == container_type, + Calendar.container_id == container_id, + Calendar.deleted_at.is_(None), + ).scalar_subquery() + + offset = (page - 1) * per_page + result = await session.execute( + select(CalendarEntry) + .where( + CalendarEntry.calendar_id.in_(cal_ids), + CalendarEntry.state == "confirmed", + CalendarEntry.deleted_at.is_(None), + CalendarEntry.start_at >= func.now(), + ) + .order_by(CalendarEntry.start_at.asc()) + .limit(per_page) + .offset(offset) + .options(selectinload(CalendarEntry.calendar)) + ) + entries = result.scalars().all() + has_more = len(entries) == per_page + return [_entry_to_dto(e) for e in entries], has_more + async def associated_entries( self, session: AsyncSession, content_type: str, content_id: int, page: int, ) -> tuple[list[CalendarEntryDTO], bool]: diff --git a/services/stubs.py b/services/stubs.py index 38b31f4..5dd36ba 100644 --- a/services/stubs.py +++ b/services/stubs.py @@ -140,6 +140,9 @@ class StubCalendarService: ) -> int: return 0 + async def upcoming_entries_for_container(self, session, container_type, container_id, *, page=1, per_page=20): + return [], False + async def entry_ids_for_content(self, session, content_type, content_id): return set()