Move events/market/blog composition from Python to .sx defcomps (Phase 9)
Continues the pattern of eliminating Python sx_call tree-building in favour of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data (dicts, lists, scalars) and let .sx handle iteration, conditionals, and layout via map/let/when/if. Single response components wrap OOB swaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,23 @@
|
||||
"""Entry panels, cards, forms, edit/add."""
|
||||
from __future__ import annotations
|
||||
|
||||
from markupsafe import escape
|
||||
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
from .utils import (
|
||||
_entry_state_badge_html, _ticket_state_badge_html,
|
||||
_entry_state_badge_html,
|
||||
_list_container, _view_toggle_html,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# All events / page summary entry cards
|
||||
# All events / page summary entry cards — data extraction
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
|
||||
def _entry_card_data(entry, page_info: dict, pending_tickets: dict,
|
||||
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
||||
post: dict | None = None) -> str:
|
||||
"""Render a list card for one event entry."""
|
||||
from .tickets import _ticket_widget_html
|
||||
post: dict | None = None) -> dict:
|
||||
"""Extract data for a single entry card (list or tile)."""
|
||||
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
||||
if is_page_scoped and post:
|
||||
page_slug = pi.get("slug", post.get("slug", ""))
|
||||
@@ -33,145 +30,103 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict,
|
||||
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
||||
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
||||
|
||||
# Title (linked or plain)
|
||||
if entry_href:
|
||||
title_html = sx_call("events-entry-title-linked",
|
||||
href=entry_href, name=entry.name)
|
||||
else:
|
||||
title_html = sx_call("events-entry-title-plain", name=entry.name)
|
||||
|
||||
# Badges
|
||||
badges_html = ""
|
||||
# Page badge (only show if different from current page title)
|
||||
page_badge_href = ""
|
||||
page_badge_title = ""
|
||||
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
|
||||
page_href = events_url_fn(f"/{page_slug}/")
|
||||
badges_html += sx_call("events-entry-page-badge",
|
||||
href=page_href, title=page_title)
|
||||
cal_name = getattr(entry, "calendar_name", "")
|
||||
if cal_name:
|
||||
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
|
||||
page_badge_href = events_url_fn(f"/{page_slug}/")
|
||||
page_badge_title = page_title
|
||||
|
||||
# Time line
|
||||
time_parts = ""
|
||||
if day_href and not is_page_scoped:
|
||||
time_parts += sx_call("events-entry-time-linked",
|
||||
href=day_href,
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))
|
||||
elif not is_page_scoped:
|
||||
time_parts += sx_call("events-entry-time-plain",
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))
|
||||
time_parts += entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_parts += f' \u2013 {entry.end_at.strftime("%H:%M")}'
|
||||
cal_name = getattr(entry, "calendar_name", "") or ""
|
||||
|
||||
# Time parts
|
||||
date_str = entry.start_at.strftime("%a %-d %b") if entry.start_at else ""
|
||||
start_time = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end_time = entry.end_at.strftime("%H:%M") if entry.end_at else ""
|
||||
|
||||
# Tile time string (combined)
|
||||
time_str_parts = []
|
||||
if date_str:
|
||||
time_str_parts.append(date_str)
|
||||
if start_time:
|
||||
time_str_parts.append(start_time)
|
||||
time_str = " \u00b7 ".join(time_str_parts)
|
||||
if end_time:
|
||||
time_str += f" \u2013 {end_time}"
|
||||
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_html = sx_call("events-entry-cost",
|
||||
cost=f"\u00a3{cost:.2f}") if cost else ""
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost else None
|
||||
|
||||
# Ticket widget
|
||||
# Ticket widget data
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
widget_html = ""
|
||||
if tp is not None:
|
||||
has_ticket = tp is not None
|
||||
ticket_data = None
|
||||
if has_ticket:
|
||||
qty = pending_tickets.get(entry.id, 0)
|
||||
widget_html = sx_call("events-entry-widget-wrapper",
|
||||
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
||||
ticket_data = {
|
||||
"entry-id": str(entry.id),
|
||||
"price": f"\u00a3{tp:.2f}",
|
||||
"qty": qty,
|
||||
"ticket-url": ticket_url,
|
||||
"csrf": _get_csrf(),
|
||||
}
|
||||
|
||||
return sx_call("events-entry-card",
|
||||
title=title_html, badges=SxExpr(badges_html),
|
||||
time_parts=SxExpr(time_parts), cost=SxExpr(cost_html),
|
||||
widget=SxExpr(widget_html))
|
||||
return {
|
||||
"entry-href": entry_href or None,
|
||||
"name": entry.name,
|
||||
"day-href": day_href or None,
|
||||
"page-badge-href": page_badge_href or None,
|
||||
"page-badge-title": page_badge_title or None,
|
||||
"cal-name": cal_name or None,
|
||||
"date-str": date_str,
|
||||
"start-time": start_time,
|
||||
"end-time": end_time or None,
|
||||
"time-str": time_str,
|
||||
"is-page-scoped": is_page_scoped or None,
|
||||
"cost": cost_str,
|
||||
"has-ticket": has_ticket or None,
|
||||
"ticket-data": ticket_data,
|
||||
}
|
||||
|
||||
|
||||
def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict,
|
||||
ticket_url: str, events_url_fn, *, is_page_scoped: bool = False,
|
||||
post: dict | None = None) -> str:
|
||||
"""Render a tile card for one event entry."""
|
||||
from .tickets import _ticket_widget_html
|
||||
pi = page_info.get(getattr(entry, "calendar_container_id", 0), {})
|
||||
if is_page_scoped and post:
|
||||
page_slug = pi.get("slug", post.get("slug", ""))
|
||||
else:
|
||||
page_slug = pi.get("slug", "")
|
||||
page_title = pi.get("title")
|
||||
def _get_csrf() -> str:
|
||||
"""Get CSRF token (lazy import)."""
|
||||
try:
|
||||
from flask_wtf.csrf import generate_csrf
|
||||
return generate_csrf()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
day_href = ""
|
||||
if page_slug and entry.start_at:
|
||||
day_href = events_url_fn(f"/{page_slug}/{entry.calendar_slug}/day/{entry.start_at.strftime('%Y/%-m/%-d')}/")
|
||||
entry_href = f"{day_href}entries/{entry.id}/" if day_href else ""
|
||||
|
||||
# Title
|
||||
if entry_href:
|
||||
title_html = sx_call("events-entry-title-tile-linked",
|
||||
href=entry_href, name=entry.name)
|
||||
else:
|
||||
title_html = sx_call("events-entry-title-tile-plain", name=entry.name)
|
||||
|
||||
# Badges
|
||||
badges_html = ""
|
||||
if page_title and (not is_page_scoped or page_title != (post or {}).get("title")):
|
||||
page_href = events_url_fn(f"/{page_slug}/")
|
||||
badges_html += sx_call("events-entry-page-badge",
|
||||
href=page_href, title=page_title)
|
||||
cal_name = getattr(entry, "calendar_name", "")
|
||||
if cal_name:
|
||||
badges_html += sx_call("events-entry-cal-badge", name=cal_name)
|
||||
|
||||
# Time
|
||||
time_html = ""
|
||||
if day_href:
|
||||
time_html += (sx_call("events-entry-time-linked",
|
||||
href=day_href,
|
||||
date_str=entry.start_at.strftime("%a %-d %b"))).replace(" · ", "")
|
||||
else:
|
||||
time_html += entry.start_at.strftime("%a %-d %b")
|
||||
time_html += f' \u00b7 {entry.start_at.strftime("%H:%M")}'
|
||||
if entry.end_at:
|
||||
time_html += f' \u2013 {entry.end_at.strftime("%H:%M")}'
|
||||
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_html = sx_call("events-entry-cost",
|
||||
cost=f"\u00a3{cost:.2f}") if cost else ""
|
||||
|
||||
# Ticket widget
|
||||
tp = getattr(entry, "ticket_price", None)
|
||||
widget_html = ""
|
||||
if tp is not None:
|
||||
qty = pending_tickets.get(entry.id, 0)
|
||||
widget_html = sx_call("events-entry-tile-widget-wrapper",
|
||||
widget=_ticket_widget_html(entry, qty, ticket_url, ctx={}))
|
||||
|
||||
return sx_call("events-entry-card-tile",
|
||||
title=title_html, badges=SxExpr(badges_html),
|
||||
time=SxExpr(time_html), cost=SxExpr(cost_html),
|
||||
widget=SxExpr(widget_html))
|
||||
def _entry_cards_data(entries, page_info, pending_tickets, ticket_url,
|
||||
events_url_fn, view, *, is_page_scoped=False, post=None) -> list:
|
||||
"""Extract data list for entry cards with date separators."""
|
||||
items = []
|
||||
last_date = None
|
||||
for entry in entries:
|
||||
if view != "tile":
|
||||
entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
|
||||
if entry_date != last_date:
|
||||
items.append({"is-separator": True, "date-str": entry_date})
|
||||
last_date = entry_date
|
||||
items.append(_entry_card_data(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
return items
|
||||
|
||||
|
||||
def _entry_cards_html(entries, page_info, pending_tickets, ticket_url,
|
||||
events_url_fn, view, page, has_more, next_url,
|
||||
*, is_page_scoped=False, post=None) -> str:
|
||||
"""Render entry cards (list or tile) with sentinel."""
|
||||
parts = []
|
||||
last_date = None
|
||||
for entry in entries:
|
||||
if view == "tile":
|
||||
parts.append(_entry_card_tile_html(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
else:
|
||||
entry_date = entry.start_at.strftime("%A %-d %B %Y") if entry.start_at else ""
|
||||
if entry_date != last_date:
|
||||
parts.append(sx_call("events-date-separator",
|
||||
date_str=entry_date))
|
||||
last_date = entry_date
|
||||
parts.append(_entry_card_html(
|
||||
entry, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
))
|
||||
|
||||
if has_more:
|
||||
parts.append(sx_call("sentinel-simple",
|
||||
id=f"sentinel-{page}", next_url=next_url))
|
||||
return "".join(parts)
|
||||
"""Render entry cards via sx defcomp with data extraction."""
|
||||
items = _entry_cards_data(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
view, is_page_scoped=is_page_scoped, post=post,
|
||||
)
|
||||
return sx_call("events-entry-cards-from-data",
|
||||
items=items, view=view, page=page,
|
||||
has_more=has_more, next_url=next_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -183,23 +138,15 @@ def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_
|
||||
*, is_page_scoped=False, post=None) -> str:
|
||||
"""Render the events main panel with view toggle + cards."""
|
||||
toggle = _view_toggle_html(ctx, view)
|
||||
|
||||
items = None
|
||||
if entries:
|
||||
cards = _entry_cards_html(
|
||||
items = _entry_cards_data(
|
||||
entries, page_info, pending_tickets, ticket_url, events_url_fn,
|
||||
view, page, has_more, next_url,
|
||||
is_page_scoped=is_page_scoped, post=post,
|
||||
view, is_page_scoped=is_page_scoped, post=post,
|
||||
)
|
||||
grid_cls = ("max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"
|
||||
if view == "tile" else "max-w-full px-3 py-3 space-y-3")
|
||||
body = sx_call("events-grid", grid_cls=grid_cls, cards=SxExpr(cards))
|
||||
else:
|
||||
body = sx_call("empty-state", icon="fa fa-calendar-xmark",
|
||||
message="No upcoming events",
|
||||
cls="px-3 py-12 text-center text-stone-400")
|
||||
|
||||
return sx_call("events-main-panel-body",
|
||||
toggle=toggle, body=body)
|
||||
return sx_call("events-main-panel-from-data",
|
||||
toggle=toggle, items=items, view=view, page=page,
|
||||
has_more=has_more, next_url=next_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -373,27 +320,16 @@ def _entry_nav_html(ctx: dict) -> str:
|
||||
entry_posts = ctx.get("entry_posts") or []
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
|
||||
blog_url_fn = ctx.get("blog_url")
|
||||
|
||||
parts = []
|
||||
|
||||
# Associated Posts scrolling menu
|
||||
# Associated Posts scrolling menu (strip OOB attr for inline embedding)
|
||||
if entry_posts:
|
||||
post_links = ""
|
||||
for ep in entry_posts:
|
||||
slug = getattr(ep, "slug", "")
|
||||
title = getattr(ep, "title", "")
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
|
||||
if feat:
|
||||
img_html = sx_call("events-post-img", src=feat, alt=title)
|
||||
else:
|
||||
img_html = sx_call("events-post-img-placeholder")
|
||||
post_links += sx_call("events-entry-nav-post-link",
|
||||
href=href, img=img_html, title=title)
|
||||
parts.append((sx_call("events-entry-posts-nav-oob",
|
||||
items=SxExpr(post_links))).replace(' :hx-swap-oob "true"', ''))
|
||||
posts_data = _entry_posts_nav_data(entry_posts, blog_url_fn)
|
||||
nav_html = sx_call("events-entry-posts-nav-inner-from-data", posts=posts_data or None)
|
||||
if nav_html:
|
||||
parts.append(nav_html.replace(' :hx-swap-oob "true"', ''))
|
||||
|
||||
# Admin link
|
||||
if is_admin:
|
||||
@@ -432,7 +368,7 @@ def _entry_title_html(entry) -> str:
|
||||
|
||||
|
||||
def _entry_options_html(entry, calendar, day, month, year) -> str:
|
||||
"""Render confirm/decline/provisional buttons based on entry state."""
|
||||
"""Render confirm/decline/provisional buttons via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -443,39 +379,30 @@ def _entry_options_html(entry, calendar, day, month, year) -> str:
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
state = getattr(entry, "state", "pending") or "pending"
|
||||
target = f"#calendar_entry_options_{eid}"
|
||||
|
||||
def _make_button(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
|
||||
url = url_for(
|
||||
f"calendar.day.calendar_entries.calendar_entry.{action_name}",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
||||
)
|
||||
btn_type = "button" if trigger_type == "button" else "submit"
|
||||
return sx_call("events-entry-option-button",
|
||||
url=url, target=target, csrf=csrf, btn_type=btn_type,
|
||||
action_btn=action_btn, confirm_title=confirm_title,
|
||||
confirm_text=confirm_text, label=label,
|
||||
is_btn=trigger_type == "button")
|
||||
def _btn_data(action_name, label, confirm_title, confirm_text, *, trigger_type="submit"):
|
||||
return {
|
||||
"url": url_for(f"calendar.day.calendar_entries.calendar_entry.{action_name}",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid),
|
||||
"csrf": csrf, "btn-type": "button" if trigger_type == "button" else "submit",
|
||||
"action-btn": action_btn, "confirm-title": confirm_title,
|
||||
"confirm-text": confirm_text, "label": label,
|
||||
"is-btn": True if trigger_type == "button" else None,
|
||||
}
|
||||
|
||||
buttons_html = ""
|
||||
buttons = []
|
||||
if state == "provisional":
|
||||
buttons_html += _make_button(
|
||||
"confirm_entry", "confirm",
|
||||
"Confirm entry?", "Are you sure you want to confirm this entry?",
|
||||
)
|
||||
buttons_html += _make_button(
|
||||
"decline_entry", "decline",
|
||||
"Decline entry?", "Are you sure you want to decline this entry?",
|
||||
)
|
||||
buttons.append(_btn_data("confirm_entry", "confirm",
|
||||
"Confirm entry?", "Are you sure you want to confirm this entry?"))
|
||||
buttons.append(_btn_data("decline_entry", "decline",
|
||||
"Decline entry?", "Are you sure you want to decline this entry?"))
|
||||
elif state == "confirmed":
|
||||
buttons_html += _make_button(
|
||||
"provisional_entry", "provisional",
|
||||
"Provisional entry?", "Are you sure you want to provisional this entry?",
|
||||
trigger_type="button",
|
||||
)
|
||||
buttons.append(_btn_data("provisional_entry", "provisional",
|
||||
"Provisional entry?", "Are you sure you want to provisional this entry?",
|
||||
trigger_type="button"))
|
||||
|
||||
return sx_call("events-entry-options",
|
||||
entry_id=str(eid), buttons=SxExpr(buttons_html))
|
||||
return sx_call("events-entry-options-from-data",
|
||||
entry_id=str(eid), buttons=buttons or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -525,7 +452,7 @@ def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
|
||||
"""Render associated posts list with remove buttons and search input."""
|
||||
"""Render associated posts list via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
@@ -533,38 +460,46 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
eid_s = str(eid)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
posts_html = ""
|
||||
posts_data = []
|
||||
if entry_posts:
|
||||
items = ""
|
||||
for ep in entry_posts:
|
||||
ep_title = getattr(ep, "title", "")
|
||||
ep_id = getattr(ep, "id", 0)
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
img_html = (sx_call("events-post-img", src=feat, alt=ep_title)
|
||||
if feat else sx_call("events-post-img-placeholder"))
|
||||
|
||||
del_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.remove_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, post_id=ep_id,
|
||||
)
|
||||
items += sx_call("events-entry-post-item",
|
||||
img=img_html, title=ep_title,
|
||||
del_url=del_url, entry_id=eid_s,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
|
||||
else:
|
||||
posts_html = sx_call("events-entry-posts-none")
|
||||
posts_data.append({
|
||||
"title": getattr(ep, "title", ""),
|
||||
"img": getattr(ep, "feature_image", None),
|
||||
"del-url": url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.remove_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, post_id=getattr(ep, "id", 0),
|
||||
),
|
||||
"csrf-hdr": csrf_hdr,
|
||||
})
|
||||
|
||||
search_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.search_posts",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid,
|
||||
)
|
||||
|
||||
return sx_call("events-entry-posts-panel",
|
||||
posts=posts_html, search_url=search_url,
|
||||
entry_id=eid_s)
|
||||
return sx_call("events-entry-posts-panel-from-data",
|
||||
entry_id=eid_s, posts=posts_data or None,
|
||||
search_url=search_url)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry posts nav data helper (shared by nav OOB + entry nav)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entry_posts_nav_data(entry_posts, blog_url_fn) -> list:
|
||||
"""Extract post nav data from ORM entry posts."""
|
||||
if not entry_posts:
|
||||
return []
|
||||
return [
|
||||
{"href": blog_url_fn(f"/{getattr(ep, 'slug', '')}/") if blog_url_fn else f"/{getattr(ep, 'slug', '')}/",
|
||||
"title": getattr(ep, "title", ""),
|
||||
"img": getattr(ep, "feature_image", None)}
|
||||
for ep in entry_posts
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -572,28 +507,15 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_entry_posts_nav_oob(entry_posts) -> str:
|
||||
"""Render OOB nav for entry posts (scrolling menu)."""
|
||||
"""Render OOB nav for entry posts via data extraction + sx defcomp."""
|
||||
from quart import g
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
||||
blog_url_fn = getattr(g, "blog_url", None)
|
||||
|
||||
if not entry_posts:
|
||||
return sx_call("events-entry-posts-nav-oob-empty")
|
||||
|
||||
items = ""
|
||||
for ep in entry_posts:
|
||||
slug = getattr(ep, "slug", "")
|
||||
title = getattr(ep, "title", "")
|
||||
feat = getattr(ep, "feature_image", None)
|
||||
href = blog_url_fn(f"/{slug}/") if blog_url_fn else f"/{slug}/"
|
||||
img_html = (sx_call("events-post-img", src=feat, alt=title)
|
||||
if feat else sx_call("events-post-img-placeholder"))
|
||||
items += sx_call("events-entry-nav-post",
|
||||
href=href, nav_btn=nav_btn,
|
||||
img=img_html, title=title)
|
||||
|
||||
return sx_call("events-entry-posts-nav-oob", items=SxExpr(items))
|
||||
posts_data = _entry_posts_nav_data(entry_posts, blog_url_fn)
|
||||
return sx_call("events-entry-posts-nav-oob-from-data",
|
||||
nav_btn=nav_btn, posts=posts_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -601,31 +523,28 @@ def render_entry_posts_nav_oob(entry_posts) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
|
||||
"""Render OOB nav for confirmed entries in a day."""
|
||||
"""Render OOB nav for confirmed entries via data extraction + sx defcomp."""
|
||||
from quart import url_for, g
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
nav_btn = getattr(styles, "nav_button", "") if hasattr(styles, "nav_button") else styles.get("nav_button", "")
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
if not confirmed_entries:
|
||||
return sx_call("events-day-entries-nav-oob-empty")
|
||||
entries_data = []
|
||||
if confirmed_entries:
|
||||
for entry in confirmed_entries:
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
entries_data.append({
|
||||
"href": url_for("defpage_entry_detail", calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
entry_id=entry.id),
|
||||
"name": entry.name,
|
||||
"time-str": start + end,
|
||||
})
|
||||
|
||||
items = ""
|
||||
for entry in confirmed_entries:
|
||||
href = url_for(
|
||||
"defpage_entry_detail",
|
||||
calendar_slug=cal_slug,
|
||||
year=day_date.year, month=day_date.month, day=day_date.day,
|
||||
entry_id=entry.id,
|
||||
)
|
||||
start = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
end = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
items += sx_call("events-day-nav-entry",
|
||||
href=href, nav_btn=nav_btn,
|
||||
name=entry.name, time_str=start + end)
|
||||
|
||||
return sx_call("events-day-entries-nav-oob", items=SxExpr(items))
|
||||
return sx_call("events-day-entries-nav-oob-from-data",
|
||||
nav_btn=nav_btn, entries=entries_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -633,7 +552,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."""
|
||||
"""Render OOB nav for associated entries and calendars via data + sx defcomp."""
|
||||
from quart import g
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
@@ -641,14 +560,9 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
||||
nav_btn = getattr(styles, "nav_button_less_pad", "") if hasattr(styles, "nav_button_less_pad") else styles.get("nav_button_less_pad", "")
|
||||
|
||||
has_entries = associated_entries and getattr(associated_entries, "entries", None)
|
||||
has_items = has_entries or calendars
|
||||
|
||||
if not has_items:
|
||||
return sx_call("events-post-nav-oob-empty")
|
||||
|
||||
slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
|
||||
|
||||
items = ""
|
||||
entries_data = []
|
||||
if has_entries:
|
||||
for entry in associated_entries.entries:
|
||||
entry_path = (
|
||||
@@ -656,27 +570,31 @@ def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/"
|
||||
f"entries/{entry.id}/"
|
||||
)
|
||||
href = events_url(entry_path)
|
||||
time_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
|
||||
end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
||||
items += sx_call("events-post-nav-entry",
|
||||
href=href, nav_btn=nav_btn,
|
||||
name=entry.name, time_str=time_str + end_str)
|
||||
entries_data.append({
|
||||
"href": events_url(entry_path),
|
||||
"name": entry.name,
|
||||
"time-str": time_str + end_str,
|
||||
})
|
||||
|
||||
calendars_data = []
|
||||
if calendars:
|
||||
for cal in calendars:
|
||||
cs = getattr(cal, "slug", "")
|
||||
local_href = events_url(f"/{slug}/{cs}/")
|
||||
items += sx_call("events-post-nav-calendar",
|
||||
href=local_href, nav_btn=nav_btn, name=cal.name)
|
||||
calendars_data.append({
|
||||
"href": events_url(f"/{slug}/{cs}/"),
|
||||
"name": cal.name,
|
||||
})
|
||||
|
||||
hs = ("on load or scroll "
|
||||
"if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth "
|
||||
"remove .hidden from .entries-nav-arrow add .flex to .entries-nav-arrow "
|
||||
"else add .hidden to .entries-nav-arrow remove .flex from .entries-nav-arrow end")
|
||||
|
||||
return sx_call("events-post-nav-wrapper",
|
||||
items=SxExpr(items), hyperscript=hs)
|
||||
return sx_call("events-post-nav-wrapper-from-data",
|
||||
nav_btn=nav_btn, entries=entries_data or None,
|
||||
calendars=calendars_data or None, hyperscript=hs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -800,42 +718,36 @@ def _entry_admin_main_panel_html(ctx: dict) -> str:
|
||||
|
||||
def render_post_search_results(search_posts, search_query, page, total_pages,
|
||||
entry, calendar, day, month, year) -> str:
|
||||
"""Render post search results (replaces _types/entry/_post_search_results.html)."""
|
||||
"""Render post search results via data extraction + sx defcomp."""
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
eid = entry.id
|
||||
post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid)
|
||||
|
||||
parts = []
|
||||
items_data = []
|
||||
for sp in search_posts:
|
||||
post_url = url_for("calendar.day.calendar_entries.calendar_entry.add_post",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid)
|
||||
feat = getattr(sp, "feature_image", None)
|
||||
title = getattr(sp, "title", "")
|
||||
if feat:
|
||||
img_html = sx_call("events-post-img", src=feat, alt=title)
|
||||
else:
|
||||
img_html = sx_call("events-post-img-placeholder")
|
||||
items_data.append({
|
||||
"post-url": post_url, "entry-id": str(eid),
|
||||
"csrf": csrf, "post-id": str(sp.id),
|
||||
"img": getattr(sp, "feature_image", None),
|
||||
"title": getattr(sp, "title", ""),
|
||||
})
|
||||
|
||||
parts.append(sx_call("events-post-search-item",
|
||||
post_url=post_url, entry_id=str(eid), csrf=csrf,
|
||||
post_id=str(sp.id), img=img_html, title=title))
|
||||
|
||||
result = "".join(parts)
|
||||
|
||||
if page < int(total_pages):
|
||||
has_more = page < int(total_pages)
|
||||
next_url = None
|
||||
if has_more:
|
||||
next_url = url_for("calendar.day.calendar_entries.calendar_entry.search_posts",
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year,
|
||||
entry_id=eid, q=search_query, page=page + 1)
|
||||
result += sx_call("events-post-search-sentinel",
|
||||
page=str(page), next_url=next_url)
|
||||
elif search_posts:
|
||||
result += sx_call("events-post-search-end")
|
||||
|
||||
return result
|
||||
return sx_call("events-post-search-results-from-data",
|
||||
items=items_data or None, page=str(page),
|
||||
next_url=next_url, has_more=has_more or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -846,7 +758,7 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
|
||||
"""Render entry edit form (replaces _types/entry/_edit.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from .slots import _slot_options_html, _SLOT_PICKER_JS
|
||||
from .slots import _slot_options_data, _SLOT_PICKER_JS
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
@@ -862,12 +774,9 @@ def render_entry_edit_form(entry, calendar, day, month, year, day_slots) -> str:
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
|
||||
|
||||
# Slot picker
|
||||
if day_slots:
|
||||
options_html = _slot_options_html(day_slots, selected_slot_id=getattr(entry, "slot_id", None))
|
||||
slot_picker_html = sx_call("events-slot-picker",
|
||||
id=f"entry-slot-{eid}", options=SxExpr(options_html))
|
||||
else:
|
||||
slot_picker_html = sx_call("events-no-slots")
|
||||
slots_data = _slot_options_data(day_slots, selected_slot_id=getattr(entry, "slot_id", None)) if day_slots else []
|
||||
slot_picker_html = sx_call("events-slot-picker-from-data",
|
||||
id=f"entry-slot-{eid}", slots=slots_data or None)
|
||||
|
||||
# Values
|
||||
start_val = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
||||
@@ -897,7 +806,7 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
|
||||
"""Render entry add form (replaces _types/day/_add.html)."""
|
||||
from quart import url_for, g
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from .slots import _slot_options_html, _SLOT_PICKER_JS
|
||||
from .slots import _slot_options_data, _SLOT_PICKER_JS
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
@@ -911,12 +820,9 @@ def render_entry_add_form(calendar, day, month, year, day_slots) -> str:
|
||||
calendar_slug=cal_slug, day=day, month=month, year=year)
|
||||
|
||||
# Slot picker
|
||||
if day_slots:
|
||||
options_html = _slot_options_html(day_slots)
|
||||
slot_picker_html = sx_call("events-slot-picker",
|
||||
id="entry-slot-new", options=SxExpr(options_html))
|
||||
else:
|
||||
slot_picker_html = sx_call("events-no-slots")
|
||||
slots_data = _slot_options_data(day_slots) if day_slots else []
|
||||
slot_picker_html = sx_call("events-slot-picker-from-data",
|
||||
id="entry-slot-new", slots=slots_data or None)
|
||||
|
||||
html = sx_call("events-entry-add-form",
|
||||
post_url=post_url, csrf=csrf,
|
||||
@@ -944,34 +850,33 @@ def render_entry_add_button(calendar, day, month, year) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
|
||||
"""Render container cards entries (replaces fragments/container_cards_entries.html)."""
|
||||
"""Render container cards entries via data extraction + sx defcomp."""
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
parts = []
|
||||
widgets_data = []
|
||||
for post_id in post_ids:
|
||||
parts.append(f"<!-- card-widget:{post_id} -->")
|
||||
widget_entries = batch.get(post_id, [])
|
||||
if widget_entries:
|
||||
cards_html = ""
|
||||
for entry in widget_entries:
|
||||
_post_slug = slug_map.get(post_id, "")
|
||||
_entry_path = (
|
||||
f"/{_post_slug}/{entry.calendar_slug}/"
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/"
|
||||
f"{entry.start_at.day}/entries/{entry.id}/"
|
||||
)
|
||||
time_str = entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
cards_html += sx_call("events-frag-entry-card",
|
||||
href=events_url(_entry_path),
|
||||
name=entry.name,
|
||||
date_str=entry.start_at.strftime("%a, %b %d"),
|
||||
time_str=time_str)
|
||||
parts.append(sx_call("events-frag-entries-widget", cards=SxExpr(cards_html)))
|
||||
parts.append(f"<!-- /card-widget:{post_id} -->")
|
||||
entries_data = []
|
||||
for entry in widget_entries:
|
||||
_post_slug = slug_map.get(post_id, "")
|
||||
_entry_path = (
|
||||
f"/{_post_slug}/{entry.calendar_slug}/"
|
||||
f"{entry.start_at.year}/{entry.start_at.month}/"
|
||||
f"{entry.start_at.day}/entries/{entry.id}/"
|
||||
)
|
||||
time_str = entry.start_at.strftime("%H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
entries_data.append({
|
||||
"href": events_url(_entry_path),
|
||||
"name": entry.name,
|
||||
"date-str": entry.start_at.strftime("%a, %b %d"),
|
||||
"time-str": time_str,
|
||||
})
|
||||
widgets_data.append({"entries": entries_data or None})
|
||||
|
||||
return "\n".join(parts)
|
||||
return sx_call("events-frag-container-cards-from-data",
|
||||
widgets=widgets_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -979,32 +884,23 @@ def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_account_tickets(tickets) -> str:
|
||||
"""Render account page tickets (replaces fragments/account_page_tickets.html)."""
|
||||
"""Render account page tickets via data extraction + sx defcomp."""
|
||||
from shared.infrastructure.urls import events_url
|
||||
|
||||
tickets_data = []
|
||||
if tickets:
|
||||
items_html = ""
|
||||
for ticket in tickets:
|
||||
href = events_url(f"/tickets/{ticket.code}/")
|
||||
date_str = ticket.entry_start_at.strftime("%d %b %Y, %H:%M")
|
||||
cal_name = ""
|
||||
if getattr(ticket, "calendar_name", None):
|
||||
cal_name = f'<span>· {escape(ticket.calendar_name)}</span>'
|
||||
type_name = ""
|
||||
if getattr(ticket, "ticket_type_name", None):
|
||||
type_name = f'<span>· {escape(ticket.ticket_type_name)}</span>'
|
||||
badge_html = sx_call("status-pill",
|
||||
status=getattr(ticket, "state", ""))
|
||||
items_html += sx_call("events-frag-ticket-item",
|
||||
href=href, entry_name=ticket.entry_name,
|
||||
date_str=date_str, calendar_name=cal_name,
|
||||
type_name=type_name, badge=badge_html)
|
||||
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
|
||||
else:
|
||||
body = sx_call("empty-state", message="No tickets yet.",
|
||||
cls="text-sm text-stone-500")
|
||||
tickets_data.append({
|
||||
"href": events_url(f"/tickets/{ticket.code}/"),
|
||||
"entry-name": ticket.entry_name,
|
||||
"date-str": ticket.entry_start_at.strftime("%d %b %Y, %H:%M"),
|
||||
"calendar-name": getattr(ticket, "calendar_name", None) or None,
|
||||
"type-name": getattr(ticket, "ticket_type_name", None) or None,
|
||||
"state": getattr(ticket, "state", ""),
|
||||
})
|
||||
|
||||
return sx_call("events-frag-tickets-panel", items=body)
|
||||
return sx_call("events-frag-tickets-panel-from-data",
|
||||
tickets=tickets_data or None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1012,31 +908,18 @@ def render_fragment_account_tickets(tickets) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def render_fragment_account_bookings(bookings) -> str:
|
||||
"""Render account page bookings (replaces fragments/account_page_bookings.html)."""
|
||||
"""Render account page bookings via data extraction + sx defcomp."""
|
||||
bookings_data = []
|
||||
if bookings:
|
||||
items_html = ""
|
||||
for booking in bookings:
|
||||
date_str = booking.start_at.strftime("%d %b %Y, %H:%M")
|
||||
if getattr(booking, "end_at", None):
|
||||
date_str_extra = f'<span>– {escape(booking.end_at.strftime("%H:%M"))}</span>'
|
||||
else:
|
||||
date_str_extra = ""
|
||||
cal_name = ""
|
||||
if getattr(booking, "calendar_name", None):
|
||||
cal_name = f'<span>· {escape(booking.calendar_name)}</span>'
|
||||
cost_str = ""
|
||||
if getattr(booking, "cost", None):
|
||||
cost_str = f'<span>· £{escape(str(booking.cost))}</span>'
|
||||
badge_html = sx_call("status-pill",
|
||||
status=getattr(booking, "state", ""))
|
||||
items_html += sx_call("events-frag-booking-item",
|
||||
name=booking.name,
|
||||
date_str=date_str + date_str_extra,
|
||||
calendar_name=cal_name, cost_str=cost_str,
|
||||
badge=badge_html)
|
||||
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
|
||||
else:
|
||||
body = sx_call("empty-state", message="No bookings yet.",
|
||||
cls="text-sm text-stone-500")
|
||||
bookings_data.append({
|
||||
"name": booking.name,
|
||||
"date-str": booking.start_at.strftime("%d %b %Y, %H:%M"),
|
||||
"end-time": booking.end_at.strftime("%H:%M") if getattr(booking, "end_at", None) else None,
|
||||
"calendar-name": getattr(booking, "calendar_name", None) or None,
|
||||
"cost-str": str(booking.cost) if getattr(booking, "cost", None) else None,
|
||||
"state": getattr(booking, "state", ""),
|
||||
})
|
||||
|
||||
return sx_call("events-frag-bookings-panel", items=body)
|
||||
return sx_call("events-frag-bookings-panel-from-data",
|
||||
bookings=bookings_data or None)
|
||||
|
||||
Reference in New Issue
Block a user