"""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, _list_container, _view_toggle_html, ) # --------------------------------------------------------------------------- # All events / page summary entry cards # --------------------------------------------------------------------------- def _entry_card_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 list 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") 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 (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 = "" 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 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")}' 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-widget-wrapper", widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) 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)) 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") 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_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) # --------------------------------------------------------------------------- # 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) if entries: cards = _entry_cards_html( entries, page_info, pending_tickets, ticket_url, events_url_fn, view, page, has_more, next_url, 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) # --------------------------------------------------------------------------- # 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 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"', '')) # 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 based on entry state.""" 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" 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") buttons_html = "" 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?", ) elif state == "confirmed": buttons_html += _make_button( "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)) # --------------------------------------------------------------------------- # 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 with remove buttons and search input.""" 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) posts_html = "" 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={"X-CSRFToken": csrf}) posts_html = sx_call("events-entry-posts-list", items=SxExpr(items)) else: posts_html = sx_call("events-entry-posts-none") 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) # --------------------------------------------------------------------------- # Entry posts nav OOB # --------------------------------------------------------------------------- def render_entry_posts_nav_oob(entry_posts) -> str: """Render OOB nav for entry posts (scrolling menu).""" 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)) # --------------------------------------------------------------------------- # Day entries nav OOB # --------------------------------------------------------------------------- def render_day_entries_nav_oob(confirmed_entries, calendar, day_date) -> str: """Render OOB nav for confirmed entries in a day.""" 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") 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)) # --------------------------------------------------------------------------- # Post nav entries OOB # --------------------------------------------------------------------------- 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.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) 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 = "" 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}/" ) 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) 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) 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) # --------------------------------------------------------------------------- # 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 (replaces _types/entry/_post_search_results.html).""" 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 parts = [] 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") 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): 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 # --------------------------------------------------------------------------- # 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_html, _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 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") # 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_html, _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 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") 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 (replaces fragments/container_cards_entries.html).""" from shared.infrastructure.urls import events_url parts = [] for post_id in post_ids: parts.append(f"") 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"") return "\n".join(parts) # --------------------------------------------------------------------------- # Fragment: account page tickets # --------------------------------------------------------------------------- def render_fragment_account_tickets(tickets) -> str: """Render account page tickets (replaces fragments/account_page_tickets.html).""" from shared.infrastructure.urls import events_url 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'· {escape(ticket.calendar_name)}' type_name = "" if getattr(ticket, "ticket_type_name", None): type_name = f'· {escape(ticket.ticket_type_name)}' 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") return sx_call("events-frag-tickets-panel", items=body) # --------------------------------------------------------------------------- # Fragment: account page bookings # --------------------------------------------------------------------------- def render_fragment_account_bookings(bookings) -> str: """Render account page bookings (replaces fragments/account_page_bookings.html).""" 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'– {escape(booking.end_at.strftime("%H:%M"))}' else: date_str_extra = "" cal_name = "" if getattr(booking, "calendar_name", None): cal_name = f'· {escape(booking.calendar_name)}' cost_str = "" if getattr(booking, "cost", None): cost_str = f'· £{escape(str(booking.cost))}' 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") return sx_call("events-frag-bookings-panel", items=body)