Compare commits

...

3 Commits

Author SHA1 Message Date
giles
b16ba34b40 Add list_marketplaces to MarketService protocol, impl, and stub
Paginated query for market listings — supports optional container filtering
and returns (dtos, has_more) for infinite scroll.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:29:14 +00:00
giles
16e4d3aa57 Make upcoming_entries_for_container work without container filter
When container_type/container_id are None, returns all upcoming
confirmed entries across all calendars (for global event listings).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:04:55 +00:00
giles
6e438dbfdc 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 <noreply@anthropic.com>
2026-02-22 22:28:18 +00:00
4 changed files with 78 additions and 0 deletions

View File

@@ -129,6 +129,12 @@ 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 | None = None, container_id: int | None = None,
*, 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,
@@ -149,6 +155,12 @@ class MarketService(Protocol):
name: str, slug: str,
) -> MarketPlaceDTO: ...
async def list_marketplaces(
self, session: AsyncSession,
container_type: str | None = None, container_id: int | None = None,
*, page: int = 1, per_page: int = 20,
) -> tuple[list[MarketPlaceDTO], bool]: ...
async def soft_delete_marketplace(
self, session: AsyncSession, container_type: str, container_id: int,
slug: str,

View File

@@ -239,6 +239,45 @@ 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 | None = None, container_id: int | None = None,
*, page: int = 1, per_page: int = 20,
) -> tuple[list[CalendarEntryDTO], bool]:
"""Upcoming confirmed entries. Optionally scoped to a container."""
filters = [
CalendarEntry.state == "confirmed",
CalendarEntry.deleted_at.is_(None),
CalendarEntry.start_at >= func.now(),
]
if container_type is not None and container_id is not None:
cal_ids = select(Calendar.id).where(
Calendar.container_type == container_type,
Calendar.container_id == container_id,
Calendar.deleted_at.is_(None),
).scalar_subquery()
filters.append(CalendarEntry.calendar_id.in_(cal_ids))
else:
# Still exclude entries from deleted calendars
cal_ids = select(Calendar.id).where(
Calendar.deleted_at.is_(None),
).scalar_subquery()
filters.append(CalendarEntry.calendar_id.in_(cal_ids))
offset = (page - 1) * per_page
result = await session.execute(
select(CalendarEntry)
.where(*filters)
.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]:

View File

@@ -52,6 +52,23 @@ class SqlMarketService:
)
return [_mp_to_dto(mp) for mp in result.scalars().all()]
async def list_marketplaces(
self, session: AsyncSession,
container_type: str | None = None, container_id: int | None = None,
*, page: int = 1, per_page: int = 20,
) -> tuple[list[MarketPlaceDTO], bool]:
stmt = select(MarketPlace).where(MarketPlace.deleted_at.is_(None))
if container_type is not None and container_id is not None:
stmt = stmt.where(
MarketPlace.container_type == container_type,
MarketPlace.container_id == container_id,
)
stmt = stmt.order_by(MarketPlace.name.asc())
stmt = stmt.offset((page - 1) * per_page).limit(per_page + 1)
rows = (await session.execute(stmt)).scalars().all()
has_more = len(rows) > per_page
return [_mp_to_dto(mp) for mp in rows[:per_page]], has_more
async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None:
product = (
await session.execute(select(Product).where(Product.id == product_id))

View File

@@ -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()
@@ -153,6 +156,13 @@ class StubMarketService:
) -> list[MarketPlaceDTO]:
return []
async def list_marketplaces(
self, session: AsyncSession,
container_type: str | None = None, container_id: int | None = None,
*, page: int = 1, per_page: int = 20,
) -> tuple[list[MarketPlaceDTO], bool]:
return [], False
async def product_by_id(self, session: AsyncSession, product_id: int) -> ProductDTO | None:
return None