AP blueprints (activitypub.py, ap_social.py) were querying federation tables (ap_actor_profiles etc.) on g.s which points to the app's own DB after the per-app split. Now uses g._ap_s backed by get_federation_session() for non-federation apps. Also hardens Ghost sync before_app_serving to catch/rollback on failure instead of crashing the Hypercorn worker. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
6.9 KiB
Ticket UX Improvements: +/- Buttons, Sold Count, Cart Grouping
Context
The entry page currently uses a numeric input + "Buy Tickets" button, which replaces itself with a confirmation after purchase. The cart lists each ticket individually. The user wants the ticket UX to match the product pattern: +/- buttons, "in basket" count, tickets grouped by event on cart.
Requirements
- Entry page: Show tickets sold count + current user's "in basket" count
- Entry page: Replace qty input with "Add to basket" / +/- buttons (product pattern)
- Entry page: Keep form active after adding (don't replace with confirmation)
- Cart page: Group tickets by event (entry_id + ticket_type), show quantity with +/- buttons
1. Add ticket_type_id to TicketDTO
File: shared/contracts/dtos.py
- Add
ticket_type_id: int | None = Nonefield toTicketDTO
File: shared/services/calendar_impl.py
- In
_ticket_to_dto(), populateticket_type_id=ticket.ticket_type_id
Sync: Copy to all 4 app submodule copies.
2. New ticket service functions
File: events/bp/tickets/services/tickets.py
- Add
get_user_reserved_count(session, entry_id, user_id, session_id, ticket_type_id=None) -> int- Counts reserved tickets for this user+entry+type
- Add
get_sold_ticket_count(session, entry_id) -> int- Counts all non-cancelled tickets for this entry
- Add
cancel_latest_reserved_ticket(session, entry_id, user_id, session_id, ticket_type_id=None) -> bool- Finds the most recently created reserved ticket for this user+entry+type, sets state='cancelled'. Returns True if one was cancelled.
3. Add adjust_quantity route to events tickets blueprint
File: events/bp/tickets/routes.py
- New route:
POST /tickets/adjust/ - Form fields:
entry_id,ticket_type_id(optional),count(target quantity) - Logic:
- Get current user reserved count for this entry/type
- If count > current: create
(count - current)tickets viacreate_ticket() - If count < current: cancel
(current - count)tickets viacancel_latest_reserved_ticket()in a loop - If count == 0: cancel all
- Check availability before adding (like existing
buy_tickets)
- Response: re-render
_buy_form.html(HTMX swap replaces form, keeps it active) - Include OOB cart-mini update:
{{ mini(oob='true') }}
4. Inject ticket counts into entry page context
File: events/bp/calendar_entry/routes.py — inject_root context processor
- Add
ticket_sold_count: total non-cancelled tickets for entry (viaget_sold_ticket_count) - Add
user_ticket_count: current user's reserved count (viaget_user_reserved_count) - For multi-type entries, add
user_ticket_counts_by_type: dict mapping ticket_type_id → count
5. Rewrite entry page buy form
File: events/templates/_types/tickets/_buy_form.html
- Show "X sold" (from
ticket_sold_count) alongside "X remaining" - Show "X in basket" for current user
For single-price entries (no ticket types):
- If
user_ticket_count == 0: show "Add to basket" button (posts to/tickets/adjust/with count=1) - If
user_ticket_count > 0: show[-][count badge][+]buttons- Minus: posts count=user_ticket_count-1
- Plus: posts count=user_ticket_count+1
- All forms:
hx-post,hx-target="#ticket-buy-{{ entry.id }}",hx-swap="outerHTML"
For multi-type entries:
- Same pattern per ticket type row, using
user_ticket_counts_by_type[tt.id]
Style: match product pattern exactly — emerald circular buttons, w-8 h-8, cart icon with badge.
6. Add ticket quantity route to cart app
File: cart/bp/cart/global_routes.py
- New route:
POST /cart/ticket-quantity/ - Form fields:
entry_id,ticket_type_id(optional),count(target quantity) - Logic: call into CalendarService or directly use ticket functions
- Since cart app uses service contracts, add
adjust_ticket_quantityto CalendarService protocol
- Since cart app uses service contracts, add
File: shared/contracts/protocols.py — CalendarService
- Add:
adjust_ticket_quantity(session, entry_id, count, *, user_id, session_id, ticket_type_id=None) -> int
File: shared/services/calendar_impl.py
- Implement
adjust_ticket_quantity:- Same logic as events adjust route (create/cancel to match target count)
- Return new count
File: shared/services/stubs.py
- Add stub: returns 0
Response: HX-Refresh: true (same as product quantity route).
7. Cart page: group tickets by event with +/- buttons
File: cart/templates/_types/cart/_cart.html — ticket section (lines 63-95)
- Replace individual ticket list with grouped display
- Group
ticket_cart_entriesby(entry_id, ticket_type_id):- Use Jinja
groupbyonentry_idfirst, then sub-group byticket_type_name - Or pre-group in the route handler and pass as a dict
- Use Jinja
Approach: Pre-group in the route handler for cleaner templates.
File: cart/bp/cart/page_routes.py — page_view
- After getting
page_tickets, group them into a list of dicts:[{"entry_name": ..., "entry_id": ..., "ticket_type_name": ..., "ticket_type_id": ..., "entry_start_at": ..., "entry_end_at": ..., "price": ..., "quantity": N}] - Pass as
ticket_groupsto template
File: cart/bp/cart/global_routes.py — overview/checkout routes
- Same grouping for global cart view if tickets appear there
Cart ticket group template: Each group shows:
- Event name + ticket type (if any)
- Date/time
- Price per ticket
-[qty]+buttons (posting to/cart/ticket-quantity/)- Line total (price × qty)
Match product cart_item macro style (article card with quantity controls).
8. Cart summary update
File: cart/templates/_types/cart/_cart.html — summary macro
- Update Items count: include ticket quantities in total (currently just product quantities)
Files to modify (summary)
shared/contracts/dtos.py— add ticket_type_id to TicketDTOshared/contracts/protocols.py— add adjust_ticket_quantity to CalendarServiceshared/services/calendar_impl.py— implement adjust_ticket_quantity, update _ticket_to_dtoshared/services/stubs.py— add stubevents/bp/tickets/services/tickets.py— add count/cancel functionsevents/bp/tickets/routes.py— add adjust routeevents/bp/calendar_entry/routes.py— inject sold/user countsevents/templates/_types/tickets/_buy_form.html— rewrite with +/- patterncart/bp/cart/global_routes.py— add ticket-quantity routecart/bp/cart/page_routes.py— group ticketscart/templates/_types/cart/_cart.html— grouped ticket display with +/-- All 4 app
shared/submodule copies synced
Verification
- Visit entry page → see "X sold", "X in basket", "Add to basket" button
- Click "Add to basket" → form stays, shows
-[1]+, basket count shows "1 in basket" - Click
+→ count increases, sold count increases - Click
-→ count decreases, ticket cancelled - Visit cart page → tickets grouped by event, +/- buttons work
- Checkout flow still works (existing tests)