Files
mono/events/bp/ticket_admin/routes.py
giles 838ec982eb Phase 7: Replace render_template() with s-expression rendering in all POST/PUT/DELETE routes
Eliminates all render_template() calls from POST/PUT/DELETE handlers across
all 7 services. Moves sexp_components.py into sexp/ packages per service.

- Blog: like toggle, snippets, cache clear, features/sumup/entry panels,
  create/delete market, WYSIWYG editor panel (render_editor_panel)
- Federation: like/unlike/boost/unboost, follow/unfollow, actor card,
  interaction buttons
- Events: ticket widget, checkin, confirm/decline/provisional, tickets
  config, posts CRUD, description edit/save, calendar/slot/ticket_type
  CRUD, payments, buy tickets, day main panel, entry page
- Market: like toggle, cart add response
- Account: newsletter toggle
- Cart: checkout error pages (3 handlers)
- Orders: checkout error page (1 handler)

Remaining render_template() calls are exclusively in GET handlers and
internal services (email templates, fragment endpoints).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:15:29 +00:00

144 lines
4.7 KiB
Python

"""
Ticket admin blueprint — check-in interface and ticket management.
Routes:
GET /admin/tickets/ — Ticket dashboard (scan + list)
GET /admin/tickets/entry/<id>/ — Tickets for a specific entry
POST /admin/tickets/<code>/checkin — Check in a ticket
GET /admin/tickets/<code>/ — Ticket admin detail
"""
from __future__ import annotations
import logging
from quart import (
Blueprint, g, request, render_template, make_response, jsonify,
)
from sqlalchemy import select, func
from sqlalchemy.orm import selectinload
from models.calendars import CalendarEntry, Ticket, TicketType
from shared.browser.app.authz import require_admin
from shared.browser.app.redis_cacher import clear_cache
from ..tickets.services.tickets import (
get_ticket_by_code,
get_tickets_for_entry,
checkin_ticket,
)
logger = logging.getLogger(__name__)
def register() -> Blueprint:
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
@bp.get("/")
@require_admin
async def dashboard():
"""Ticket admin dashboard with QR scanner and recent tickets."""
from shared.browser.app.utils.htmx import is_htmx_request
# Get recent tickets
result = await g.s.execute(
select(Ticket)
.options(
selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
selectinload(Ticket.ticket_type),
)
.order_by(Ticket.created_at.desc())
.limit(50)
)
tickets = result.scalars().all()
# Stats
total = await g.s.scalar(select(func.count(Ticket.id)))
confirmed = await g.s.scalar(
select(func.count(Ticket.id)).where(Ticket.state == "confirmed")
)
checked_in = await g.s.scalar(
select(func.count(Ticket.id)).where(Ticket.state == "checked_in")
)
reserved = await g.s.scalar(
select(func.count(Ticket.id)).where(Ticket.state == "reserved")
)
stats = {
"total": total or 0,
"confirmed": confirmed or 0,
"checked_in": checked_in or 0,
"reserved": reserved or 0,
}
from shared.sexp.page import get_template_context
from sexp.sexp_components import render_ticket_admin_page, render_ticket_admin_oob
ctx = await get_template_context()
if not is_htmx_request():
html = await render_ticket_admin_page(ctx, tickets, stats)
else:
html = await render_ticket_admin_oob(ctx, tickets, stats)
return await make_response(html, 200)
@bp.get("/entry/<int:entry_id>/")
@require_admin
async def entry_tickets(entry_id: int):
"""List all tickets for a specific calendar entry."""
from shared.browser.app.utils.htmx import is_htmx_request
entry = await g.s.scalar(
select(CalendarEntry)
.where(
CalendarEntry.id == entry_id,
CalendarEntry.deleted_at.is_(None),
)
.options(selectinload(CalendarEntry.calendar))
)
if not entry:
return await make_response("Entry not found", 404)
tickets = await get_tickets_for_entry(g.s, entry_id)
from sexp.sexp_components import render_entry_tickets_admin
html = render_entry_tickets_admin(entry, tickets)
return await make_response(html, 200)
@bp.get("/lookup/")
@require_admin
async def lookup():
"""Look up a ticket by code (used by scanner)."""
code = request.args.get("code", "").strip()
if not code:
return await make_response(
'<div class="text-sm text-stone-500">Enter a ticket code</div>',
200,
)
ticket = await get_ticket_by_code(g.s, code)
from sexp.sexp_components import render_lookup_result
if not ticket:
html = render_lookup_result(None, "Ticket not found")
return await make_response(html, 200)
html = render_lookup_result(ticket, None)
return await make_response(html, 200)
@bp.post("/<code>/checkin/")
@require_admin
@clear_cache(tag="calendars", tag_scope="all")
async def do_checkin(code: str):
"""Check in a ticket by its code."""
success, error = await checkin_ticket(g.s, code)
from sexp.sexp_components import render_checkin_result
if not success:
html = render_checkin_result(False, error, None)
return await make_response(html, 200)
ticket = await get_ticket_by_code(g.s, code)
html = render_checkin_result(True, None, ticket)
return await make_response(html, 200)
return bp