feat: initialize events app with calendars, slots, tickets, and internal API
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Extract events/calendar functionality into standalone microservice: - app.py and events_api.py from apps/events/ - Calendar blueprints (calendars, calendar, calendar_entries, calendar_entry, day, slots, slot, ticket_types, ticket_type) - Templates for all calendar/event views including admin - Dockerfile (APP_MODULE=app:app, IMAGE=events) - entrypoint.sh (no Alembic - migrations managed by blog app) - Gitea CI workflow for build and deploy Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
109
bp/calendar/services/calendar_view.py
Normal file
109
bp/calendar/services/calendar_view.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
import calendar as pycalendar
|
||||
|
||||
from quart import request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload, with_loader_criteria
|
||||
|
||||
from models.calendars import Calendar, CalendarSlot
|
||||
|
||||
|
||||
def parse_int_arg(name: str, default: Optional[int] = None) -> Optional[int]:
|
||||
"""Parse an integer query parameter from the request."""
|
||||
val = request.args.get(name, "").strip()
|
||||
if not val:
|
||||
return default
|
||||
try:
|
||||
return int(val)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def add_months(year: int, month: int, delta: int) -> tuple[int, int]:
|
||||
"""Add (or subtract) months to a given year/month, handling year overflow."""
|
||||
new_month = month + delta
|
||||
new_year = year + (new_month - 1) // 12
|
||||
new_month = ((new_month - 1) % 12) + 1
|
||||
return new_year, new_month
|
||||
|
||||
|
||||
def build_calendar_weeks(year: int, month: int) -> list[list[dict]]:
|
||||
"""
|
||||
Build a calendar grid for the given year and month.
|
||||
Returns a list of weeks, where each week is a list of 7 day dictionaries.
|
||||
"""
|
||||
today = datetime.now(timezone.utc).date()
|
||||
cal = pycalendar.Calendar(firstweekday=0) # 0 = Monday
|
||||
weeks: list[list[dict]] = []
|
||||
|
||||
for week in cal.monthdatescalendar(year, month):
|
||||
week_days = []
|
||||
for d in week:
|
||||
week_days.append(
|
||||
{
|
||||
"date": d,
|
||||
"in_month": (d.month == month),
|
||||
"is_today": (d == today),
|
||||
}
|
||||
)
|
||||
weeks.append(week_days)
|
||||
|
||||
return weeks
|
||||
|
||||
|
||||
async def get_calendar_by_post_and_slug(
|
||||
session: AsyncSession,
|
||||
post_id: int,
|
||||
calendar_slug: str,
|
||||
) -> Optional[Calendar]:
|
||||
"""
|
||||
Fetch a calendar by post_id and slug, with slots eagerly loaded.
|
||||
Returns None if not found.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Calendar)
|
||||
.options(
|
||||
selectinload(Calendar.slots),
|
||||
with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)),
|
||||
)
|
||||
.where(
|
||||
Calendar.post_id == post_id,
|
||||
Calendar.slug == calendar_slug,
|
||||
Calendar.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def get_calendar_by_slug(
|
||||
session: AsyncSession,
|
||||
calendar_slug: str,
|
||||
) -> Optional[Calendar]:
|
||||
"""
|
||||
Fetch a calendar by slug only (for standalone events service).
|
||||
With slots eagerly loaded. Returns None if not found.
|
||||
"""
|
||||
result = await session.execute(
|
||||
select(Calendar)
|
||||
.options(
|
||||
selectinload(Calendar.slots),
|
||||
with_loader_criteria(CalendarSlot, CalendarSlot.deleted_at.is_(None)),
|
||||
)
|
||||
.where(
|
||||
Calendar.slug == calendar_slug,
|
||||
Calendar.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def update_calendar_description(
|
||||
calendar: Calendar,
|
||||
description: Optional[str],
|
||||
) -> None:
|
||||
"""Update calendar description (in-place on the calendar object)."""
|
||||
calendar.description = description or None
|
||||
Reference in New Issue
Block a user