From b52ef719bf9387429bd7e2192325aaafc85aff7d Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Feb 2026 01:40:02 +0000 Subject: [PATCH] Fix 500 errors and double-slash URLs found during sexp rendering testing - events: fix ImportError for events_url (was importing from shared.utils instead of shared.infrastructure.urls) - blog: add missing ~mobile-filter sexp component (details/summary panel) - shared: fix double-slash URLs in ~auth-menu, ~cart-mini, ~header-row by removing redundant "/" concatenation on URLs that already have trailing slash - blog: fix ghost_sync select UnboundLocalError caused by redundant local import shadowing module-level import Co-Authored-By: Claude Opus 4.6 --- blog/bp/blog/ghost/ghost_sync.py | 1 - events/sexp/sexp_components.py | 20 +++++++---- shared/sexp/components.py | 52 +++++++++++++++++++++++----- shared/sexp/tests/test_components.py | 8 ++--- 4 files changed, 61 insertions(+), 20 deletions(-) diff --git a/blog/bp/blog/ghost/ghost_sync.py b/blog/bp/blog/ghost/ghost_sync.py index 0c97f14..2f65251 100644 --- a/blog/bp/blog/ghost/ghost_sync.py +++ b/blog/bp/blog/ghost/ghost_sync.py @@ -251,7 +251,6 @@ async def _upsert_post(sess: AsyncSession, gp: Dict[str, Any], author_map: Dict[ # Auto-create PageConfig for pages (blog owns this table — direct DB, # not via HTTP, since this may run during startup before the server is ready) if obj.is_page: - from sqlalchemy import select from shared.models.page_config import PageConfig existing = (await sess.execute( select(PageConfig).where( diff --git a/events/sexp/sexp_components.py b/events/sexp/sexp_components.py index d1e1ac2..949dcf0 100644 --- a/events/sexp/sexp_components.py +++ b/events/sexp/sexp_components.py @@ -1530,7 +1530,8 @@ async def render_all_events_page(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """Full page: all events listing.""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() view_param = f"&view={view}" if view != "list" else "" @@ -1549,7 +1550,8 @@ async def render_all_events_oob(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """OOB response: all events listing (htmx nav).""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("all_events.adjust_ticket") @@ -1566,7 +1568,8 @@ async def render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) -> str: """Pagination fragment: all events cards only.""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("all_events.adjust_ticket") @@ -1586,7 +1589,8 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets page_info, page, view) -> str: """Full page: page-scoped events listing.""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() post = ctx.get("post") or {} @@ -1611,7 +1615,8 @@ async def render_page_summary_oob(ctx: dict, entries, has_more, pending_tickets, page_info, page, view) -> str: """OOB response: page-scoped events (htmx nav).""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() post = ctx.get("post") or {} @@ -1632,7 +1637,8 @@ async def render_page_summary_cards(entries, has_more, pending_tickets, page_info, page, view, post) -> str: """Pagination fragment: page-scoped events cards only.""" from quart import url_for - from shared.utils import route_prefix, events_url + from shared.utils import route_prefix + from shared.infrastructure.urls import events_url prefix = route_prefix() ticket_url = url_for("page_summary.adjust_ticket") @@ -2634,7 +2640,7 @@ def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: def render_post_nav_entries_oob(associated_entries, calendars, post) -> str: """Render OOB nav for associated entries and calendars of a post.""" from quart import g - from shared.utils import events_url + from shared.infrastructure.urls import events_url styles = getattr(g, "styles", None) or {} nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "") diff --git a/shared/sexp/components.py b/shared/sexp/components.py index 92ac65e..5421d54 100644 --- a/shared/sexp/components.py +++ b/shared/sexp/components.py @@ -34,6 +34,7 @@ def load_shared_components() -> None: register_components(_STATUS_PILL) register_components(_SEARCH_MOBILE) register_components(_SEARCH_DESKTOP) + register_components(_MOBILE_FILTER) register_components(_ORDER_SUMMARY_CARD) @@ -88,11 +89,11 @@ _CART_MINI = ''' :hx-swap-oob oob (if (= cart-count 0) (div :class "h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0" - (a :href (str blog-url "/") + (a :href blog-url :class "h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1" - (img :src (str blog-url "/static/img/logo.jpg") + (img :src (str blog-url "static/img/logo.jpg") :class "h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0"))) - (a :href (str cart-url "/") + (a :href cart-url :class "relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700" (i :class "fa fa-shopping-cart text-5xl" :aria-hidden "true") (span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5" @@ -115,22 +116,22 @@ _AUTH_MENU = ''' (<> (span :id "auth-menu-desktop" :class "hidden md:inline-flex" (if user-email - (a :href (str account-url "/") + (a :href account-url :class "justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black" :data-close-details true (i :class "fa-solid fa-user") (span user-email)) - (a :href (str account-url "/") + (a :href account-url :class "justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black" :data-close-details true (i :class "fa-solid fa-key") (span "sign in or register")))) (span :id "auth-menu-mobile" :class "block md:hidden text-md font-bold" (if user-email - (a :href (str account-url "/") :data-close-details true + (a :href account-url :data-close-details true (i :class "fa-solid fa-user") (span user-email)) - (a :href (str account-url "/") + (a :href account-url (i :class "fa-solid fa-key") (span "sign in or register")))))) ''' @@ -487,7 +488,7 @@ _HEADER_ROW = ''' (div :class "w-full flex flex-row items-top" (when cart-mini-html (raw! cart-mini-html)) (div :class "font-bold text-5xl flex-1" - (a :href (str (or blog-url "") "/") :class "flex justify-center md:justify-start" + (a :href (or blog-url "/") :class "flex justify-center md:justify-start" (h1 (or site-title "")))) (nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0" (when nav-tree-html (raw! nav-tree-html)) @@ -716,6 +717,41 @@ _SEARCH_DESKTOP = ''' ''' +# --------------------------------------------------------------------------- +# ~mobile-filter — mobile filter details/summary panel +# --------------------------------------------------------------------------- +# Replaces: blog/templates/_types/blog/mobile/_filter/summary.html +# + macros/layout.html details/filter_summary +# +# Usage: +# sexp('(~mobile-filter :filter-summary-html fsh :action-buttons-html abh +# :filter-details-html fdh)', +# fsh="...", abh="...", fdh="...") +# --------------------------------------------------------------------------- + +_MOBILE_FILTER = ''' +(defcomp ~mobile-filter (&key filter-summary-html action-buttons-html filter-details-html) + (details :class "group/filter p-2 md:hidden" :data-toggle-group "mobile-panels" + (summary :class "bg-white/90" + (div :class "flex flex-row items-start" + (div + (div :class "md:hidden mx-2 bg-stone-200 rounded" + (span :class "flex items-center justify-center text-stone-600 text-lg h-12 w-12 transition-transform group-open/filter:hidden self-start" + (i :class "fa-solid fa-filter")) + (span + (svg :aria-hidden "true" :viewBox "0 0 24 24" + :class "w-12 h-12 rotate-180 transition-transform group-open/filter:block hidden self-start" + (path :d "M6 9l6 6 6-6" :fill "currentColor"))))) + (div :id "filter-summary-mobile" + :class "flex-1 md:hidden grid grid-cols-12 items-center gap-3" + (div :class "flex flex-col items-start gap-2" + (raw! filter-summary-html))))) + (raw! (or action-buttons-html "")) + (div :id "filter-details-mobile" :style "display:contents" + (raw! (or filter-details-html ""))))) +''' + + # --------------------------------------------------------------------------- # ~order-summary-card — reusable order summary card # --------------------------------------------------------------------------- diff --git a/shared/sexp/tests/test_components.py b/shared/sexp/tests/test_components.py index 72c5f10..a3f20f6 100644 --- a/shared/sexp/tests/test_components.py +++ b/shared/sexp/tests/test_components.py @@ -21,7 +21,7 @@ class TestCartMini: def test_empty_cart_shows_logo(self): html = sexp( '(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', - **{"cart-count": 0, "blog-url": "https://blog.example.com", "cart-url": "https://cart.example.com"}, + **{"cart-count": 0, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, ) assert 'id="cart-mini"' in html assert "logo.jpg" in html @@ -31,7 +31,7 @@ class TestCartMini: def test_nonempty_cart_shows_badge(self): html = sexp( '(~cart-mini :cart-count cart-count :blog-url blog-url :cart-url cart-url)', - **{"cart-count": 3, "blog-url": "https://blog.example.com", "cart-url": "https://cart.example.com"}, + **{"cart-count": 3, "blog-url": "https://blog.example.com/", "cart-url": "https://cart.example.com/"}, ) assert 'id="cart-mini"' in html assert "fa-shopping-cart" in html @@ -60,7 +60,7 @@ class TestAuthMenu: def test_logged_in(self): html = sexp( '(~auth-menu :user-email user-email :account-url account-url)', - **{"user-email": "alice@example.com", "account-url": "https://account.example.com"}, + **{"user-email": "alice@example.com", "account-url": "https://account.example.com/"}, ) assert 'id="auth-menu-desktop"' in html assert 'id="auth-menu-mobile"' in html @@ -71,7 +71,7 @@ class TestAuthMenu: def test_logged_out(self): html = sexp( '(~auth-menu :account-url account-url)', - **{"account-url": "https://account.example.com"}, + **{"account-url": "https://account.example.com/"}, ) assert "fa-solid fa-key" in html assert "sign in or register" in html