Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m33s
Continues the pattern of eliminating Python sx_call tree-building in favour of data-driven .sx defcomps. POST/PUT/DELETE routes now pass plain data (dicts, lists, scalars) and let .sx handle iteration, conditionals, and layout via map/let/when/if. Single response components wrap OOB swaps. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
560 lines
23 KiB
Python
560 lines
23 KiB
Python
"""Calendar grid, day panels, month navigation, calendar-specific helpers."""
|
|
from __future__ import annotations
|
|
|
|
from shared.sx.helpers import (
|
|
call_url, sx_call, render_to_sx_with_env,
|
|
post_admin_header_sx,
|
|
)
|
|
from shared.sx.parser import SxExpr
|
|
|
|
from .utils import (
|
|
_ensure_container_nav,
|
|
_list_container,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Post header helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _post_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the post-level header row — delegates to shared sx helper."""
|
|
from shared.sx.helpers import post_header_sx
|
|
return await post_header_sx(ctx, oob=oob)
|
|
|
|
|
|
def _post_nav_sx(ctx: dict) -> str:
|
|
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
|
|
from quart import url_for, g
|
|
|
|
calendars_orm = ctx.get("calendars") or []
|
|
select_colours = ctx.get("select_colours", "")
|
|
current_cal_slug = getattr(g, "calendar_slug", None)
|
|
|
|
calendars_data = []
|
|
for cal in calendars_orm:
|
|
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
|
|
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
|
|
href = url_for("calendar.get", calendar_slug=cal_slug)
|
|
calendars_data.append({
|
|
"href": href, "name": cal_name,
|
|
"is-selected": True if cal_slug == current_cal_slug else None,
|
|
})
|
|
|
|
container_nav = ctx.get("container_nav", "") or None
|
|
|
|
rights = ctx.get("rights") or {}
|
|
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
|
admin_href = None
|
|
aclass = None
|
|
if has_admin:
|
|
post = ctx.get("post") or {}
|
|
slug = post.get("slug", "")
|
|
styles = ctx.get("styles") or {}
|
|
nav_btn = styles.get("nav_button", "") if isinstance(styles, dict) else getattr(styles, "nav_button", "")
|
|
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
|
aclass = f"{nav_btn} {select_colours}".strip() or (
|
|
"justify-center cursor-pointer flex flex-row items-center gap-2 "
|
|
"rounded bg-stone-200 text-black p-3"
|
|
)
|
|
|
|
return sx_call("events-post-nav-from-data",
|
|
calendars=calendars_data or None, container_nav=container_nav,
|
|
select_colours=select_colours,
|
|
has_admin=has_admin or None, admin_href=admin_href, aclass=aclass)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendars_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the calendars section header row."""
|
|
from quart import url_for
|
|
link_href = url_for("calendars.home")
|
|
return sx_call("menu-row-sx", id="calendars-row", level=3,
|
|
link_href=link_href,
|
|
link_label_content=sx_call("events-calendars-label"),
|
|
child_id="calendars-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build a single calendar's header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
cal_name = getattr(calendar, "name", "")
|
|
cal_desc = getattr(calendar, "description", "") or ""
|
|
|
|
link_href = url_for("calendar.get", calendar_slug=cal_slug)
|
|
label_html = sx_call("events-calendar-label",
|
|
name=cal_name, description=cal_desc)
|
|
|
|
# Desktop nav: slots + admin
|
|
nav_html = _calendar_nav_sx(ctx)
|
|
|
|
return sx_call("menu-row-sx", id="calendar-row", level=3,
|
|
link_href=link_href, link_label_content=label_html,
|
|
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-header-child", oob=oob)
|
|
|
|
|
|
def _calendar_nav_sx(ctx: dict) -> str:
|
|
"""Calendar desktop nav: Slots + admin link."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
select_colours = ctx.get("select_colours", "")
|
|
|
|
slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug)
|
|
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug) if is_admin else None
|
|
|
|
return sx_call("events-calendar-nav-from-data",
|
|
slots_href=slots_href, admin_href=admin_href,
|
|
select_colours=select_colours,
|
|
is_admin=is_admin or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build day detail header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
if not day_date:
|
|
return ""
|
|
|
|
link_href = url_for(
|
|
"calendar.day.show_day",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
label_html = sx_call("events-day-label",
|
|
date_str=day_date.strftime("%A %d %B %Y"))
|
|
|
|
nav_html = _day_nav_sx(ctx)
|
|
|
|
return sx_call("menu-row-sx", id="day-row", level=4,
|
|
link_href=link_href, link_label_content=label_html,
|
|
nav=SxExpr(nav_html) if nav_html else None, child_id="day-header-child", oob=oob)
|
|
|
|
|
|
def _day_nav_sx(ctx: dict) -> str:
|
|
"""Day desktop nav: confirmed entries scrolling menu + admin link."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
confirmed_entries = ctx.get("confirmed_entries") or []
|
|
rights = ctx.get("rights") or {}
|
|
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
|
|
|
|
entries_data = []
|
|
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 ""
|
|
entries_data.append({"href": href, "name": entry.name, "time-str": f"{start}{end}"})
|
|
|
|
admin_href = None
|
|
if is_admin and day_date:
|
|
admin_href = url_for(
|
|
"defpage_day_admin",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
|
|
return sx_call("events-day-nav-from-data",
|
|
entries=entries_data or None,
|
|
is_admin=is_admin or None, admin_href=admin_href)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day admin header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build day admin header row."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_date = ctx.get("day_date")
|
|
if not day_date:
|
|
return ""
|
|
|
|
link_href = url_for(
|
|
"defpage_day_admin",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year,
|
|
month=day_date.month,
|
|
day=day_date.day,
|
|
)
|
|
return sx_call("menu-row-sx", id="day-admin-row", level=5,
|
|
link_href=link_href, link_label="admin", icon="fa fa-cog",
|
|
child_id="day-admin-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build calendar admin header row with nav links."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
cal_slug = getattr(calendar, "slug", "") if calendar else ""
|
|
select_colours = ctx.get("select_colours", "")
|
|
|
|
links_data = []
|
|
if cal_slug:
|
|
for endpoint, label in [
|
|
("defpage_slots_listing", "slots"),
|
|
("calendar.admin.calendar_description_edit", "description"),
|
|
]:
|
|
links_data.append({"href": url_for(endpoint, calendar_slug=cal_slug), "label": label})
|
|
|
|
nav_html = sx_call("events-calendar-admin-nav-from-data",
|
|
links=links_data or None, select_colours=select_colours) if links_data else ""
|
|
return sx_call("menu-row-sx", id="calendar-admin-row", level=4,
|
|
link_label="admin", icon="fa fa-cog",
|
|
nav=SxExpr(nav_html) if nav_html else None, child_id="calendar-admin-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets header
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|
"""Build the markets section header row."""
|
|
from quart import url_for
|
|
link_href = url_for("defpage_events_markets")
|
|
return sx_call("menu-row-sx", id="markets-row", level=3,
|
|
link_href=link_href,
|
|
link_label_content=sx_call("events-markets-label"),
|
|
child_id="markets-header-child", oob=oob)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendars main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendars_main_panel_sx(ctx: dict) -> str:
|
|
"""Render the calendars list + create form panel."""
|
|
from quart import url_for
|
|
from shared.utils import route_prefix
|
|
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("calendars.create_calendar") if callable(has_access) else is_admin
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
prefix = route_prefix()
|
|
|
|
calendars = ctx.get("calendars") or []
|
|
items_data = []
|
|
for cal in calendars:
|
|
cal_slug = getattr(cal, "slug", "")
|
|
cal_name = getattr(cal, "name", "")
|
|
items_data.append({
|
|
"href": prefix + url_for("calendar.get", calendar_slug=cal_slug),
|
|
"name": cal_name, "slug": cal_slug,
|
|
"del-url": url_for("calendar.delete", calendar_slug=cal_slug),
|
|
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
|
|
"confirm-title": "Delete calendar?",
|
|
"confirm-text": "Entries will be hidden (soft delete)",
|
|
})
|
|
|
|
return sx_call("events-crud-panel-from-data",
|
|
can_create=can_create or None,
|
|
create_url=url_for("calendars.create_calendar") if can_create else None,
|
|
csrf=csrf, errors_id="cal-create-errors", list_id="calendars-list",
|
|
placeholder="e.g. Events, Gigs, Meetings", btn_label="Add calendar",
|
|
items=items_data or None,
|
|
empty_msg="No calendars yet. Create one above.")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar month grid
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_main_panel_html(ctx: dict) -> str:
|
|
"""Render the calendar month grid via data extraction + sx defcomp."""
|
|
from quart import url_for
|
|
from quart import session as qsession
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
styles = ctx.get("styles") or {}
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
|
|
year = ctx.get("year", 2024)
|
|
month = ctx.get("month", 1)
|
|
month_name = ctx.get("month_name", "")
|
|
weekday_names = ctx.get("weekday_names", [])
|
|
weeks = ctx.get("weeks", [])
|
|
prev_month = ctx.get("prev_month", 1)
|
|
prev_month_year = ctx.get("prev_month_year", year)
|
|
next_month = ctx.get("next_month", 1)
|
|
next_month_year = ctx.get("next_month_year", year)
|
|
prev_year = ctx.get("prev_year", year - 1)
|
|
next_year = ctx.get("next_year", year + 1)
|
|
month_entries = ctx.get("month_entries") or []
|
|
user = ctx.get("user")
|
|
qs = qsession if "qsession" not in ctx else ctx["qsession"]
|
|
|
|
def nav_link(y, m):
|
|
return url_for("calendar.get", calendar_slug=cal_slug, year=y, month=m)
|
|
|
|
# Day cells data
|
|
cells_data = []
|
|
for week in weeks:
|
|
for day_cell in week:
|
|
if isinstance(day_cell, dict):
|
|
in_month = day_cell.get("in_month", True)
|
|
is_today = day_cell.get("is_today", False)
|
|
day_date = day_cell.get("date")
|
|
else:
|
|
in_month = getattr(day_cell, "in_month", True)
|
|
is_today = getattr(day_cell, "is_today", False)
|
|
day_date = getattr(day_cell, "date", None)
|
|
|
|
cell_cls = "min-h-20 sm:min-h-24 bg-white px-3 py-2 text-xs"
|
|
if not in_month:
|
|
cell_cls += " bg-stone-50 text-stone-400"
|
|
if is_today:
|
|
cell_cls += " ring-2 ring-blue-500 z-10 relative"
|
|
|
|
cell = {"cell-cls": cell_cls}
|
|
if day_date:
|
|
cell["day-str"] = day_date.strftime("%a")
|
|
cell["day-href"] = url_for(
|
|
"calendar.day.show_day",
|
|
calendar_slug=cal_slug,
|
|
year=day_date.year, month=day_date.month, day=day_date.day,
|
|
)
|
|
cell["day-num"] = str(day_date.day)
|
|
|
|
# Entry badges for this day
|
|
badges = []
|
|
for e in month_entries:
|
|
if e.start_at and e.start_at.date() == day_date:
|
|
is_mine = (
|
|
(user and e.user_id == user.id)
|
|
or (not user and e.session_id == qs.get("calendar_sid"))
|
|
)
|
|
if e.state == "confirmed":
|
|
bg_cls = "bg-emerald-200 text-emerald-900" if is_mine else "bg-emerald-100 text-emerald-800"
|
|
else:
|
|
bg_cls = "bg-sky-100 text-sky-800" if is_mine else "bg-stone-100 text-stone-700"
|
|
badges.append({
|
|
"bg-cls": bg_cls, "name": e.name,
|
|
"state-label": (e.state or "pending").replace("_", " "),
|
|
})
|
|
if badges:
|
|
cell["badges"] = badges
|
|
|
|
cells_data.append(cell)
|
|
|
|
return sx_call("events-calendar-grid-from-data",
|
|
pill_cls=pill_cls, month_name=month_name, year=str(year),
|
|
prev_year_href=nav_link(prev_year, month),
|
|
prev_month_href=nav_link(prev_month_year, prev_month),
|
|
next_month_href=nav_link(next_month_year, next_month),
|
|
next_year_href=nav_link(next_year, month),
|
|
weekday_names=weekday_names or None,
|
|
cells=cells_data or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_main_panel_html(ctx: dict) -> str:
|
|
"""Render the day entries table via data extraction + sx defcomp."""
|
|
from quart import url_for
|
|
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
day_entries = ctx.get("day_entries") or []
|
|
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", "")
|
|
pill_cls = getattr(styles, "pill", "") if hasattr(styles, "pill") else styles.get("pill", "")
|
|
tr_cls = getattr(styles, "tr", "") if hasattr(styles, "tr") else styles.get("tr", "")
|
|
pre_action = getattr(styles, "pre_action_button", "") if hasattr(styles, "pre_action_button") else styles.get("pre_action_button", "")
|
|
|
|
rows_data = []
|
|
for entry in day_entries:
|
|
entry_href = url_for(
|
|
"defpage_entry_detail",
|
|
calendar_slug=cal_slug, day=day, month=month, year=year, entry_id=entry.id,
|
|
)
|
|
row = {
|
|
"href": entry_href, "name": entry.name,
|
|
"state-id": f"entry-state-{entry.id}",
|
|
"state": getattr(entry, "state", "pending") or "pending",
|
|
}
|
|
|
|
# Slot/Time
|
|
slot = getattr(entry, "slot", None)
|
|
if slot:
|
|
row["slot-name"] = slot.name
|
|
row["slot-href"] = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
|
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
|
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
|
row["slot-time"] = f"({time_start}{time_end})"
|
|
else:
|
|
row["start"] = entry.start_at.strftime("%H:%M") if entry.start_at else ""
|
|
row["end"] = f" \u2192 {entry.end_at.strftime('%H:%M')}" if entry.end_at else ""
|
|
|
|
# Cost
|
|
cost = getattr(entry, "cost", None)
|
|
row["cost-str"] = f"\u00a3{cost:.2f}" if cost is not None else "\u00a30.00"
|
|
|
|
# Tickets
|
|
tp = getattr(entry, "ticket_price", None)
|
|
if tp is not None:
|
|
tc = getattr(entry, "ticket_count", None)
|
|
row["has-tickets"] = True
|
|
row["price-str"] = f"\u00a3{tp:.2f}"
|
|
row["count-str"] = f"{tc} tickets" if tc is not None else "Unlimited"
|
|
|
|
rows_data.append(row)
|
|
|
|
add_url = url_for(
|
|
"calendar.day.calendar_entries.add_form",
|
|
calendar_slug=cal_slug,
|
|
day=day, month=month, year=year,
|
|
)
|
|
|
|
return sx_call("events-day-table-from-data",
|
|
list_container=list_container, pre_action=pre_action,
|
|
add_url=add_url, tr_cls=tr_cls, pill_cls=pill_cls,
|
|
rows=rows_data or None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Day admin main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _day_admin_main_panel_html(ctx: dict) -> str:
|
|
"""Render day admin panel (placeholder nav)."""
|
|
return sx_call("events-day-admin-panel")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _calendar_admin_main_panel_html(ctx: dict) -> str:
|
|
"""Render calendar admin config panel with description editor."""
|
|
from quart import url_for
|
|
calendar = ctx.get("calendar")
|
|
if not calendar:
|
|
return ""
|
|
csrf_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
cal_slug = getattr(calendar, "slug", "")
|
|
desc = getattr(calendar, "description", "") or ""
|
|
hx_select = ctx.get("hx_select_search", "#main-panel")
|
|
|
|
desc_edit_url = url_for("calendar.admin.calendar_description_edit", calendar_slug=cal_slug)
|
|
description_html = _calendar_description_display_html(calendar, desc_edit_url)
|
|
|
|
return sx_call("events-calendar-admin-panel",
|
|
description_content=description_html, csrf=csrf,
|
|
description=desc)
|
|
|
|
|
|
def _calendar_description_display_html(calendar, edit_url: str) -> str:
|
|
"""Render calendar description display with edit button."""
|
|
desc = getattr(calendar, "description", "") or ""
|
|
return sx_call("events-calendar-description-display",
|
|
description=desc, edit_url=edit_url)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Markets main panel
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _markets_main_panel_html(ctx: dict) -> str:
|
|
"""Render markets list + create form panel via data extraction + sx defcomp."""
|
|
from quart import url_for
|
|
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_token = ctx.get("csrf_token")
|
|
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
|
|
markets = ctx.get("markets") or []
|
|
post = ctx.get("post") or {}
|
|
slug = post.get("slug", "")
|
|
|
|
items_data = []
|
|
for m in markets:
|
|
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", "")
|
|
items_data.append({
|
|
"href": call_url(ctx, "market_url", f"/{slug}/{m_slug}/"),
|
|
"name": m_name, "slug": m_slug,
|
|
"del-url": url_for("markets.delete_market", market_slug=m_slug),
|
|
"csrf-hdr": f'{{"X-CSRFToken":"{csrf}"}}',
|
|
"confirm-title": "Delete market?",
|
|
"confirm-text": "Products will be hidden (soft delete)",
|
|
})
|
|
|
|
return sx_call("events-crud-panel-from-data",
|
|
can_create=can_create or None,
|
|
create_url=url_for("markets.create_market") if can_create else None,
|
|
csrf=csrf, errors_id="market-create-errors", list_id="markets-list",
|
|
placeholder="e.g. Farm Shop, Bakery", btn_label="Add market",
|
|
items=items_data or None,
|
|
empty_msg="No markets yet. Create one above.")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Calendar admin helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
|
|
selected: str = "") -> str:
|
|
"""Post-level admin row for events — delegates to shared helper."""
|
|
slug = (ctx.get("post") or {}).get("slug", "")
|
|
return await post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|