"""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)