Move events composition from Python to .sx defcomps (Phase 9)
Convert all 14 events page helpers from returning sx_call() strings to returning data dicts. Defpage expressions compose SX components with data bindings using map/fn/if/when. Complex sub-panels (entry tickets config, buy form, posts panel, options buttons, entry nav menu) returned as SxExpr from existing render functions which remain for HTMX handler use. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,89 +1,235 @@
|
||||
;; Events pages — auto-mounted with absolute paths
|
||||
;; All helpers return data dicts — markup composition in SX.
|
||||
|
||||
;; Calendar admin
|
||||
(defpage calendar-admin
|
||||
:path "/<slug>/<calendar_slug>/admin/"
|
||||
:auth :admin
|
||||
:layout :events-calendar-admin
|
||||
:content (calendar-admin-content calendar-slug))
|
||||
:data (calendar-admin-data calendar-slug)
|
||||
:content (~events-calendar-admin-panel
|
||||
:description-content (~events-calendar-description-display
|
||||
:description cal-description :edit-url desc-edit-url)
|
||||
:csrf csrf :description cal-description))
|
||||
|
||||
;; Day admin
|
||||
(defpage day-admin
|
||||
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/admin/"
|
||||
:auth :admin
|
||||
:layout :events-day-admin
|
||||
:content (day-admin-content calendar-slug year month day))
|
||||
:data (day-admin-data calendar-slug year month day)
|
||||
:content (~events-day-admin-panel))
|
||||
|
||||
;; Slots listing
|
||||
(defpage slots-listing
|
||||
:path "/<slug>/<calendar_slug>/slots/"
|
||||
:auth :public
|
||||
:layout :events-slots
|
||||
:content (slots-content calendar-slug))
|
||||
:data (slots-data calendar-slug)
|
||||
:content (~events-slots-table
|
||||
:list-container list-container
|
||||
:rows (if has-slots
|
||||
(<> (map (fn (s)
|
||||
(~events-slots-row
|
||||
:tr-cls tr-cls :slot-href (get s "slot-href")
|
||||
:pill-cls pill-cls :hx-select hx-select
|
||||
:slot-name (get s "name") :description (get s "description")
|
||||
:flexible (get s "flexible")
|
||||
:days (if (get s "has-days")
|
||||
(~events-slot-days-pills :days-inner
|
||||
(<> (map (fn (d) (~events-slot-day-pill :day d)) (get s "day-list"))))
|
||||
(~events-slot-no-days))
|
||||
:time-str (get s "time-str")
|
||||
:cost-str (get s "cost-str") :action-btn action-btn
|
||||
:del-url (get s "del-url")
|
||||
:csrf-hdr csrf-hdr))
|
||||
slots-list))
|
||||
(~events-slots-empty-row))
|
||||
:pre-action pre-action :add-url add-url))
|
||||
|
||||
;; Slot detail
|
||||
(defpage slot-detail
|
||||
:path "/<slug>/<calendar_slug>/slots/<int:slot_id>/"
|
||||
:auth :admin
|
||||
:layout :events-slot
|
||||
:content (slot-content calendar-slug slot-id))
|
||||
:data (slot-data calendar-slug slot-id)
|
||||
:content (~events-slot-panel
|
||||
:slot-id slot-id-str
|
||||
:list-container list-container
|
||||
:days (if has-days
|
||||
(~events-slot-days-pills :days-inner
|
||||
(<> (map (fn (d) (~events-slot-day-pill :day d)) day-list)))
|
||||
(~events-slot-no-days))
|
||||
:flexible flexible
|
||||
:time-str time-str :cost-str cost-str
|
||||
:pre-action pre-action :edit-url edit-url))
|
||||
|
||||
;; Entry detail
|
||||
(defpage entry-detail
|
||||
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/"
|
||||
:auth :admin
|
||||
:layout :events-entry
|
||||
:content (entry-content calendar-slug entry-id)
|
||||
:menu (entry-menu calendar-slug entry-id))
|
||||
:data (entry-data calendar-slug entry-id)
|
||||
:content (~events-entry-panel
|
||||
:entry-id entry-id-str :list-container list-container
|
||||
:name (~events-entry-field :label "Name"
|
||||
:content (~events-entry-name-field :name entry-name))
|
||||
:slot (~events-entry-field :label "Slot"
|
||||
:content (if has-slot
|
||||
(~events-entry-slot-assigned :slot-name slot-name :flex-label flex-label)
|
||||
(~events-entry-slot-none)))
|
||||
:time (~events-entry-field :label "Time Period"
|
||||
:content (~events-entry-time-field :time-str time-str))
|
||||
:state (~events-entry-field :label "State"
|
||||
:content (~events-entry-state-field :entry-id entry-id-str
|
||||
:badge (~badge :cls state-badge-cls :label state-badge-label)))
|
||||
:cost (~events-entry-field :label "Cost"
|
||||
:content (~events-entry-cost-field :cost cost-str))
|
||||
:tickets (~events-entry-field :label "Tickets"
|
||||
:content (~events-entry-tickets-field :entry-id entry-id-str
|
||||
:tickets-config tickets-config))
|
||||
:buy buy-form
|
||||
:date (~events-entry-field :label "Date"
|
||||
:content (~events-entry-date-field :date-str date-str))
|
||||
:posts (~events-entry-field :label "Associated Posts"
|
||||
:content (~events-entry-posts-field :entry-id entry-id-str
|
||||
:posts-panel posts-panel))
|
||||
:options options-html
|
||||
:pre-action pre-action :edit-url edit-url)
|
||||
:menu entry-menu)
|
||||
|
||||
;; Entry admin
|
||||
(defpage entry-admin
|
||||
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/admin/"
|
||||
:auth :admin
|
||||
:layout :events-entry-admin
|
||||
:content (entry-admin-content calendar-slug entry-id)
|
||||
:menu (admin-menu))
|
||||
:data (entry-admin-data calendar-slug entry-id year month day)
|
||||
:content (~nav-link :href ticket-types-href :label "ticket_types"
|
||||
:select-colours select-colours :aclass nav-btn :is-selected false)
|
||||
:menu (~events-admin-placeholder-nav))
|
||||
|
||||
;; Ticket types listing
|
||||
(defpage ticket-types-listing
|
||||
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/ticket-types/"
|
||||
:auth :public
|
||||
:layout :events-ticket-types
|
||||
:content (ticket-types-content calendar-slug entry-id year month day)
|
||||
:menu (admin-menu))
|
||||
:data (ticket-types-data calendar-slug entry-id year month day)
|
||||
:content (~events-ticket-types-table
|
||||
:list-container list-container
|
||||
:rows (if has-types
|
||||
(<> (map (fn (tt)
|
||||
(~events-ticket-types-row
|
||||
:tr-cls tr-cls :tt-href (get tt "tt-href")
|
||||
:pill-cls pill-cls :hx-select hx-select
|
||||
:tt-name (get tt "tt-name") :cost-str (get tt "cost-str")
|
||||
:count (get tt "count") :action-btn action-btn
|
||||
:del-url (get tt "del-url")
|
||||
:csrf-hdr csrf-hdr))
|
||||
types-list))
|
||||
(~events-ticket-types-empty-row))
|
||||
:action-btn action-btn :add-url add-url)
|
||||
:menu (~events-admin-placeholder-nav))
|
||||
|
||||
;; Ticket type detail
|
||||
(defpage ticket-type-detail
|
||||
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/ticket-types/<int:ticket_type_id>/"
|
||||
:auth :admin
|
||||
:layout :events-ticket-type
|
||||
:content (ticket-type-content calendar-slug entry-id ticket-type-id year month day)
|
||||
:menu (admin-menu))
|
||||
:data (ticket-type-data calendar-slug entry-id ticket-type-id year month day)
|
||||
:content (~events-ticket-type-panel
|
||||
:ticket-id ticket-id :list-container list-container
|
||||
:c1 (~events-ticket-type-col :label "Name" :value tt-name)
|
||||
:c2 (~events-ticket-type-col :label "Cost" :value cost-str)
|
||||
:c3 (~events-ticket-type-col :label "Count" :value count-str)
|
||||
:pre-action pre-action :edit-url edit-url)
|
||||
:menu (~events-admin-placeholder-nav))
|
||||
|
||||
;; My tickets
|
||||
(defpage my-tickets
|
||||
:path "/tickets/"
|
||||
:auth :public
|
||||
:layout :root
|
||||
:content (tickets-content))
|
||||
:data (tickets-data)
|
||||
:content (~events-tickets-panel
|
||||
:list-container list-container
|
||||
:has-tickets has-tickets
|
||||
:cards (when has-tickets
|
||||
(<> (map (fn (t)
|
||||
(~events-ticket-card
|
||||
:href (get t "href") :entry-name (get t "entry-name")
|
||||
:type-name (get t "type-name") :time-str (get t "time-str")
|
||||
:cal-name (get t "cal-name")
|
||||
:badge (~badge :cls (get t "badge-cls") :label (get t "badge-label"))
|
||||
:code-prefix (get t "code-prefix")))
|
||||
tickets-list)))))
|
||||
|
||||
;; Ticket detail
|
||||
(defpage ticket-detail
|
||||
:path "/tickets/<code>/"
|
||||
:auth :public
|
||||
:layout :root
|
||||
:content (ticket-detail-content code))
|
||||
:data (ticket-detail-data code)
|
||||
:content (~events-ticket-detail
|
||||
:list-container list-container :back-href back-href
|
||||
:header-bg header-bg :entry-name entry-name
|
||||
:badge (span :class (str "inline-flex items-center rounded-full px-3 py-1 text-sm font-medium " badge-cls)
|
||||
badge-label)
|
||||
:type-name type-name :code ticket-code
|
||||
:time-date time-date :time-range time-range
|
||||
:cal-name cal-name :type-desc type-desc :checkin-str checkin-str
|
||||
:qr-script qr-script))
|
||||
|
||||
;; Ticket admin dashboard
|
||||
(defpage ticket-admin
|
||||
:path "/admin/tickets/"
|
||||
:auth :admin
|
||||
:layout :root
|
||||
:content (ticket-admin-content))
|
||||
:data (ticket-admin-data)
|
||||
:content (~events-ticket-admin-panel
|
||||
:list-container list-container
|
||||
:stats (<> (map (fn (s)
|
||||
(~events-ticket-admin-stat
|
||||
:border (get s "border") :bg (get s "bg")
|
||||
:text-cls (get s "text-cls") :label-cls (get s "label-cls")
|
||||
:value (get s "value") :label (get s "label")))
|
||||
admin-stats))
|
||||
:lookup-url lookup-url :has-tickets has-tickets
|
||||
:rows (when has-tickets
|
||||
(<> (map (fn (t)
|
||||
(~events-ticket-admin-row
|
||||
:code (get t "code") :code-short (get t "code-short")
|
||||
:entry-name (get t "entry-name")
|
||||
:date (when (get t "date-str")
|
||||
(~events-ticket-admin-date :date-str (get t "date-str")))
|
||||
:type-name (get t "type-name")
|
||||
:badge (~badge :cls (get t "badge-cls") :label (get t "badge-label"))
|
||||
:action (if (get t "can-checkin")
|
||||
(~events-ticket-admin-checkin-form
|
||||
:checkin-url (get t "checkin-url") :code (get t "code") :csrf csrf)
|
||||
(when (get t "is-checked-in")
|
||||
(~events-ticket-admin-checked-in :time-str (get t "checkin-time"))))))
|
||||
admin-tickets)))))
|
||||
|
||||
;; Markets
|
||||
(defpage events-markets
|
||||
:path "/<slug>/markets/"
|
||||
:auth :public
|
||||
:layout :events-markets
|
||||
:content (markets-content))
|
||||
:data (markets-data)
|
||||
:content (~crud-panel
|
||||
:list-id "markets-list"
|
||||
:form (when can-create
|
||||
(~crud-create-form :create-url create-url :csrf csrf
|
||||
:errors-id "market-create-errors" :list-id "markets-list"
|
||||
:placeholder "e.g. Farm Shop, Bakery" :btn-label "Add market"))
|
||||
:list (if markets-list
|
||||
(<> (map (fn (m)
|
||||
(~crud-item :href (get m "href") :name (get m "name")
|
||||
:slug (get m "slug") :del-url (get m "del-url")
|
||||
:csrf-hdr (get m "csrf-hdr")
|
||||
:list-id "markets-list"
|
||||
:confirm-title "Delete market?"
|
||||
:confirm-text "Products will be hidden (soft delete)"))
|
||||
markets-list))
|
||||
(~empty-state :message "No markets yet. Create one above."
|
||||
:cls "text-gray-500 mt-4"))))
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
"""Layout registrations, page helpers, and shared hydration helpers."""
|
||||
"""Layout registrations, page helpers, and shared hydration helpers.
|
||||
|
||||
All helpers return data dicts — no sx_call().
|
||||
Markup composition lives entirely in .sx defpage and .sx defcomp files.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from shared.sx.helpers import sx_call
|
||||
|
||||
from .calendar import (
|
||||
_calendar_admin_main_panel_html,
|
||||
_day_admin_main_panel_html,
|
||||
_markets_main_panel_html,
|
||||
)
|
||||
from .entries import (
|
||||
_entry_main_panel_html,
|
||||
_entry_nav_html,
|
||||
_entry_admin_main_panel_html,
|
||||
)
|
||||
from .tickets import (
|
||||
_tickets_main_panel_html, _ticket_detail_panel_html,
|
||||
_ticket_admin_main_panel_html,
|
||||
render_ticket_type_main_panel, render_ticket_types_table,
|
||||
)
|
||||
from .slots import render_slot_main_panel, render_slots_table
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -261,6 +248,60 @@ def _register_events_layouts() -> None:
|
||||
"events-markets-layout-full", "events-markets-layout-oob")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Badge data helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_ENTRY_STATE_CLASSES = {
|
||||
"confirmed": "bg-emerald-100 text-emerald-800",
|
||||
"provisional": "bg-amber-100 text-amber-800",
|
||||
"ordered": "bg-sky-100 text-sky-800",
|
||||
"pending": "bg-stone-100 text-stone-700",
|
||||
"declined": "bg-red-100 text-red-800",
|
||||
}
|
||||
|
||||
_TICKET_STATE_CLASSES = {
|
||||
"confirmed": "bg-emerald-100 text-emerald-800",
|
||||
"checked_in": "bg-blue-100 text-blue-800",
|
||||
"reserved": "bg-amber-100 text-amber-800",
|
||||
"cancelled": "bg-red-100 text-red-800",
|
||||
}
|
||||
|
||||
|
||||
def _entry_badge_data(state: str) -> dict:
|
||||
cls = _ENTRY_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700")
|
||||
label = state.replace("_", " ").capitalize()
|
||||
return {"cls": cls, "label": label}
|
||||
|
||||
|
||||
def _ticket_badge_data(state: str) -> dict:
|
||||
cls = _TICKET_STATE_CLASSES.get(state, "bg-stone-100 text-stone-700")
|
||||
label = (state or "").replace("_", " ").capitalize()
|
||||
return {"cls": cls, "label": label}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Styles helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _styles_data() -> dict:
|
||||
"""Extract common style classes from g.styles."""
|
||||
from quart import g
|
||||
styles = getattr(g, "styles", None) or {}
|
||||
|
||||
def _gs(attr):
|
||||
return getattr(styles, attr, "") if hasattr(styles, attr) else styles.get(attr, "")
|
||||
|
||||
return {
|
||||
"list-container": _gs("list_container"),
|
||||
"pre-action": _gs("pre_action_button"),
|
||||
"action-btn": _gs("action_button"),
|
||||
"tr-cls": _gs("tr"),
|
||||
"pill-cls": _gs("pill"),
|
||||
"nav-btn": _gs("nav_button"),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -269,141 +310,468 @@ def _register_events_helpers() -> None:
|
||||
from shared.sx.pages import register_page_helpers
|
||||
|
||||
register_page_helpers("events", {
|
||||
"calendar-admin-content": _h_calendar_admin_content,
|
||||
"day-admin-content": _h_day_admin_content,
|
||||
"slots-content": _h_slots_content,
|
||||
"slot-content": _h_slot_content,
|
||||
"entry-content": _h_entry_content,
|
||||
"entry-menu": _h_entry_menu,
|
||||
"entry-admin-content": _h_entry_admin_content,
|
||||
"admin-menu": _h_admin_menu,
|
||||
"ticket-types-content": _h_ticket_types_content,
|
||||
"ticket-type-content": _h_ticket_type_content,
|
||||
"tickets-content": _h_tickets_content,
|
||||
"ticket-detail-content": _h_ticket_detail_content,
|
||||
"ticket-admin-content": _h_ticket_admin_content,
|
||||
"markets-content": _h_markets_content,
|
||||
"calendar-admin-data": _h_calendar_admin_data,
|
||||
"day-admin-data": _h_day_admin_data,
|
||||
"slots-data": _h_slots_data,
|
||||
"slot-data": _h_slot_data,
|
||||
"entry-data": _h_entry_data,
|
||||
"entry-admin-data": _h_entry_admin_data,
|
||||
"ticket-types-data": _h_ticket_types_data,
|
||||
"ticket-type-data": _h_ticket_type_data,
|
||||
"tickets-data": _h_tickets_data,
|
||||
"ticket-detail-data": _h_ticket_detail_data,
|
||||
"ticket-admin-data": _h_ticket_admin_data,
|
||||
"markets-data": _h_markets_data,
|
||||
})
|
||||
|
||||
|
||||
async def _h_calendar_admin_content(calendar_slug=None, **kw):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calendar admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_calendar_admin_data(calendar_slug=None, **kw) -> dict:
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_container_nav_defpage_ctx()
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _calendar_admin_main_panel_html(ctx)
|
||||
|
||||
from quart import g
|
||||
calendar = getattr(g, "calendar", None)
|
||||
if not calendar:
|
||||
return {}
|
||||
|
||||
csrf = generate_csrf_token()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
desc = getattr(calendar, "description", "") or ""
|
||||
desc_edit_url = url_for("calendar.admin.calendar_description_edit",
|
||||
calendar_slug=cal_slug)
|
||||
|
||||
return {
|
||||
"cal-description": desc,
|
||||
"csrf": csrf,
|
||||
"desc-edit-url": desc_edit_url,
|
||||
}
|
||||
|
||||
|
||||
async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Day admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_day_admin_data(calendar_slug=None, year=None, month=None,
|
||||
day=None, **kw) -> dict:
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_container_nav_defpage_ctx()
|
||||
if year is not None:
|
||||
await _ensure_day_data(int(year), int(month), int(day))
|
||||
return _day_admin_main_panel_html({})
|
||||
return {}
|
||||
|
||||
|
||||
async def _h_slots_content(calendar_slug=None, **kw):
|
||||
from quart import g
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slots listing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_slots_data(calendar_slug=None, **kw) -> dict:
|
||||
from quart import g, url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from bp.slots.services.slots import list_slots as svc_list_slots
|
||||
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_container_nav_defpage_ctx()
|
||||
|
||||
calendar = getattr(g, "calendar", None)
|
||||
from bp.slots.services.slots import list_slots as svc_list_slots
|
||||
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
|
||||
_add_to_defpage_ctx(slots=slots)
|
||||
return render_slots_table(slots, calendar)
|
||||
|
||||
styles = _styles_data()
|
||||
csrf = generate_csrf_token()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||
|
||||
slots_list = []
|
||||
for s in slots:
|
||||
slot_href = url_for("defpage_slot_detail",
|
||||
calendar_slug=cal_slug, slot_id=s.id)
|
||||
del_url = url_for("calendar.slots.slot.slot_delete",
|
||||
calendar_slug=cal_slug, slot_id=s.id)
|
||||
desc = getattr(s, "description", "") or ""
|
||||
days_display = getattr(s, "days_display", "\u2014")
|
||||
day_list = days_display.split(", ")
|
||||
has_days = bool(day_list and day_list[0] != "\u2014")
|
||||
time_start = s.time_start.strftime("%H:%M") if s.time_start else ""
|
||||
time_end = s.time_end.strftime("%H:%M") if s.time_end else ""
|
||||
cost = getattr(s, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
|
||||
slots_list.append({
|
||||
"name": s.name,
|
||||
"description": desc,
|
||||
"day-list": day_list if has_days else [],
|
||||
"has-days": has_days,
|
||||
"flexible": "yes" if s.flexible else "no",
|
||||
"time-str": f"{time_start} - {time_end}",
|
||||
"cost-str": cost_str,
|
||||
"slot-href": slot_href,
|
||||
"del-url": del_url,
|
||||
})
|
||||
|
||||
return {
|
||||
"has-slots": bool(slots),
|
||||
"slots-list": slots_list,
|
||||
"add-url": add_url,
|
||||
"csrf-hdr": csrf_hdr,
|
||||
"hx-select": hx_select,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
|
||||
from quart import g, abort
|
||||
# ---------------------------------------------------------------------------
|
||||
# Slot detail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_slot_data(calendar_slug=None, slot_id=None, **kw) -> dict:
|
||||
from quart import g, abort, url_for
|
||||
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_container_nav_defpage_ctx()
|
||||
|
||||
from bp.slot.services.slot import get_slot as svc_get_slot
|
||||
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
|
||||
if not slot:
|
||||
abort(404)
|
||||
g.slot = slot
|
||||
_add_to_defpage_ctx(slot=slot)
|
||||
|
||||
calendar = getattr(g, "calendar", None)
|
||||
return render_slot_main_panel(slot, calendar)
|
||||
styles = _styles_data()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
|
||||
days_display = getattr(slot, "days_display", "\u2014")
|
||||
day_list = days_display.split(", ")
|
||||
has_days = bool(day_list and day_list[0] != "\u2014")
|
||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||
time_end = slot.time_end.strftime("%H:%M") if slot.time_end else ""
|
||||
cost = getattr(slot, "cost", None)
|
||||
cost_str = f"{cost:.2f}" if cost is not None else ""
|
||||
edit_url = url_for("calendar.slots.slot.get_edit",
|
||||
slot_id=slot.id, calendar_slug=cal_slug)
|
||||
|
||||
return {
|
||||
"slot-id-str": str(slot.id),
|
||||
"day-list": day_list if has_days else [],
|
||||
"has-days": has_days,
|
||||
"flexible": "yes" if getattr(slot, "flexible", False) else "no",
|
||||
"time-str": f"{time_start} \u2014 {time_end}",
|
||||
"cost-str": cost_str,
|
||||
"edit-url": edit_url,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
async def _h_entry_content(calendar_slug=None, entry_id=None, **kw):
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry detail (complex — sub-panels returned as SxExpr)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_entry_data(calendar_slug=None, entry_id=None, **kw) -> dict:
|
||||
from quart import url_for, g
|
||||
from .entries import (
|
||||
_entry_nav_html,
|
||||
_entry_options_html,
|
||||
render_entry_tickets_config,
|
||||
render_entry_posts_panel,
|
||||
)
|
||||
from .tickets import render_buy_form
|
||||
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry_context(entry_id)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _entry_main_panel_html(ctx)
|
||||
|
||||
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 = _styles_data()
|
||||
eid = entry.id
|
||||
state = getattr(entry, "state", "pending") or "pending"
|
||||
|
||||
# Simple field data
|
||||
slot = getattr(entry, "slot", None)
|
||||
has_slot = slot is not None
|
||||
slot_name = slot.name if slot else ""
|
||||
flex_label = "(flexible)" if slot and getattr(slot, "flexible", False) else "(fixed)"
|
||||
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"
|
||||
cost = getattr(entry, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
date_str = entry.start_at.strftime("%A, %B %d, %Y") if entry.start_at else ""
|
||||
badge = _entry_badge_data(state)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# Complex sub-panels (pre-composed as SxExpr)
|
||||
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 {}
|
||||
entry_posts = ctx.get("entry_posts") or []
|
||||
|
||||
tickets_config = render_entry_tickets_config(entry, calendar, day, month, year)
|
||||
buy_form = render_buy_form(
|
||||
entry, ticket_remaining, ticket_sold_count,
|
||||
user_ticket_count, user_ticket_counts_by_type,
|
||||
)
|
||||
posts_panel = render_entry_posts_panel(
|
||||
entry_posts, entry, calendar, day, month, year,
|
||||
)
|
||||
options_html = _entry_options_html(entry, calendar, day, month, year)
|
||||
|
||||
# Entry menu (pre-composed for :menu slot)
|
||||
entry_menu = _entry_nav_html(ctx)
|
||||
|
||||
return {
|
||||
"entry-id-str": str(eid),
|
||||
"entry-name": entry.name,
|
||||
"has-slot": has_slot,
|
||||
"slot-name": slot_name,
|
||||
"flex-label": flex_label,
|
||||
"time-str": start_str + end_str,
|
||||
"state-badge-cls": badge["cls"],
|
||||
"state-badge-label": badge["label"],
|
||||
"cost-str": cost_str,
|
||||
"date-str": date_str,
|
||||
"edit-url": edit_url,
|
||||
"tickets-config": SxExpr(tickets_config),
|
||||
"buy-form": SxExpr(buy_form) if buy_form else None,
|
||||
"posts-panel": SxExpr(posts_panel),
|
||||
"options-html": SxExpr(options_html),
|
||||
"entry-menu": SxExpr(entry_menu) if entry_menu else None,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry_context(entry_id)
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _entry_nav_html(ctx)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry admin
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_entry_admin_data(calendar_slug=None, entry_id=None,
|
||||
year=None, month=None, day=None, **kw) -> dict:
|
||||
from quart import url_for, g
|
||||
|
||||
async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_container_nav_defpage_ctx()
|
||||
await _ensure_entry_context(entry_id)
|
||||
|
||||
calendar = getattr(g, "calendar", None)
|
||||
entry = getattr(g, "entry", None)
|
||||
if not calendar or not entry:
|
||||
return {}
|
||||
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
styles = _styles_data()
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _entry_admin_main_panel_html(ctx)
|
||||
select_colours = ctx.get("select_colours", "")
|
||||
|
||||
ticket_types_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 {
|
||||
"ticket-types-href": ticket_types_href,
|
||||
"select-colours": select_colours,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
def _h_admin_menu():
|
||||
return sx_call("events-admin-placeholder-nav")
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket types listing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_ticket_types_data(calendar_slug=None, entry_id=None,
|
||||
year=None, month=None, day=None, **kw) -> dict:
|
||||
from quart import g, url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
async def _h_ticket_types_content(calendar_slug=None, entry_id=None,
|
||||
year=None, month=None, day=None, **kw):
|
||||
from quart import g
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry(entry_id)
|
||||
|
||||
entry = getattr(g, "entry", None)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
|
||||
from bp.ticket_types.services.tickets import list_ticket_types as svc_list_ticket_types
|
||||
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
|
||||
_add_to_defpage_ctx(ticket_types=ticket_types)
|
||||
return render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
|
||||
|
||||
styles = _styles_data()
|
||||
csrf = generate_csrf_token()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
eid = entry.id if entry else 0
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
|
||||
types_list = []
|
||||
for tt in (ticket_types or []):
|
||||
tt_href = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
del_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete",
|
||||
calendar_slug=cal_slug, year=year, month=month, day=day,
|
||||
entry_id=eid, ticket_type_id=tt.id,
|
||||
)
|
||||
cost = getattr(tt, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
|
||||
types_list.append({
|
||||
"tt-href": tt_href,
|
||||
"tt-name": tt.name,
|
||||
"cost-str": cost_str,
|
||||
"count": str(tt.count),
|
||||
"del-url": del_url,
|
||||
})
|
||||
|
||||
add_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.add_form",
|
||||
calendar_slug=cal_slug, entry_id=eid, year=year, month=month, day=day,
|
||||
)
|
||||
|
||||
return {
|
||||
"has-types": bool(ticket_types),
|
||||
"types-list": types_list,
|
||||
"add-url": add_url,
|
||||
"csrf-hdr": csrf_hdr,
|
||||
"hx-select": hx_select,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
async def _h_ticket_type_content(calendar_slug=None, entry_id=None,
|
||||
ticket_type_id=None, year=None, month=None, day=None, **kw):
|
||||
from quart import g, abort
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket type detail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_ticket_type_data(calendar_slug=None, entry_id=None,
|
||||
ticket_type_id=None,
|
||||
year=None, month=None, day=None, **kw) -> dict:
|
||||
from quart import g, abort, url_for
|
||||
|
||||
await _ensure_calendar(calendar_slug)
|
||||
await _ensure_entry(entry_id)
|
||||
|
||||
from bp.ticket_type.services.ticket import get_ticket_type as svc_get_ticket_type
|
||||
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
|
||||
if not ticket_type:
|
||||
abort(404)
|
||||
g.ticket_type = ticket_type
|
||||
_add_to_defpage_ctx(ticket_type=ticket_type)
|
||||
|
||||
entry = getattr(g, "entry", None)
|
||||
calendar = getattr(g, "calendar", None)
|
||||
return render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
|
||||
styles = _styles_data()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
cost = getattr(ticket_type, "cost", None)
|
||||
cost_str = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
||||
count = getattr(ticket_type, "count", 0)
|
||||
|
||||
edit_url = url_for(
|
||||
"calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit",
|
||||
ticket_type_id=ticket_type.id, calendar_slug=cal_slug,
|
||||
year=year, month=month, day=day,
|
||||
entry_id=entry.id if entry else 0,
|
||||
)
|
||||
|
||||
return {
|
||||
"ticket-id": str(ticket_type.id),
|
||||
"tt-name": ticket_type.name,
|
||||
"cost-str": cost_str,
|
||||
"count-str": str(count),
|
||||
"edit-url": edit_url,
|
||||
**styles,
|
||||
}
|
||||
|
||||
|
||||
async def _h_tickets_content(**kw):
|
||||
from quart import g
|
||||
# ---------------------------------------------------------------------------
|
||||
# My tickets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_tickets_data(**kw) -> dict:
|
||||
from quart import g, url_for
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from bp.tickets.services.tickets import get_user_tickets
|
||||
|
||||
ident = current_cart_identity()
|
||||
tickets = await get_user_tickets(
|
||||
g.s,
|
||||
user_id=ident["user_id"],
|
||||
session_id=ident["session_id"],
|
||||
)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _tickets_main_panel_html(ctx, tickets)
|
||||
styles = ctx.get("styles") or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
|
||||
tickets_list = []
|
||||
for ticket in (tickets or []):
|
||||
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||
entry = getattr(ticket, "entry", None)
|
||||
entry_name = entry.name if entry else "Unknown event"
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
|
||||
time_str = ""
|
||||
if entry and entry.start_at:
|
||||
time_str = entry.start_at.strftime("%A, %B %d, %Y at %H:%M")
|
||||
if entry.end_at:
|
||||
time_str += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
|
||||
badge = _ticket_badge_data(state)
|
||||
tickets_list.append({
|
||||
"href": href,
|
||||
"entry-name": entry_name,
|
||||
"type-name": tt.name if tt else None,
|
||||
"time-str": time_str or None,
|
||||
"cal-name": cal.name if cal else None,
|
||||
"badge-cls": badge["cls"],
|
||||
"badge-label": badge["label"],
|
||||
"code-prefix": ticket.code[:8],
|
||||
})
|
||||
|
||||
return {
|
||||
"has-tickets": bool(tickets),
|
||||
"tickets-list": tickets_list,
|
||||
"list-container": list_container,
|
||||
}
|
||||
|
||||
|
||||
async def _h_ticket_detail_content(code=None, **kw):
|
||||
from quart import g, abort
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket detail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_ticket_detail_data(code=None, **kw) -> dict:
|
||||
from quart import g, abort, url_for
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from bp.tickets.services.tickets import get_ticket_by_code
|
||||
|
||||
ticket = await get_ticket_by_code(g.s, code) if code else None
|
||||
if not ticket:
|
||||
abort(404)
|
||||
@@ -417,16 +785,71 @@ async def _h_ticket_detail_content(code=None, **kw):
|
||||
abort(404)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _ticket_detail_panel_html(ctx, ticket)
|
||||
styles = ctx.get("styles") or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
ticket_code = ticket.code
|
||||
cal = getattr(entry, "calendar", None) if entry else None
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50",
|
||||
"reserved": "bg-amber-50"}
|
||||
header_bg = bg_map.get(state, "bg-stone-50")
|
||||
entry_name = entry.name if entry else "Ticket"
|
||||
back_href = url_for("defpage_my_tickets")
|
||||
|
||||
badge = _ticket_badge_data(state)
|
||||
|
||||
time_date = entry.start_at.strftime("%A, %B %d, %Y") if entry and entry.start_at else None
|
||||
time_range = entry.start_at.strftime("%H:%M") if entry and entry.start_at else None
|
||||
if time_range and entry.end_at:
|
||||
time_range += f" \u2013 {entry.end_at.strftime('%H:%M')}"
|
||||
|
||||
tt_desc = f"{tt.name} \u2014 \u00a3{tt.cost:.2f}" if tt and getattr(tt, "cost", None) else None
|
||||
checkin_str = checked_in_at.strftime("Checked in: %B %d, %Y at %H:%M") if checked_in_at else None
|
||||
|
||||
qr_script = (
|
||||
f"(function(){{var c=document.getElementById('ticket-qr-{ticket_code}');"
|
||||
"if(c&&typeof QRCode!=='undefined'){"
|
||||
"var cv=document.createElement('canvas');"
|
||||
f"QRCode.toCanvas(cv,'{ticket_code}',{{width:200,margin:2,color:{{dark:'#1c1917',light:'#ffffff'}}}},function(e){{if(!e)c.appendChild(cv)}});"
|
||||
"}})()"
|
||||
)
|
||||
|
||||
return {
|
||||
"list-container": list_container,
|
||||
"back-href": back_href,
|
||||
"header-bg": header_bg,
|
||||
"entry-name": entry_name,
|
||||
"badge-cls": badge["cls"],
|
||||
"badge-label": badge["label"],
|
||||
"type-name": tt.name if tt else None,
|
||||
"ticket-code": ticket_code,
|
||||
"time-date": time_date,
|
||||
"time-range": time_range,
|
||||
"cal-name": cal.name if cal else None,
|
||||
"type-desc": tt_desc,
|
||||
"checkin-str": checkin_str,
|
||||
"qr-script": qr_script,
|
||||
}
|
||||
|
||||
|
||||
async def _h_ticket_admin_content(**kw):
|
||||
from quart import g
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ticket admin dashboard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_ticket_admin_data(**kw) -> dict:
|
||||
from quart import g, url_for
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from models.calendars import CalendarEntry, Ticket
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
result = await g.s.execute(
|
||||
select(Ticket)
|
||||
@@ -449,20 +872,118 @@ async def _h_ticket_admin_content(**kw):
|
||||
reserved = await g.s.scalar(
|
||||
select(func.count(Ticket.id)).where(Ticket.state == "reserved")
|
||||
)
|
||||
stats = {
|
||||
"total": total or 0,
|
||||
"confirmed": confirmed or 0,
|
||||
"checked_in": checked_in or 0,
|
||||
"reserved": reserved or 0,
|
||||
|
||||
csrf = generate_csrf_token()
|
||||
lookup_url = url_for("ticket_admin.lookup")
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
styles = ctx.get("styles") or {}
|
||||
list_container = getattr(styles, "list_container", "") if hasattr(styles, "list_container") else styles.get("list_container", "")
|
||||
|
||||
# Stats cards data
|
||||
admin_stats = []
|
||||
for label, key, border, bg, text_cls in [
|
||||
("Total", "total", "border-stone-200", "", "text-stone-900"),
|
||||
("Confirmed", "confirmed", "border-emerald-200", "bg-emerald-50", "text-emerald-700"),
|
||||
("Checked In", "checked_in", "border-blue-200", "bg-blue-50", "text-blue-700"),
|
||||
("Reserved", "reserved", "border-amber-200", "bg-amber-50", "text-amber-700"),
|
||||
]:
|
||||
val_map = {"total": total, "confirmed": confirmed,
|
||||
"checked_in": checked_in, "reserved": reserved}
|
||||
val = val_map.get(key, 0) or 0
|
||||
lbl_cls = text_cls.replace("700", "600").replace("900", "500") if "stone" not in text_cls else "text-stone-500"
|
||||
admin_stats.append({
|
||||
"border": border, "bg": bg, "text-cls": text_cls,
|
||||
"label-cls": lbl_cls, "value": str(val), "label": label,
|
||||
})
|
||||
|
||||
# Ticket rows data
|
||||
admin_tickets = []
|
||||
for ticket in tickets:
|
||||
entry = getattr(ticket, "entry", None)
|
||||
tt = getattr(ticket, "ticket_type", None)
|
||||
state = getattr(ticket, "state", "")
|
||||
tcode = ticket.code
|
||||
checked_in_at = getattr(ticket, "checked_in_at", None)
|
||||
|
||||
date_str = None
|
||||
if entry and entry.start_at:
|
||||
date_str = entry.start_at.strftime("%d %b %Y, %H:%M")
|
||||
|
||||
badge = _ticket_badge_data(state)
|
||||
can_checkin = state in ("confirmed", "reserved")
|
||||
is_checked_in = state == "checked_in"
|
||||
checkin_url = url_for("ticket_admin.do_checkin", code=tcode) if can_checkin else None
|
||||
checkin_time = checked_in_at.strftime("%H:%M") if checked_in_at else ""
|
||||
|
||||
admin_tickets.append({
|
||||
"code": tcode,
|
||||
"code-short": tcode[:12] + "...",
|
||||
"entry-name": entry.name if entry else "\u2014",
|
||||
"date-str": date_str,
|
||||
"type-name": tt.name if tt else "\u2014",
|
||||
"badge-cls": badge["cls"],
|
||||
"badge-label": badge["label"],
|
||||
"can-checkin": can_checkin,
|
||||
"is-checked-in": is_checked_in,
|
||||
"checkin-url": checkin_url,
|
||||
"checkin-time": checkin_time,
|
||||
})
|
||||
|
||||
return {
|
||||
"admin-stats": admin_stats,
|
||||
"admin-tickets": admin_tickets,
|
||||
"list-container": list_container,
|
||||
"lookup-url": lookup_url,
|
||||
"csrf": csrf,
|
||||
"has-tickets": bool(tickets),
|
||||
}
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _ticket_admin_main_panel_html(ctx, tickets, stats)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Markets
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_markets_data(**kw) -> dict:
|
||||
from quart import url_for
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from shared.sx.helpers import call_url
|
||||
|
||||
async def _h_markets_content(**kw):
|
||||
_ensure_post_defpage_ctx()
|
||||
|
||||
from shared.sx.page import get_template_context
|
||||
ctx = await get_template_context()
|
||||
return _markets_main_panel_html(ctx)
|
||||
|
||||
rights = ctx.get("rights") or {}
|
||||
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
||||
has_access = ctx.get("has_access")
|
||||
can_create = has_access("markets.create_market") if callable(has_access) else is_admin
|
||||
csrf = generate_csrf_token()
|
||||
markets_raw = ctx.get("markets") or []
|
||||
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
|
||||
markets_list = []
|
||||
for m in markets_raw:
|
||||
m_slug = getattr(m, "slug", "") if hasattr(m, "slug") else m.get("slug", "")
|
||||
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
||||
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
||||
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
||||
|
||||
markets_list.append({
|
||||
"href": market_href,
|
||||
"name": m_name,
|
||||
"slug": m_slug,
|
||||
"del-url": del_url,
|
||||
"csrf-hdr": csrf_hdr,
|
||||
})
|
||||
|
||||
return {
|
||||
"can-create": can_create,
|
||||
"create-url": url_for("markets.create_market") if can_create else None,
|
||||
"csrf": csrf,
|
||||
"markets-list": markets_list,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user