Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s
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>
926 lines
38 KiB
Python
926 lines
38 KiB
Python
"""Entry panels, cards, forms, edit/add."""
|
|
from __future__ import annotations
|
|
|
|
from shared.sx.helpers import sx_call
|
|
from shared.sx.parser import SxExpr
|
|
|
|
from .utils import (
|
|
_entry_state_badge_html,
|
|
_list_container, _view_toggle_html,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All events / page summary entry cards — data extraction
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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) -> 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", ""))
|
|
else:
|
|
page_slug = pi.get("slug", "")
|
|
page_title = pi.get("title")
|
|
|
|
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 ""
|
|
|
|
# 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_badge_href = events_url_fn(f"/{page_slug}/")
|
|
page_badge_title = page_title
|
|
|
|
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_str = f"\u00a3{cost:.2f}" if cost else None
|
|
|
|
# Ticket widget data
|
|
tp = getattr(entry, "ticket_price", None)
|
|
has_ticket = tp is not None
|
|
ticket_data = None
|
|
if has_ticket:
|
|
qty = pending_tickets.get(entry.id, 0)
|
|
ticket_data = {
|
|
"entry-id": str(entry.id),
|
|
"price": f"\u00a3{tp:.2f}",
|
|
"qty": qty,
|
|
"ticket-url": ticket_url,
|
|
"csrf": _get_csrf(),
|
|
}
|
|
|
|
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 _get_csrf() -> str:
|
|
"""Get CSRF token (lazy import)."""
|
|
try:
|
|
from flask_wtf.csrf import generate_csrf
|
|
return generate_csrf()
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
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 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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# All events / page summary main panels
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info,
|
|
page, view, ticket_url, next_url, events_url_fn,
|
|
*, 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:
|
|
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-main-panel-from-data",
|
|
toggle=toggle, items=items, view=view, page=page,
|
|
has_more=has_more, next_url=next_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_main_panel_html(ctx: dict) -> str:
|
|
"""Render the entry detail panel (name, slot, time, state, cost, tickets,
|
|
buy form, date, posts, options + edit button)."""
|
|
from quart import url_for
|
|
from .tickets import render_buy_form
|
|
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
|
|
calendar = ctx.get("calendar")
|
|
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
styles = ctx.get("styles") or {}
|
|
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
|
|
eid = entry.id
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
|
|
def _field(label, content_html):
|
|
return sx_call("events-entry-field", label=label, content=SxExpr(content_html))
|
|
|
|
# Name
|
|
name_html = _field("Name", sx_call("events-entry-name-field", name=entry.name))
|
|
|
|
# Slot
|
|
slot = getattr(entry, "slot", None)
|
|
if slot:
|
|
flex_label = "(flexible)" if getattr(slot, "flexible", False) else "(fixed)"
|
|
slot_inner = sx_call("events-entry-slot-assigned",
|
|
slot_name=slot.name, flex_label=flex_label)
|
|
else:
|
|
slot_inner = sx_call("events-entry-slot-none")
|
|
slot_html = _field("Slot", slot_inner)
|
|
|
|
# Time Period
|
|
start_str = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
|
end_str = f" \u2013 {entry.end_at.strftime('%H:%M')}" if entry.end_at else " \u2013 open-ended"
|
|
time_html = _field("Time Period", sx_call("events-entry-time-field",
|
|
time_str=start_str + end_str))
|
|
|
|
# State
|
|
state_html = _field("State", sx_call("events-entry-state-field",
|
|
entry_id=str(eid),
|
|
badge=_entry_state_badge_html(state)))
|
|
|
|
# Cost
|
|
cost = getattr(entry, "cost", None)
|
|
cost_str = f"{cost:.2f}" if cost is not None else "0.00"
|
|
cost_html = _field("Cost", sx_call("events-entry-cost-field",
|
|
cost=f"\u00a3{cost_str}"))
|
|
|
|
# Ticket Configuration (admin)
|
|
tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
|
|
entry_id=str(eid),
|
|
tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year))))
|
|
|
|
# Buy Tickets (public-facing)
|
|
ticket_remaining = ctx.get("ticket_remaining")
|
|
ticket_sold_count = ctx.get("ticket_sold_count", 0)
|
|
user_ticket_count = ctx.get("user_ticket_count", 0)
|
|
user_ticket_counts_by_type = ctx.get("user_ticket_counts_by_type") or {}
|
|
buy_html = render_buy_form(
|
|
entry, ticket_remaining, ticket_sold_count,
|
|
user_ticket_count, user_ticket_counts_by_type,
|
|
)
|
|
|
|
# Date
|
|
date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
|
|
date_html = _field("Date", sx_call("events-entry-date-field", date_str=date_str))
|
|
|
|
# Associated Posts
|
|
entry_posts = ctx.get("entry_posts") or []
|
|
posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
|
|
entry_id=str(eid),
|
|
posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))
|
|
|
|
# Options and Edit Button
|
|
edit_url = url_for(
|
|
"calendar.day.calendar_entries.calendar_entry.get_edit",
|
|
entry_id=eid, calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
)
|
|
|
|
return sx_call("events-entry-panel",
|
|
entry_id=str(eid), list_container=list_container,
|
|
name=name_html, slot=slot_html,
|
|
time=time_html, state=state_html,
|
|
cost=cost_html, tickets=tickets_html,
|
|
buy=SxExpr(buy_html), date=date_html,
|
|
posts=posts_html,
|
|
options=_entry_options_html(entry, calendar, day, month, year),
|
|
pre_action=pre_action, edit_url=edit_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry header row
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build entry detail header row."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
|
|
link_href = url_for(
|
|
"defpage_entry_detail",
|
|
calendar_slug=cal_slug,
|
|
year=year, month=month, day=day,
|
|
entry_id=entry.id,
|
|
)
|
|
label_html = sx_call("events-entry-label",
|
|
entry_id=str(entry.id),
|
|
title=_entry_title_html(entry),
|
|
times=SxExpr(_entry_times_html(entry)))
|
|
|
|
nav_html = _entry_nav_html(ctx)
|
|
|
|
return sx_call("menu-row-sx", id="entry-row", level=5,
|
|
link_href=link_href, link_label_content=label_html,
|
|
nav=SxExpr(nav_html) if nav_html else None, child_id="entry-header-child", oob=oob)
|
|
|
|
|
|
def _entry_times_html(entry) -> str:
|
|
"""Render entry times label."""
|
|
start = entry.start_at
|
|
end = entry.end_at
|
|
if not start:
|
|
return ""
|
|
start_str = start.strftime("%H:%M")
|
|
end_str = f" \u2192 {end.strftime('%H:%M')}" if end else ""
|
|
return sx_call("events-entry-times", time_str=start_str + end_str)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry nav (desktop + admin link)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_nav_html(ctx: dict) -> str:
|
|
"""Entry desktop nav: associated posts scrolling menu + admin link."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
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 (strip OOB attr for inline embedding)
|
|
if entry_posts:
|
|
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:
|
|
admin_url = url_for(
|
|
"defpage_entry_admin",
|
|
calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
entry_id=entry.id,
|
|
)
|
|
parts.append(sx_call("events-entry-admin-link", href=admin_url))
|
|
|
|
return "".join(parts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry optioned (confirm/decline/provisional response)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_optioned(entry, calendar, day, month, year) -> str:
|
|
"""Render entry options buttons + OOB title & state swaps."""
|
|
options = _entry_options_html(entry, calendar, day, month, year)
|
|
title = _entry_title_html(entry)
|
|
state = _entry_state_badge_html(getattr(entry, "state", "pending") or "pending")
|
|
|
|
return options + sx_call("events-entry-optioned-oob",
|
|
entry_id=str(entry.id),
|
|
title=title, state=state)
|
|
|
|
|
|
def _entry_title_html(entry) -> str:
|
|
"""Render entry title (icon + name + state badge)."""
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
return sx_call("events-entry-title",
|
|
name=entry.name,
|
|
badge=_entry_state_badge_html(state))
|
|
|
|
|
|
def _entry_options_html(entry, calendar, day, month, year) -> str:
|
|
"""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()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
action_btn = getattr(styles, "action_button", "") if hasattr(styles, "action_button") else styles.get("action_button", "")
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
state = getattr(entry, "state", "pending") or "pending"
|
|
|
|
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 = []
|
|
if state == "provisional":
|
|
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.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-from-data",
|
|
entry_id=str(eid), buttons=buttons or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry tickets config (display + form)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_tickets_config(entry, calendar, day, month, year) -> str:
|
|
"""Render ticket config display + edit form for admin entry view."""
|
|
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
|
|
tp = getattr(entry, "ticket_price", None)
|
|
tc = getattr(entry, "ticket_count", None)
|
|
eid_s = str(eid)
|
|
show_js = f"document.getElementById('ticket-form-{eid}').classList.remove('hidden'); this.classList.add('hidden');"
|
|
hide_js = (f"document.getElementById('ticket-form-{eid}').classList.add('hidden'); "
|
|
f"document.getElementById('entry-tickets-{eid}').querySelectorAll('button:not([type=submit])').forEach(btn => btn.classList.remove('hidden'));")
|
|
|
|
if tp is not None:
|
|
tc_str = f"{tc} tickets" if tc is not None else "Unlimited"
|
|
display_html = sx_call("events-ticket-config-display",
|
|
price_str=f"\u00a3{tp:.2f}",
|
|
count_str=tc_str, show_js=show_js)
|
|
else:
|
|
display_html = sx_call("events-ticket-config-none", show_js=show_js)
|
|
|
|
update_url = url_for(
|
|
"calendar.day.calendar_entries.calendar_entry.update_tickets",
|
|
entry_id=eid, calendar_slug=cal_slug, day=day, month=month, year=year,
|
|
)
|
|
hidden_cls = "" if tp is None else "hidden"
|
|
tp_val = f"{tp:.2f}" if tp is not None else ""
|
|
tc_val = str(tc) if tc is not None else ""
|
|
|
|
form_html = sx_call("events-ticket-config-form",
|
|
entry_id=eid_s, hidden_cls=hidden_cls,
|
|
update_url=update_url, csrf=csrf,
|
|
price_val=tp_val, count_val=tc_val, hide_js=hide_js)
|
|
return display_html + form_html
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry posts panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) -> str:
|
|
"""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()
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
eid_s = str(eid)
|
|
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
|
|
|
posts_data = []
|
|
if entry_posts:
|
|
for ep in entry_posts:
|
|
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-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
|
|
]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry posts nav OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_entry_posts_nav_oob(entry_posts) -> str:
|
|
"""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)
|
|
|
|
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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day entries nav OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str:
|
|
"""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", "")
|
|
|
|
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,
|
|
})
|
|
|
|
return sx_call("events-day-entries-nav-oob-from-data",
|
|
nav_btn=nav_btn, entries=entries_data or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post nav entries OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_post_nav_entries_oob(associated_entries, calendars, post) -> str:
|
|
"""Render OOB nav for associated entries and calendars via data + sx defcomp."""
|
|
from quart import g
|
|
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", "")
|
|
|
|
has_entries = associated_entries and getattr(associated_entries, "entries", None)
|
|
slug = post.get("slug", "") if isinstance(post, dict) else getattr(post, "slug", "")
|
|
|
|
entries_data = []
|
|
if has_entries:
|
|
for entry in associated_entries.entries:
|
|
entry_path = (
|
|
f"/{slug}/{entry.calendar_slug}/"
|
|
f"{entry.start_at.year}/{entry.start_at.month}/{entry.start_at.day}/"
|
|
f"entries/{entry.id}/"
|
|
)
|
|
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 ""
|
|
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", "")
|
|
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-from-data",
|
|
nav_btn=nav_btn, entries=entries_data or None,
|
|
calendars=calendars_data or None, hyperscript=hs)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar description display + edit form
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_calendar_description(calendar, *, oob: bool = False) -> str:
|
|
"""Render calendar description display with edit button, optionally with OOB title."""
|
|
from quart import url_for
|
|
from .calendar import _calendar_description_display_html
|
|
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
|
html = _calendar_description_display_html(calendar, edit_url)
|
|
|
|
if oob:
|
|
desc = getattr(calendar, "description", "") or ""
|
|
html += sx_call("events-calendar-description-title-oob",
|
|
description=desc)
|
|
return html
|
|
|
|
|
|
def render_calendar_description_edit(calendar) -> str:
|
|
"""Render calendar description edit form."""
|
|
from quart import url_for
|
|
from shared.browser.app.csrf import generate_csrf_token
|
|
csrf = generate_csrf_token()
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
desc = getattr(calendar, "description", "") or ""
|
|
|
|
save_url = url_for("calendar.admin.calendar_description_save", calendar_slug=cal_slug)
|
|
cancel_url = url_for("calendar.admin.calendar_description_view", calendar_slug=cal_slug)
|
|
|
|
return sx_call("events-calendar-description-edit-form",
|
|
save_url=save_url, cancel_url=cancel_url,
|
|
csrf=csrf, description=desc)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry admin page / OOB
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _entry_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the entry admin header row."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
|
|
link_href = url_for(
|
|
"defpage_entry_admin",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
|
)
|
|
# Nav: ticket_types link
|
|
nav_html = _entry_admin_nav_html(ctx)
|
|
|
|
return sx_call("menu-row-sx", id="entry-admin-row", level=6,
|
|
link_href=link_href, link_label="admin", icon="fa fa-cog",
|
|
nav=nav_html or None, child_id="entry-admin-header-child", oob=oob)
|
|
|
|
|
|
def _entry_admin_nav_html(ctx: dict) -> str:
|
|
"""Entry admin nav: ticket_types link."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
entry = ctx.get("entry")
|
|
if not entry:
|
|
return ""
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
select_colours = ctx.get("select_colours", "")
|
|
|
|
href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
|
|
calendar_slug=cal_slug, entry_id=entry.id,
|
|
year=year, month=month, day=day)
|
|
return sx_call("nav-link", href=href, label="ticket_types",
|
|
select_colours=select_colours)
|
|
|
|
|
|
def _entry_admin_main_panel_html(ctx: dict) -> str:
|
|
"""Entry admin main panel: just a ticket_types link."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
entry = ctx.get("entry")
|
|
if not calendar or not entry:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day = ctx.get("day")
|
|
month = ctx.get("month")
|
|
year = ctx.get("year")
|
|
styles = ctx.get("styles") or {}
|
|
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
select_colours = ctx.get("select_colours", "")
|
|
|
|
href = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.get",
|
|
calendar_slug=cal_slug, entry_id=entry.id,
|
|
year=year, month=month, day=day)
|
|
return sx_call("nav-link", href=href, label="ticket_types",
|
|
select_colours=select_colours, aclass=nav_btn,
|
|
is_selected=False)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post search results
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_post_search_results(search_posts, search_query, page, total_pages,
|
|
entry, calendar, day, month, year) -> str:
|
|
"""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)
|
|
|
|
items_data = []
|
|
for sp in search_posts:
|
|
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", ""),
|
|
})
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry edit form
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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_data, _SLOT_PICKER_JS
|
|
csrf = generate_csrf_token()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
list_container = styles.get("list_container", "") if isinstance(styles, dict) else getattr(styles, "list_container", "")
|
|
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
|
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
eid = entry.id
|
|
|
|
put_url = url_for("calendar.day.calendar_entries.calendar_entry.put",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
|
|
cancel_url = url_for("defpage_entry_detail",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=eid)
|
|
|
|
# Slot picker
|
|
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 ""
|
|
end_val = entry.end_at.strftime("%H:%M") if entry.end_at else ""
|
|
cost = getattr(entry, "cost", None)
|
|
cost_display = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
|
tp = getattr(entry, "ticket_price", None)
|
|
tc = getattr(entry, "ticket_count", None)
|
|
tp_val = f"{tp:.2f}" if tp is not None else ""
|
|
tc_val = str(tc) if tc is not None else ""
|
|
|
|
html = sx_call("events-entry-edit-form",
|
|
entry_id=str(eid), list_container=list_container,
|
|
put_url=put_url, cancel_url=cancel_url, csrf=csrf,
|
|
name_val=entry.name or "", slot_picker=slot_picker_html,
|
|
start_val=start_val, end_val=end_val, cost_display=cost_display,
|
|
ticket_price_val=tp_val, ticket_count_val=tc_val,
|
|
action_btn=action_btn, cancel_btn=cancel_btn)
|
|
return html + _SLOT_PICKER_JS
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Entry add form / button
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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_data, _SLOT_PICKER_JS
|
|
csrf = generate_csrf_token()
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
action_btn = styles.get("action_button", "") if isinstance(styles, dict) else getattr(styles, "action_button", "")
|
|
cancel_btn = styles.get("cancel_button", "") if isinstance(styles, dict) else getattr(styles, "cancel_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
|
|
post_url = url_for("calendar.day.calendar_entries.add_entry",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year)
|
|
cancel_url = url_for("calendar.day.calendar_entries.add_button",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year)
|
|
|
|
# Slot picker
|
|
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,
|
|
slot_picker=slot_picker_html,
|
|
action_btn=action_btn, cancel_btn=cancel_btn,
|
|
cancel_url=cancel_url)
|
|
return html + _SLOT_PICKER_JS
|
|
|
|
|
|
def render_entry_add_button(calendar, day, month, year) -> str:
|
|
"""Render entry add button (replaces _types/day/_add_button.html)."""
|
|
from quart import url_for, g
|
|
|
|
styles = getattr(g, "styles", None) or {}
|
|
pre_action = styles.get("pre_action_button", "") if isinstance(styles, dict) else getattr(styles, "pre_action_button", "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
add_url = url_for("calendar.day.calendar_entries.add_form",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year)
|
|
|
|
return sx_call("events-entry-add-button", pre_action=pre_action, add_url=add_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fragment: container cards entries
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_fragment_container_cards(batch, post_ids, slug_map) -> str:
|
|
"""Render container cards entries via data extraction + sx defcomp."""
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
widgets_data = []
|
|
for post_id in post_ids:
|
|
widget_entries = batch.get(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 sx_call("events-frag-container-cards-from-data",
|
|
widgets=widgets_data or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fragment: account page tickets
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_fragment_account_tickets(tickets) -> str:
|
|
"""Render account page tickets via data extraction + sx defcomp."""
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
tickets_data = []
|
|
if tickets:
|
|
for ticket in tickets:
|
|
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-from-data",
|
|
tickets=tickets_data or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Fragment: account page bookings
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def render_fragment_account_bookings(bookings) -> str:
|
|
"""Render account page bookings via data extraction + sx defcomp."""
|
|
bookings_data = []
|
|
if bookings:
|
|
for booking in bookings:
|
|
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-from-data",
|
|
bookings=bookings_data or None)
|