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>
133 lines
4.3 KiB
Python
133 lines
4.3 KiB
Python
"""
|
|
Internal JSON API for the events app.
|
|
|
|
These endpoints are called by other apps (cart) over HTTP.
|
|
They are CSRF-exempt because they are server-to-server calls.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, g, request, jsonify
|
|
from sqlalchemy import select, update, func
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from models.calendars import CalendarEntry, Calendar
|
|
from suma_browser.app.csrf import csrf_exempt
|
|
|
|
|
|
def register() -> Blueprint:
|
|
bp = Blueprint("events_api", __name__, url_prefix="/internal/events")
|
|
|
|
@bp.get("/calendar-entries")
|
|
@csrf_exempt
|
|
async def calendar_entries():
|
|
"""
|
|
Return pending calendar entries for a user/session.
|
|
Used by the cart app to display calendar items in the cart.
|
|
|
|
Query params: user_id, session_id, state (default: pending)
|
|
"""
|
|
user_id = request.args.get("user_id", type=int)
|
|
session_id = request.args.get("session_id")
|
|
state = request.args.get("state", "pending")
|
|
|
|
filters = [
|
|
CalendarEntry.deleted_at.is_(None),
|
|
CalendarEntry.state == state,
|
|
]
|
|
if user_id is not None:
|
|
filters.append(CalendarEntry.user_id == user_id)
|
|
elif session_id:
|
|
filters.append(CalendarEntry.session_id == session_id)
|
|
else:
|
|
return jsonify([])
|
|
|
|
result = await g.s.execute(
|
|
select(CalendarEntry)
|
|
.where(*filters)
|
|
.options(selectinload(CalendarEntry.calendar))
|
|
.order_by(CalendarEntry.start_at.asc())
|
|
)
|
|
entries = result.scalars().all()
|
|
|
|
return jsonify([
|
|
{
|
|
"id": e.id,
|
|
"name": e.name,
|
|
"cost": float(e.cost) if e.cost else 0,
|
|
"state": e.state,
|
|
"start_at": e.start_at.isoformat() if e.start_at else None,
|
|
"end_at": e.end_at.isoformat() if e.end_at else None,
|
|
"calendar_name": e.calendar.name if e.calendar else None,
|
|
"calendar_slug": e.calendar.slug if e.calendar else None,
|
|
}
|
|
for e in entries
|
|
])
|
|
|
|
@bp.post("/adopt")
|
|
@csrf_exempt
|
|
async def adopt():
|
|
"""
|
|
Adopt anonymous calendar entries for a user.
|
|
Called by the cart app after login.
|
|
|
|
Body: {"user_id": int, "session_id": str}
|
|
"""
|
|
data = await request.get_json() or {}
|
|
user_id = data.get("user_id")
|
|
session_id = data.get("session_id")
|
|
|
|
if not user_id or not session_id:
|
|
return jsonify({"ok": False, "error": "user_id and session_id required"}), 400
|
|
|
|
# Soft-delete existing user entries
|
|
await g.s.execute(
|
|
update(CalendarEntry)
|
|
.where(
|
|
CalendarEntry.deleted_at.is_(None),
|
|
CalendarEntry.user_id == user_id,
|
|
)
|
|
.values(deleted_at=func.now())
|
|
)
|
|
|
|
# Adopt anonymous entries
|
|
cal_result = await g.s.execute(
|
|
select(CalendarEntry).where(
|
|
CalendarEntry.deleted_at.is_(None),
|
|
CalendarEntry.session_id == session_id,
|
|
)
|
|
)
|
|
for entry in cal_result.scalars().all():
|
|
entry.user_id = user_id
|
|
|
|
return jsonify({"ok": True})
|
|
|
|
@bp.get("/entry/<int:entry_id>")
|
|
@csrf_exempt
|
|
async def entry_detail(entry_id: int):
|
|
"""
|
|
Return entry details for order display.
|
|
Called by the cart app when showing order items.
|
|
"""
|
|
result = await g.s.execute(
|
|
select(CalendarEntry)
|
|
.where(CalendarEntry.id == entry_id)
|
|
.options(selectinload(CalendarEntry.calendar))
|
|
)
|
|
entry = result.scalar_one_or_none()
|
|
|
|
if not entry:
|
|
return jsonify(None), 404
|
|
|
|
return jsonify({
|
|
"id": entry.id,
|
|
"name": entry.name,
|
|
"cost": float(entry.cost) if entry.cost else 0,
|
|
"state": entry.state,
|
|
"start_at": entry.start_at.isoformat() if entry.start_at else None,
|
|
"end_at": entry.end_at.isoformat() if entry.end_at else None,
|
|
"calendar_name": entry.calendar.name if entry.calendar else None,
|
|
"calendar_slug": entry.calendar.slug if entry.calendar else None,
|
|
})
|
|
|
|
return bp
|