diff --git a/blog/bp/blog/services/posts_data.py b/blog/bp/blog/services/posts_data.py index 1d2c0ad..d24ee6b 100644 --- a/blog/bp/blog/services/posts_data.py +++ b/blog/bp/blog/services/posts_data.py @@ -126,7 +126,7 @@ _CARD_MARKER_RE = re.compile( def _parse_card_fragments(html: str) -> dict[str, str]: """Parse the container-cards fragment into {post_id_str: html} dict.""" result = {} - for m in _CARD_MARKER_RE.finditer(html): + for m in _CARD_MARKER_RE.finditer(str(html)): post_id_str = m.group(1) inner = m.group(2).strip() if inner: diff --git a/events/app.py b/events/app.py index 59dae16..be8fce9 100644 --- a/events/app.py +++ b/events/app.py @@ -113,10 +113,7 @@ def create_app() -> "Quart": ) from shared.sx.handlers import auto_mount_fragment_handlers - from bp.fragments.python_handlers import container_cards_handler, account_page_handler - 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) + auto_mount_fragment_handlers(app, "events") app.register_blueprint(register_actions()) app.register_blueprint(register_data()) diff --git a/events/bp/fragments/__init__.py b/events/bp/fragments/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/events/bp/fragments/python_handlers.py b/events/bp/fragments/python_handlers.py deleted file mode 100644 index 0ea1dc4..0000000 --- a/events/bp/fragments/python_handlers.py +++ /dev/null @@ -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 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 "" diff --git a/events/sx/handlers/account-page.sx b/events/sx/handlers/account-page.sx new file mode 100644 index 0000000..925eb8c --- /dev/null +++ b/events/sx/handlers/account-page.sx @@ -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)))))))))) diff --git a/events/sx/handlers/container-cards.sx b/events/sx/handlers/container-cards.sx new file mode 100644 index 0000000..193f23a --- /dev/null +++ b/events/sx/handlers/container-cards.sx @@ -0,0 +1,38 @@ +;; Container-cards fragment handler +;; +;; Returns HTML with 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 "") + (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 "")))) ids)))))) diff --git a/shared/sx/primitives_io.py b/shared/sx/primitives_io.py index ae17a28..c00565c 100644 --- a/shared/sx/primitives_io.py +++ b/shared/sx/primitives_io.py @@ -242,6 +242,8 @@ def _convert_result(result: Any) -> Any: if result is None: from .types import NIL return NIL + if isinstance(result, dict): + return {k: _convert_result(v) for k, v in result.items()} if isinstance(result, tuple): # Tuple returns (e.g. (entries, has_more)) → list for sx access return [_convert_result(item) for item in result]