Convert last Python fragment handlers to SX defhandlers: 100% declarative fragment API
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34m5s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 34m5s
- Add dict recursion to _convert_result for service methods returning dict[K, list[DTO]] - New container-cards.sx: parses post_ids/slugs, calls confirmed-entries-for-posts, emits card-widget markers - New account-page.sx: dispatches on slug for tickets/bookings panels with status pills and empty states - Fix blog _parse_card_fragments to handle SxExpr via str() cast - Remove events Python fragment handlers and simplify app.py to plain auto_mount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -126,7 +126,7 @@ _CARD_MARKER_RE = re.compile(
|
|||||||
def _parse_card_fragments(html: str) -> dict[str, str]:
|
def _parse_card_fragments(html: str) -> dict[str, str]:
|
||||||
"""Parse the container-cards fragment into {post_id_str: html} dict."""
|
"""Parse the container-cards fragment into {post_id_str: html} dict."""
|
||||||
result = {}
|
result = {}
|
||||||
for m in _CARD_MARKER_RE.finditer(html):
|
for m in _CARD_MARKER_RE.finditer(str(html)):
|
||||||
post_id_str = m.group(1)
|
post_id_str = m.group(1)
|
||||||
inner = m.group(2).strip()
|
inner = m.group(2).strip()
|
||||||
if inner:
|
if inner:
|
||||||
|
|||||||
@@ -113,10 +113,7 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
|
|
||||||
from shared.sx.handlers import auto_mount_fragment_handlers
|
from shared.sx.handlers import auto_mount_fragment_handlers
|
||||||
from bp.fragments.python_handlers import container_cards_handler, account_page_handler
|
auto_mount_fragment_handlers(app, "events")
|
||||||
add_fragment_handler = auto_mount_fragment_handlers(app, "events")
|
|
||||||
add_fragment_handler("container-cards", container_cards_handler, content_type="text/html")
|
|
||||||
add_fragment_handler("account-page", account_page_handler)
|
|
||||||
|
|
||||||
app.register_blueprint(register_actions())
|
app.register_blueprint(register_actions())
|
||||||
app.register_blueprint(register_data())
|
app.register_blueprint(register_data())
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
"""Python fragment handlers for events.
|
|
||||||
|
|
||||||
These handlers call domain services and use sx_call() for rendering,
|
|
||||||
so they can't be expressed as declarative .sx handlers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from quart import g, request
|
|
||||||
|
|
||||||
from shared.services.registry import services
|
|
||||||
|
|
||||||
|
|
||||||
async def container_cards_handler():
|
|
||||||
"""Container-cards fragment: entries for blog listing cards.
|
|
||||||
|
|
||||||
Returns text/html with <!-- card-widget:POST_ID --> comment markers
|
|
||||||
so the blog consumer can split per-post fragments.
|
|
||||||
"""
|
|
||||||
from sx.sx_components import render_fragment_container_cards
|
|
||||||
|
|
||||||
post_ids_raw = request.args.get("post_ids", "")
|
|
||||||
post_slugs_raw = request.args.get("post_slugs", "")
|
|
||||||
post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()]
|
|
||||||
post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()]
|
|
||||||
if not post_ids:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
slug_map = {}
|
|
||||||
for i, pid in enumerate(post_ids):
|
|
||||||
slug_map[pid] = post_slugs[i] if i < len(post_slugs) else ""
|
|
||||||
|
|
||||||
batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids)
|
|
||||||
return render_fragment_container_cards(batch, post_ids, slug_map)
|
|
||||||
|
|
||||||
|
|
||||||
async def account_page_handler():
|
|
||||||
"""Account-page fragment: tickets or bookings panel.
|
|
||||||
|
|
||||||
Returns text/sx — the account app embeds this as sx source.
|
|
||||||
"""
|
|
||||||
from sx.sx_components import (
|
|
||||||
render_fragment_account_tickets,
|
|
||||||
render_fragment_account_bookings,
|
|
||||||
)
|
|
||||||
|
|
||||||
slug = request.args.get("slug", "")
|
|
||||||
user_id = request.args.get("user_id", type=int)
|
|
||||||
if not user_id:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if slug == "tickets":
|
|
||||||
tickets = await services.calendar.user_tickets(g.s, user_id=user_id)
|
|
||||||
return render_fragment_account_tickets(tickets)
|
|
||||||
elif slug == "bookings":
|
|
||||||
bookings = await services.calendar.user_bookings(g.s, user_id=user_id)
|
|
||||||
return render_fragment_account_bookings(bookings)
|
|
||||||
return ""
|
|
||||||
49
events/sx/handlers/account-page.sx
Normal file
49
events/sx/handlers/account-page.sx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
;; Account-page fragment handler
|
||||||
|
;;
|
||||||
|
;; Renders tickets or bookings panel for the account dashboard.
|
||||||
|
;; slug=tickets → ticket list; slug=bookings → booking list.
|
||||||
|
|
||||||
|
(defhandler account-page (&key slug user_id)
|
||||||
|
(let ((uid (parse-int (or user_id "0"))))
|
||||||
|
(when (> uid 0)
|
||||||
|
(cond
|
||||||
|
(= slug "tickets")
|
||||||
|
(let ((tickets (service "calendar" "user-tickets" :user-id uid)))
|
||||||
|
(~events-frag-tickets-panel
|
||||||
|
:items (if (empty? tickets)
|
||||||
|
(~empty-state :message "No tickets yet."
|
||||||
|
:cls "text-sm text-stone-500")
|
||||||
|
(~events-frag-tickets-list
|
||||||
|
:items (<> (map (fn (t)
|
||||||
|
(~events-frag-ticket-item
|
||||||
|
:href (app-url "events"
|
||||||
|
(str "/tickets/" (get t "code") "/"))
|
||||||
|
:entry-name (get t "entry_name")
|
||||||
|
:date-str (format-date (get t "entry_start_at") "%d %b %Y, %H:%M")
|
||||||
|
:calendar-name (when (get t "calendar_name")
|
||||||
|
(span (str "\u00b7 " (get t "calendar_name"))))
|
||||||
|
:type-name (when (get t "ticket_type_name")
|
||||||
|
(span (str "\u00b7 " (get t "ticket_type_name"))))
|
||||||
|
:badge (~status-pill :status (or (get t "state") ""))))
|
||||||
|
tickets))))))
|
||||||
|
|
||||||
|
(= slug "bookings")
|
||||||
|
(let ((bookings (service "calendar" "user-bookings" :user-id uid)))
|
||||||
|
(~events-frag-bookings-panel
|
||||||
|
:items (if (empty? bookings)
|
||||||
|
(~empty-state :message "No bookings yet."
|
||||||
|
:cls "text-sm text-stone-500")
|
||||||
|
(~events-frag-bookings-list
|
||||||
|
:items (<> (map (fn (b)
|
||||||
|
(~events-frag-booking-item
|
||||||
|
:name (get b "name")
|
||||||
|
:date-str (str (format-date (get b "start_at") "%d %b %Y, %H:%M")
|
||||||
|
(if (get b "end_at")
|
||||||
|
(str " \u2013 " (format-date (get b "end_at") "%H:%M"))
|
||||||
|
""))
|
||||||
|
:calendar-name (when (get b "calendar_name")
|
||||||
|
(span (str "\u00b7 " (get b "calendar_name"))))
|
||||||
|
:cost-str (when (get b "cost")
|
||||||
|
(span (str "\u00b7 \u00a3" (get b "cost"))))
|
||||||
|
:badge (~status-pill :status (or (get b "state") ""))))
|
||||||
|
bookings))))))))))
|
||||||
38
events/sx/handlers/container-cards.sx
Normal file
38
events/sx/handlers/container-cards.sx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
;; Container-cards fragment handler
|
||||||
|
;;
|
||||||
|
;; Returns HTML with <!-- card-widget:ID --> comment markers so the
|
||||||
|
;; blog consumer can split per-post fragments. Each post section
|
||||||
|
;; contains an events-frag-entries-widget with entry cards.
|
||||||
|
|
||||||
|
(defhandler container-cards (&key post_ids post_slugs)
|
||||||
|
(let ((ids (filter (fn (x) (> x 0))
|
||||||
|
(map parse-int
|
||||||
|
(filter (fn (s) (not (empty? s)))
|
||||||
|
(split (or post_ids "") ",")))))
|
||||||
|
(slugs (map trim
|
||||||
|
(split (or post_slugs "") ","))))
|
||||||
|
(when (not (empty? ids))
|
||||||
|
(let ((batch (service "calendar" "confirmed-entries-for-posts" :post-ids ids)))
|
||||||
|
(<> (map-indexed (fn (i pid)
|
||||||
|
(let ((entries (or (get batch pid) (list)))
|
||||||
|
(post-slug (or (nth slugs i) "")))
|
||||||
|
(<> (str "<!-- card-widget:" pid " -->")
|
||||||
|
(when (not (empty? entries))
|
||||||
|
(~events-frag-entries-widget
|
||||||
|
:cards (<> (map (fn (e)
|
||||||
|
(let ((time-str (str (format-date (get e "start_at") "%H:%M")
|
||||||
|
(if (get e "end_at")
|
||||||
|
(str " \u2013 " (format-date (get e "end_at") "%H:%M"))
|
||||||
|
""))))
|
||||||
|
(~events-frag-entry-card
|
||||||
|
:href (app-url "events"
|
||||||
|
(str "/" post-slug
|
||||||
|
"/" (get e "calendar_slug")
|
||||||
|
"/" (get e "start_at_year")
|
||||||
|
"/" (get e "start_at_month")
|
||||||
|
"/" (get e "start_at_day")
|
||||||
|
"/entries/" (get e "id") "/"))
|
||||||
|
:name (get e "name")
|
||||||
|
:date-str (format-date (get e "start_at") "%a, %b %d")
|
||||||
|
:time-str time-str))) entries))))
|
||||||
|
(str "<!-- /card-widget:" pid " -->")))) ids))))))
|
||||||
@@ -242,6 +242,8 @@ def _convert_result(result: Any) -> Any:
|
|||||||
if result is None:
|
if result is None:
|
||||||
from .types import NIL
|
from .types import NIL
|
||||||
return NIL
|
return NIL
|
||||||
|
if isinstance(result, dict):
|
||||||
|
return {k: _convert_result(v) for k, v in result.items()}
|
||||||
if isinstance(result, tuple):
|
if isinstance(result, tuple):
|
||||||
# Tuple returns (e.g. (entries, has_more)) → list for sx access
|
# Tuple returns (e.g. (entries, has_more)) → list for sx access
|
||||||
return [_convert_result(item) for item in result]
|
return [_convert_result(item) for item in result]
|
||||||
|
|||||||
Reference in New Issue
Block a user