Migrate all apps to defpage declarative page routes
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m41s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m41s
Replace Python GET page handlers with declarative defpage definitions in .sx files across all 8 apps (sx docs, orders, account, market, cart, federation, events, blog). Each app now has sxc/pages/ with setup functions, layout registrations, page helpers, and .sx defpage declarations. Core infrastructure: add g I/O primitive, PageDef support for auth/layout/ data/content/filter/aside/menu slots, post_author auth level, and custom layout registration. Remove ~1400 lines of render_*_page/render_*_oob boilerplate. Update all endpoint references in routes, sx_components, and templates to defpage_* naming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -72,9 +72,19 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Setup defpage routes
|
||||||
|
import sx.sx_components # noqa: F811 — ensure components loaded
|
||||||
|
from sxc.pages import setup_account_pages
|
||||||
|
setup_account_pages()
|
||||||
|
|
||||||
# --- blueprints ---
|
# --- blueprints ---
|
||||||
app.register_blueprint(register_auth_bp())
|
app.register_blueprint(register_auth_bp())
|
||||||
app.register_blueprint(register_account_bp())
|
|
||||||
|
account_bp = register_account_bp()
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(account_bp, "account")
|
||||||
|
app.register_blueprint(account_bp)
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
|
||||||
from bp.actions.routes import register as register_actions
|
from bp.actions.routes import register as register_actions
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""Account pages blueprint.
|
"""Account pages blueprint.
|
||||||
|
|
||||||
Moved from federation/bp/auth — newsletters, fragment pages (tickets, bookings).
|
Moved from federation/bp/auth — newsletters, fragment pages (tickets, bookings).
|
||||||
Mounted at root /.
|
Mounted at root /. GET page handlers replaced by defpage.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
request,
|
request,
|
||||||
make_response,
|
|
||||||
redirect,
|
redirect,
|
||||||
g,
|
g,
|
||||||
)
|
)
|
||||||
@@ -20,85 +19,62 @@ from shared.infrastructure.urls import login_url
|
|||||||
from shared.infrastructure.fragments import fetch_fragment, fetch_fragments
|
from shared.infrastructure.fragments import fetch_fragment, fetch_fragments
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
oob = {
|
|
||||||
"oob_extends": "oob_elements.html",
|
|
||||||
"extends": "_types/root/_index.html",
|
|
||||||
"parent_id": "root-header-child",
|
|
||||||
"child_id": "auth-header-child",
|
|
||||||
"header": "_types/auth/header/_header.html",
|
|
||||||
"parent_header": "_types/root/header/_header.html",
|
|
||||||
"nav": "_types/auth/_nav.html",
|
|
||||||
"main": "_types/auth/_main_panel.html",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def register(url_prefix="/"):
|
def register(url_prefix="/"):
|
||||||
account_bp = Blueprint("account", __name__, url_prefix=url_prefix)
|
account_bp = Blueprint("account", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
@account_bp.context_processor
|
@account_bp.before_request
|
||||||
async def context():
|
async def _prepare_page_data():
|
||||||
|
"""Fetch account_nav fragments and load data for defpage routes."""
|
||||||
|
# Fetch account nav items for layout (was in context_processor)
|
||||||
events_nav, cart_nav, artdag_nav = await fetch_fragments([
|
events_nav, cart_nav, artdag_nav = await fetch_fragments([
|
||||||
("events", "account-nav-item", {}),
|
("events", "account-nav-item", {}),
|
||||||
("cart", "account-nav-item", {}),
|
("cart", "account-nav-item", {}),
|
||||||
("artdag", "nav-item", {}),
|
("artdag", "nav-item", {}),
|
||||||
], required=False)
|
], required=False)
|
||||||
return {"oob": oob, "account_nav": events_nav + cart_nav + artdag_nav}
|
g.account_nav = events_nav + cart_nav + artdag_nav
|
||||||
|
|
||||||
@account_bp.get("/")
|
if request.method != "GET":
|
||||||
async def account():
|
return
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_account_page, render_account_oob
|
|
||||||
|
|
||||||
if not g.get("user"):
|
endpoint = request.endpoint or ""
|
||||||
return redirect(login_url("/"))
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
# Newsletters page — load newsletter data
|
||||||
if not is_htmx_request():
|
if endpoint.endswith("defpage_newsletters"):
|
||||||
html = await render_account_page(ctx)
|
result = await g.s.execute(
|
||||||
return await make_response(html)
|
select(GhostNewsletter).order_by(GhostNewsletter.name)
|
||||||
else:
|
|
||||||
sx_src = await render_account_oob(ctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@account_bp.get("/newsletters/")
|
|
||||||
async def newsletters():
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
if not g.get("user"):
|
|
||||||
return redirect(login_url("/newsletters/"))
|
|
||||||
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(GhostNewsletter).order_by(GhostNewsletter.name)
|
|
||||||
)
|
|
||||||
all_newsletters = result.scalars().all()
|
|
||||||
|
|
||||||
sub_result = await g.s.execute(
|
|
||||||
select(UserNewsletter).where(
|
|
||||||
UserNewsletter.user_id == g.user.id,
|
|
||||||
)
|
)
|
||||||
)
|
all_newsletters = result.scalars().all()
|
||||||
user_subs = {un.newsletter_id: un for un in sub_result.scalars().all()}
|
|
||||||
|
|
||||||
newsletter_list = []
|
sub_result = await g.s.execute(
|
||||||
for nl in all_newsletters:
|
select(UserNewsletter).where(
|
||||||
un = user_subs.get(nl.id)
|
UserNewsletter.user_id == g.user.id,
|
||||||
newsletter_list.append({
|
)
|
||||||
"newsletter": nl,
|
)
|
||||||
"un": un,
|
user_subs = {un.newsletter_id: un for un in sub_result.scalars().all()}
|
||||||
"subscribed": un.subscribed if un else False,
|
|
||||||
})
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
newsletter_list = []
|
||||||
from sx.sx_components import render_newsletters_page, render_newsletters_oob
|
for nl in all_newsletters:
|
||||||
|
un = user_subs.get(nl.id)
|
||||||
|
newsletter_list.append({
|
||||||
|
"newsletter": nl,
|
||||||
|
"un": un,
|
||||||
|
"subscribed": un.subscribed if un else False,
|
||||||
|
})
|
||||||
|
g.newsletters_data = newsletter_list
|
||||||
|
|
||||||
ctx = await get_template_context()
|
# Fragment page — load fragment from events service
|
||||||
if not is_htmx_request():
|
elif endpoint.endswith("defpage_fragment_page"):
|
||||||
html = await render_newsletters_page(ctx, newsletter_list)
|
slug = request.view_args.get("slug")
|
||||||
return await make_response(html)
|
if slug and g.get("user"):
|
||||||
else:
|
fragment_html = await fetch_fragment(
|
||||||
sx_src = await render_newsletters_oob(ctx, newsletter_list)
|
"events", "account-page",
|
||||||
return sx_response(sx_src)
|
params={"slug": slug, "user_id": str(g.user.id)},
|
||||||
|
)
|
||||||
|
if not fragment_html:
|
||||||
|
from quart import abort
|
||||||
|
abort(404)
|
||||||
|
g.fragment_page_data = fragment_html
|
||||||
|
|
||||||
@account_bp.post("/newsletter/<int:newsletter_id>/toggle/")
|
@account_bp.post("/newsletter/<int:newsletter_id>/toggle/")
|
||||||
async def toggle_newsletter(newsletter_id: int):
|
async def toggle_newsletter(newsletter_id: int):
|
||||||
@@ -128,31 +104,4 @@ def register(url_prefix="/"):
|
|||||||
from sx.sx_components import render_newsletter_toggle
|
from sx.sx_components import render_newsletter_toggle
|
||||||
return sx_response(render_newsletter_toggle(un))
|
return sx_response(render_newsletter_toggle(un))
|
||||||
|
|
||||||
# Catch-all for fragment-provided pages — must be last
|
|
||||||
@account_bp.get("/<slug>/")
|
|
||||||
async def fragment_page(slug):
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from quart import abort
|
|
||||||
|
|
||||||
if not g.get("user"):
|
|
||||||
return redirect(login_url(f"/{slug}/"))
|
|
||||||
|
|
||||||
fragment_html = await fetch_fragment(
|
|
||||||
"events", "account-page",
|
|
||||||
params={"slug": slug, "user_id": str(g.user.id)},
|
|
||||||
)
|
|
||||||
if not fragment_html:
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_fragment_page, render_fragment_oob
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_fragment_page(ctx, fragment_html)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_fragment_oob(ctx, fragment_html)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
return account_bp
|
return account_bp
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from typing import Any
|
|||||||
from shared.sx.jinja_bridge import load_service_components
|
from shared.sx.jinja_bridge import load_service_components
|
||||||
from shared.sx.helpers import (
|
from shared.sx.helpers import (
|
||||||
call_url, sx_call, SxExpr,
|
call_url, sx_call, SxExpr,
|
||||||
root_header_sx, full_page_sx, header_child_sx, oob_page_sx,
|
root_header_sx, full_page_sx,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load account-specific .sx components + handlers at import time
|
# Load account-specific .sx components + handlers at import time
|
||||||
@@ -238,88 +238,8 @@ def _device_approved_content() -> str:
|
|||||||
# Public API: Account dashboard
|
# Public API: Account dashboard
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_account_page(ctx: dict) -> str:
|
|
||||||
"""Full page: account dashboard."""
|
|
||||||
main = _account_main_panel_sx(ctx)
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
hdr_child = header_child_sx(_auth_header_sx(ctx))
|
|
||||||
header_rows = "(<> " + hdr + " " + hdr_child + ")"
|
|
||||||
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows,
|
|
||||||
content=main,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
async def render_account_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response for account dashboard."""
|
|
||||||
main = _account_main_panel_sx(ctx)
|
|
||||||
|
|
||||||
oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
|
|
||||||
|
|
||||||
return oob_page_sx(oobs=oobs,
|
|
||||||
content=main,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API: Newsletters
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str:
|
|
||||||
"""Full page: newsletters."""
|
|
||||||
main = _newsletters_panel_sx(ctx, newsletter_list)
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
hdr_child = header_child_sx(_auth_header_sx(ctx))
|
|
||||||
header_rows = "(<> " + hdr + " " + hdr_child + ")"
|
|
||||||
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows,
|
|
||||||
content=main,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str:
|
|
||||||
"""OOB response for newsletters."""
|
|
||||||
main = _newsletters_panel_sx(ctx, newsletter_list)
|
|
||||||
|
|
||||||
oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
|
|
||||||
|
|
||||||
return oob_page_sx(oobs=oobs,
|
|
||||||
content=main,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API: Fragment pages
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_fragment_page(ctx: dict, page_fragment: str) -> str:
|
|
||||||
"""Full page: fragment-provided content.
|
|
||||||
|
|
||||||
*page_fragment* may be sx source (from text/sx fragments wrapped in
|
|
||||||
SxExpr) or HTML (from text/html fragments). Sx source is embedded
|
|
||||||
directly; HTML is wrapped in ``~rich-text``.
|
|
||||||
"""
|
|
||||||
from shared.sx.parser import SxExpr
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
hdr_child = header_child_sx(_auth_header_sx(ctx))
|
|
||||||
header_rows = "(<> " + hdr + " " + hdr_child + ")"
|
|
||||||
|
|
||||||
content = _fragment_content(page_fragment)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows,
|
|
||||||
content=content,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
async def render_fragment_oob(ctx: dict, page_fragment: str) -> str:
|
|
||||||
"""OOB response for fragment pages."""
|
|
||||||
oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
|
|
||||||
|
|
||||||
content = _fragment_content(page_fragment)
|
|
||||||
return oob_page_sx(oobs=oobs,
|
|
||||||
content=content,
|
|
||||||
menu=_auth_nav_mobile_sx(ctx))
|
|
||||||
|
|
||||||
|
|
||||||
def _fragment_content(frag: object) -> str:
|
def _fragment_content(frag: object) -> str:
|
||||||
|
|||||||
0
account/sxc/__init__.py
Normal file
0
account/sxc/__init__.py
Normal file
105
account/sxc/pages/__init__.py
Normal file
105
account/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
"""Account defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_account_pages() -> None:
|
||||||
|
"""Register account-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_account_layouts()
|
||||||
|
_register_account_helpers()
|
||||||
|
_load_account_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_account_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "account")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_account_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("account", _account_full, _account_oob, _account_mobile)
|
||||||
|
|
||||||
|
|
||||||
|
def _account_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import _auth_header_sx
|
||||||
|
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
hdr_child = header_child_sx(_auth_header_sx(ctx))
|
||||||
|
return "(<> " + root_hdr + " " + hdr_child + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _account_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _auth_header_sx
|
||||||
|
|
||||||
|
return "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _account_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _auth_nav_mobile_sx
|
||||||
|
ctx = _inject_account_nav(ctx)
|
||||||
|
auth_section = sx_call("mobile-menu-section",
|
||||||
|
label="account", href="/", level=1, colour="sky",
|
||||||
|
items=SxExpr(_auth_nav_mobile_sx(ctx)))
|
||||||
|
return mobile_menu_sx(auth_section, mobile_root_nav_sx(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
def _inject_account_nav(ctx: dict) -> dict:
|
||||||
|
"""Ensure account_nav is in ctx from g.account_nav."""
|
||||||
|
if "account_nav" not in ctx:
|
||||||
|
from quart import g
|
||||||
|
ctx = dict(ctx)
|
||||||
|
ctx["account_nav"] = getattr(g, "account_nav", "")
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_account_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
|
||||||
|
register_page_helpers("account", {
|
||||||
|
"account-content": _h_account_content,
|
||||||
|
"newsletters-content": _h_newsletters_content,
|
||||||
|
"fragment-content": _h_fragment_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_account_content():
|
||||||
|
from sx.sx_components import _account_main_panel_sx
|
||||||
|
return _account_main_panel_sx({})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_newsletters_content():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "newsletters_data", None)
|
||||||
|
if not d:
|
||||||
|
from shared.sx.helpers import sx_call
|
||||||
|
return sx_call("account-newsletter-empty")
|
||||||
|
from shared.sx.page import get_template_context_sync
|
||||||
|
from sx.sx_components import _newsletters_panel_sx
|
||||||
|
# Build a minimal ctx with account_url
|
||||||
|
ctx = {"account_url": getattr(g, "_account_url", None)}
|
||||||
|
if ctx["account_url"] is None:
|
||||||
|
from shared.infrastructure.urls import account_url
|
||||||
|
ctx["account_url"] = account_url
|
||||||
|
return _newsletters_panel_sx(ctx, d)
|
||||||
|
|
||||||
|
|
||||||
|
def _h_fragment_content():
|
||||||
|
from quart import g
|
||||||
|
frag = getattr(g, "fragment_page_data", None)
|
||||||
|
if not frag:
|
||||||
|
return ""
|
||||||
|
from sx.sx_components import _fragment_content
|
||||||
|
return _fragment_content(frag)
|
||||||
31
account/sxc/pages/account.sx
Normal file
31
account/sxc/pages/account.sx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
;; Account app — declarative page definitions
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Account dashboard
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defpage account-dashboard
|
||||||
|
:path "/"
|
||||||
|
:auth :login
|
||||||
|
:layout :account
|
||||||
|
:content (account-content))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Newsletters
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defpage newsletters
|
||||||
|
:path "/newsletters/"
|
||||||
|
:auth :login
|
||||||
|
:layout :account
|
||||||
|
:content (newsletters-content))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Fragment pages (tickets, bookings, etc. from events service)
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defpage fragment-page
|
||||||
|
:path "/<slug>/"
|
||||||
|
:auth :login
|
||||||
|
:layout :account
|
||||||
|
:content (fragment-content))
|
||||||
@@ -20,6 +20,7 @@ from bp import (
|
|||||||
register_data,
|
register_data,
|
||||||
register_actions,
|
register_actions,
|
||||||
)
|
)
|
||||||
|
from sxc.pages import setup_blog_pages
|
||||||
|
|
||||||
|
|
||||||
async def blog_context() -> dict:
|
async def blog_context() -> dict:
|
||||||
@@ -80,6 +81,8 @@ async def blog_context() -> dict:
|
|||||||
def create_app() -> "Quart":
|
def create_app() -> "Quart":
|
||||||
from services import register_domain_services
|
from services import register_domain_services
|
||||||
|
|
||||||
|
setup_blog_pages()
|
||||||
|
|
||||||
app = create_base_app(
|
app = create_base_app(
|
||||||
"blog",
|
"blog",
|
||||||
context_fn=blog_context,
|
context_fn=blog_context,
|
||||||
|
|||||||
@@ -27,33 +27,22 @@ def register(url_prefix):
|
|||||||
"base_title": f"{config()['title']} settings",
|
"base_title": f"{config()['title']} settings",
|
||||||
}
|
}
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def home():
|
ep = request.endpoint or ""
|
||||||
from shared.sx.page import get_template_context
|
if "defpage_settings_home" in ep:
|
||||||
from sx.sx_components import render_settings_page, render_settings_oob
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _settings_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
g.settings_content = _settings_main_panel_sx(tctx)
|
||||||
|
elif "defpage_cache_page" in ep:
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _cache_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
g.cache_content = _cache_main_panel_sx(tctx)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
from shared.sx.pages import mount_pages
|
||||||
if not is_htmx_request():
|
mount_pages(bp, "blog", names=["settings-home", "cache-page"])
|
||||||
html = await render_settings_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_settings_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/cache/")
|
|
||||||
@require_admin
|
|
||||||
async def cache():
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_cache_page, render_cache_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_cache_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_cache_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/cache_clear/")
|
@bp.post("/cache_clear/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -65,7 +54,7 @@ def register(url_prefix):
|
|||||||
html = render_comp("cache-cleared", time_str=now.strftime("%H:%M:%S"))
|
html = render_comp("cache-cleared", time_str=now.strftime("%H:%M:%S"))
|
||||||
return sx_response(html)
|
return sx_response(html)
|
||||||
|
|
||||||
return redirect(url_for("settings.cache"))
|
return redirect(url_for("settings.defpage_cache_page"))
|
||||||
return bp
|
return bp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,27 +46,52 @@ async def _unassigned_tags(session):
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("tag_groups_admin", __name__, url_prefix="/settings/tag-groups")
|
bp = Blueprint("tag_groups_admin", __name__, url_prefix="/settings/tag-groups")
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def index():
|
ep = request.endpoint or ""
|
||||||
groups = list(
|
if "defpage_tag_groups_page" in ep:
|
||||||
(await g.s.execute(
|
groups = list(
|
||||||
select(TagGroup).order_by(TagGroup.sort_order, TagGroup.name)
|
(await g.s.execute(
|
||||||
)).scalars()
|
select(TagGroup).order_by(TagGroup.sort_order, TagGroup.name)
|
||||||
)
|
)).scalars()
|
||||||
unassigned = await _unassigned_tags(g.s)
|
)
|
||||||
|
unassigned = await _unassigned_tags(g.s)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _tag_groups_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({"groups": groups, "unassigned_tags": unassigned})
|
||||||
|
g.tag_groups_content = _tag_groups_main_panel_sx(tctx)
|
||||||
|
elif "defpage_tag_group_edit" in ep:
|
||||||
|
tag_id = (request.view_args or {}).get("id")
|
||||||
|
tg = await g.s.get(TagGroup, tag_id)
|
||||||
|
if not tg:
|
||||||
|
from quart import abort
|
||||||
|
abort(404)
|
||||||
|
assigned_rows = list(
|
||||||
|
(await g.s.execute(
|
||||||
|
select(TagGroupTag.tag_id).where(TagGroupTag.tag_group_id == tag_id)
|
||||||
|
)).scalars()
|
||||||
|
)
|
||||||
|
all_tags = list(
|
||||||
|
(await g.s.execute(
|
||||||
|
select(Tag).where(
|
||||||
|
Tag.deleted_at.is_(None),
|
||||||
|
(Tag.visibility == "public") | (Tag.visibility.is_(None)),
|
||||||
|
).order_by(Tag.name)
|
||||||
|
)).scalars()
|
||||||
|
)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _tag_groups_edit_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({
|
||||||
|
"group": tg,
|
||||||
|
"all_tags": all_tags,
|
||||||
|
"assigned_tag_ids": set(assigned_rows),
|
||||||
|
})
|
||||||
|
g.tag_group_edit_content = _tag_groups_edit_main_panel_sx(tctx)
|
||||||
|
|
||||||
ctx = {"groups": groups, "unassigned_tags": unassigned}
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "blog", names=["tag-groups-page", "tag-group-edit"])
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_tag_groups_page, render_tag_groups_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update(ctx)
|
|
||||||
if not is_htmx_request():
|
|
||||||
return await make_response(await render_tag_groups_page(tctx))
|
|
||||||
else:
|
|
||||||
return sx_response(await render_tag_groups_oob(tctx))
|
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -74,7 +99,7 @@ def register():
|
|||||||
form = await request.form
|
form = await request.form
|
||||||
name = (form.get("name") or "").strip()
|
name = (form.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
return redirect(url_for("blog.tag_groups_admin.index"))
|
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
||||||
|
|
||||||
slug = _slugify(name)
|
slug = _slugify(name)
|
||||||
feature_image = (form.get("feature_image") or "").strip() or None
|
feature_image = (form.get("feature_image") or "").strip() or None
|
||||||
@@ -90,55 +115,14 @@ def register():
|
|||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
|
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.index"))
|
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
||||||
|
|
||||||
@bp.get("/<int:id>/")
|
|
||||||
@require_admin
|
|
||||||
async def edit(id: int):
|
|
||||||
tg = await g.s.get(TagGroup, id)
|
|
||||||
if not tg:
|
|
||||||
return redirect(url_for("blog.tag_groups_admin.index"))
|
|
||||||
|
|
||||||
# Assigned tag IDs for this group
|
|
||||||
assigned_rows = list(
|
|
||||||
(await g.s.execute(
|
|
||||||
select(TagGroupTag.tag_id).where(TagGroupTag.tag_group_id == id)
|
|
||||||
)).scalars()
|
|
||||||
)
|
|
||||||
assigned_tag_ids = set(assigned_rows)
|
|
||||||
|
|
||||||
# All public, non-deleted tags
|
|
||||||
all_tags = list(
|
|
||||||
(await g.s.execute(
|
|
||||||
select(Tag).where(
|
|
||||||
Tag.deleted_at.is_(None),
|
|
||||||
(Tag.visibility == "public") | (Tag.visibility.is_(None)),
|
|
||||||
).order_by(Tag.name)
|
|
||||||
)).scalars()
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
"group": tg,
|
|
||||||
"all_tags": all_tags,
|
|
||||||
"assigned_tag_ids": assigned_tag_ids,
|
|
||||||
}
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_tag_group_edit_page, render_tag_group_edit_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update(ctx)
|
|
||||||
if not is_htmx_request():
|
|
||||||
return await make_response(await render_tag_group_edit_page(tctx))
|
|
||||||
else:
|
|
||||||
return sx_response(await render_tag_group_edit_oob(tctx))
|
|
||||||
|
|
||||||
@bp.post("/<int:id>/")
|
@bp.post("/<int:id>/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def save(id: int):
|
async def save(id: int):
|
||||||
tg = await g.s.get(TagGroup, id)
|
tg = await g.s.get(TagGroup, id)
|
||||||
if not tg:
|
if not tg:
|
||||||
return redirect(url_for("blog.tag_groups_admin.index"))
|
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
||||||
|
|
||||||
form = await request.form
|
form = await request.form
|
||||||
name = (form.get("name") or "").strip()
|
name = (form.get("name") or "").strip()
|
||||||
@@ -169,7 +153,7 @@ def register():
|
|||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
|
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.edit", id=id))
|
return redirect(url_for("blog.tag_groups_admin.defpage_tag_group_edit", id=id))
|
||||||
|
|
||||||
@bp.post("/<int:id>/delete/")
|
@bp.post("/<int:id>/delete/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -179,6 +163,6 @@ def register():
|
|||||||
await g.s.delete(tg)
|
await g.s.delete(tg)
|
||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.index"))
|
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -51,10 +51,19 @@ def register(url_prefix, title):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@blogs_bp.before_request
|
@blogs_bp.before_request
|
||||||
def route():
|
async def route():
|
||||||
g.makeqs_factory = makeqs_factory
|
g.makeqs_factory = makeqs_factory
|
||||||
|
ep = request.endpoint or ""
|
||||||
|
if "defpage_new_post" in ep:
|
||||||
|
from sx.sx_components import render_editor_panel
|
||||||
|
g.editor_content = render_editor_panel()
|
||||||
|
elif "defpage_new_page" in ep:
|
||||||
|
from sx.sx_components import render_editor_panel
|
||||||
|
g.editor_page_content = render_editor_panel(is_page=True)
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(blogs_bp, "blog", names=["new-post", "new-page"])
|
||||||
|
|
||||||
|
|
||||||
@blogs_bp.context_processor
|
@blogs_bp.context_processor
|
||||||
async def inject_root():
|
async def inject_root():
|
||||||
return {
|
return {
|
||||||
@@ -215,21 +224,6 @@ def register(url_prefix, title):
|
|||||||
sx_src = await render_blog_oob(tctx)
|
sx_src = await render_blog_oob(tctx)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
@blogs_bp.get("/new/")
|
|
||||||
@require_admin
|
|
||||||
async def new_post():
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["editor_html"] = render_editor_panel()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_new_post_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_new_post_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@blogs_bp.post("/new/")
|
@blogs_bp.post("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def new_post_save():
|
async def new_post_save():
|
||||||
@@ -283,25 +277,9 @@ def register(url_prefix, title):
|
|||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
|
|
||||||
# Redirect to the edit page
|
# Redirect to the edit page
|
||||||
return redirect(host_url(url_for("blog.post.admin.edit", slug=post.slug)))
|
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=post.slug)))
|
||||||
|
|
||||||
|
|
||||||
@blogs_bp.get("/new-page/")
|
|
||||||
@require_admin
|
|
||||||
async def new_page():
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["editor_html"] = render_editor_panel(is_page=True)
|
|
||||||
tctx["is_page"] = True
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_new_post_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_new_post_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@blogs_bp.post("/new-page/")
|
@blogs_bp.post("/new-page/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def new_page_save():
|
async def new_page_save():
|
||||||
@@ -357,7 +335,7 @@ def register(url_prefix, title):
|
|||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
|
|
||||||
# Redirect to the page admin
|
# Redirect to the page admin
|
||||||
return redirect(host_url(url_for("blog.post.admin.edit", slug=page.slug)))
|
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=page.slug)))
|
||||||
|
|
||||||
|
|
||||||
@blogs_bp.get("/drafts/")
|
@blogs_bp.get("/drafts/")
|
||||||
|
|||||||
@@ -23,24 +23,19 @@ def register():
|
|||||||
from sx.sx_components import render_menu_items_nav_oob
|
from sx.sx_components import render_menu_items_nav_oob
|
||||||
return render_menu_items_nav_oob(menu_items)
|
return render_menu_items_nav_oob(menu_items)
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def list_menu_items():
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
"""List all menu items"""
|
return
|
||||||
menu_items = await get_all_menu_items(g.s)
|
menu_items = await get_all_menu_items(g.s)
|
||||||
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_menu_items_page, render_menu_items_oob
|
from sx.sx_components import _menu_items_main_panel_sx
|
||||||
|
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
tctx["menu_items"] = menu_items
|
tctx["menu_items"] = menu_items
|
||||||
if not is_htmx_request():
|
g.menu_items_content = _menu_items_main_panel_sx(tctx)
|
||||||
html = await render_menu_items_page(tctx)
|
|
||||||
return await make_response(html)
|
from shared.sx.pages import mount_pages
|
||||||
else:
|
mount_pages(bp, "blog", names=["menu-items-page"])
|
||||||
sx_src = await render_menu_items_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/new/")
|
@bp.get("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -55,51 +55,154 @@ def _post_to_edit_dict(post) -> dict:
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
|
@bp.before_request
|
||||||
@bp.get("/")
|
async def _prepare_page_data():
|
||||||
@require_admin
|
ep = request.endpoint or ""
|
||||||
async def admin(slug: str):
|
if "defpage_post_admin" in ep:
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from sqlalchemy import select
|
||||||
from sqlalchemy import select
|
from shared.models.page_config import PageConfig
|
||||||
from shared.models.page_config import PageConfig
|
post = (g.post_data or {}).get("post", {})
|
||||||
|
features = {}
|
||||||
|
sumup_configured = False
|
||||||
|
sumup_merchant_code = ""
|
||||||
|
sumup_checkout_prefix = ""
|
||||||
|
if post.get("is_page"):
|
||||||
|
pc = (await g.s.execute(
|
||||||
|
select(PageConfig).where(
|
||||||
|
PageConfig.container_type == "page",
|
||||||
|
PageConfig.container_id == post["id"],
|
||||||
|
)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
if pc:
|
||||||
|
features = pc.features or {}
|
||||||
|
sumup_configured = bool(pc.sumup_api_key)
|
||||||
|
sumup_merchant_code = pc.sumup_merchant_code or ""
|
||||||
|
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_admin_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({
|
||||||
|
"features": features,
|
||||||
|
"sumup_configured": sumup_configured,
|
||||||
|
"sumup_merchant_code": sumup_merchant_code,
|
||||||
|
"sumup_checkout_prefix": sumup_checkout_prefix,
|
||||||
|
})
|
||||||
|
g.post_admin_content = _post_admin_main_panel_sx(tctx)
|
||||||
|
|
||||||
# Load features for page admin (page_configs now lives in db_blog)
|
elif "defpage_post_data" in ep:
|
||||||
post = (g.post_data or {}).get("post", {})
|
from shared.sx.page import get_template_context
|
||||||
features = {}
|
from sx.sx_components import _post_data_content_sx
|
||||||
sumup_configured = False
|
tctx = await get_template_context()
|
||||||
sumup_merchant_code = ""
|
g.post_data_content = _post_data_content_sx(tctx)
|
||||||
sumup_checkout_prefix = ""
|
|
||||||
if post.get("is_page"):
|
|
||||||
pc = (await g.s.execute(
|
|
||||||
select(PageConfig).where(
|
|
||||||
PageConfig.container_type == "page",
|
|
||||||
PageConfig.container_id == post["id"],
|
|
||||||
)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
if pc:
|
|
||||||
features = pc.features or {}
|
|
||||||
sumup_configured = bool(pc.sumup_api_key)
|
|
||||||
sumup_merchant_code = pc.sumup_merchant_code or ""
|
|
||||||
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
|
|
||||||
|
|
||||||
ctx = {
|
elif "defpage_post_preview" in ep:
|
||||||
"features": features,
|
from models.ghost_content import Post
|
||||||
"sumup_configured": sumup_configured,
|
from sqlalchemy import select as sa_select
|
||||||
"sumup_merchant_code": sumup_merchant_code,
|
post_id = g.post_data["post"]["id"]
|
||||||
"sumup_checkout_prefix": sumup_checkout_prefix,
|
post = (await g.s.execute(
|
||||||
}
|
sa_select(Post).where(Post.id == post_id)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
preview_ctx = {}
|
||||||
|
sx_content = getattr(post, "sx_content", None) or ""
|
||||||
|
if sx_content:
|
||||||
|
from shared.sx.prettify import sx_to_pretty_sx
|
||||||
|
preview_ctx["sx_pretty"] = sx_to_pretty_sx(sx_content)
|
||||||
|
lexical_raw = getattr(post, "lexical", None) or ""
|
||||||
|
if lexical_raw:
|
||||||
|
from shared.sx.prettify import json_to_pretty_sx
|
||||||
|
preview_ctx["json_pretty"] = json_to_pretty_sx(lexical_raw)
|
||||||
|
if sx_content:
|
||||||
|
from shared.sx.parser import parse as sx_parse
|
||||||
|
from shared.sx.html import render as sx_html_render
|
||||||
|
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
||||||
|
try:
|
||||||
|
parsed = sx_parse(sx_content)
|
||||||
|
preview_ctx["sx_rendered"] = sx_html_render(parsed, dict(_COMPONENT_ENV))
|
||||||
|
except Exception:
|
||||||
|
preview_ctx["sx_rendered"] = "<em>Error rendering sx</em>"
|
||||||
|
if lexical_raw:
|
||||||
|
from bp.blog.ghost.lexical_renderer import render_lexical
|
||||||
|
try:
|
||||||
|
preview_ctx["lex_rendered"] = render_lexical(lexical_raw)
|
||||||
|
except Exception:
|
||||||
|
preview_ctx["lex_rendered"] = "<em>Error rendering lexical</em>"
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _preview_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update(preview_ctx)
|
||||||
|
g.post_preview_content = _preview_main_panel_sx(tctx)
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
elif "defpage_post_entries" in ep:
|
||||||
from sx.sx_components import render_post_admin_page, render_post_admin_oob
|
from sqlalchemy import select
|
||||||
|
from shared.models.calendars import Calendar
|
||||||
|
from ..services.entry_associations import get_post_entry_ids
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
associated_entry_ids = await get_post_entry_ids(post_id)
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(Calendar)
|
||||||
|
.where(Calendar.deleted_at.is_(None))
|
||||||
|
.order_by(Calendar.name.asc())
|
||||||
|
)
|
||||||
|
all_calendars = result.scalars().all()
|
||||||
|
for calendar in all_calendars:
|
||||||
|
await g.s.refresh(calendar, ["entries", "post"])
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_entries_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["all_calendars"] = all_calendars
|
||||||
|
tctx["associated_entry_ids"] = associated_entry_ids
|
||||||
|
g.post_entries_content = _post_entries_content_sx(tctx)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
elif "defpage_post_settings" in ep:
|
||||||
tctx.update(ctx)
|
from models.ghost_content import Post
|
||||||
if not is_htmx_request():
|
from sqlalchemy import select as sa_select
|
||||||
html = await render_post_admin_page(tctx)
|
from sqlalchemy.orm import selectinload
|
||||||
return await make_response(html)
|
post_id = g.post_data["post"]["id"]
|
||||||
else:
|
post = (await g.s.execute(
|
||||||
sx_src = await render_post_admin_oob(tctx)
|
sa_select(Post)
|
||||||
return sx_response(sx_src)
|
.where(Post.id == post_id)
|
||||||
|
.options(selectinload(Post.tags))
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
ghost_post = _post_to_edit_dict(post) if post else {}
|
||||||
|
save_success = request.args.get("saved") == "1"
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_settings_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["ghost_post"] = ghost_post
|
||||||
|
tctx["save_success"] = save_success
|
||||||
|
g.post_settings_content = _post_settings_content_sx(tctx)
|
||||||
|
|
||||||
|
elif "defpage_post_edit" in ep:
|
||||||
|
from models.ghost_content import Post
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
post = (await g.s.execute(
|
||||||
|
sa_select(Post)
|
||||||
|
.where(Post.id == post_id)
|
||||||
|
.options(selectinload(Post.tags))
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
ghost_post = _post_to_edit_dict(post) if post else {}
|
||||||
|
save_success = request.args.get("saved") == "1"
|
||||||
|
save_error = request.args.get("error", "")
|
||||||
|
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
|
||||||
|
from types import SimpleNamespace
|
||||||
|
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_edit_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["ghost_post"] = ghost_post
|
||||||
|
tctx["save_success"] = save_success
|
||||||
|
tctx["save_error"] = save_error
|
||||||
|
tctx["newsletters"] = newsletters
|
||||||
|
g.post_edit_content = _post_edit_content_sx(tctx)
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "blog", names=[
|
||||||
|
"post-admin", "post-data", "post-preview",
|
||||||
|
"post-entries", "post-settings", "post-edit",
|
||||||
|
])
|
||||||
|
|
||||||
@bp.put("/features/")
|
@bp.put("/features/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -184,77 +287,6 @@ def register():
|
|||||||
)
|
)
|
||||||
return sx_response(html)
|
return sx_response(html)
|
||||||
|
|
||||||
@bp.get("/data/")
|
|
||||||
@require_admin
|
|
||||||
async def data(slug: str):
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_post_data_page, render_post_data_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_post_data_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_post_data_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/preview/")
|
|
||||||
@require_admin
|
|
||||||
async def preview(slug: str):
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_post_preview_page, render_post_preview_oob
|
|
||||||
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post).where(Post.id == post_id)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
|
|
||||||
# Build the 4 preview views
|
|
||||||
preview_ctx = {}
|
|
||||||
|
|
||||||
# 1. Prettified sx source
|
|
||||||
sx_content = getattr(post, "sx_content", None) or ""
|
|
||||||
if sx_content:
|
|
||||||
from shared.sx.prettify import sx_to_pretty_sx
|
|
||||||
preview_ctx["sx_pretty"] = sx_to_pretty_sx(sx_content)
|
|
||||||
|
|
||||||
# 2. Prettified lexical JSON
|
|
||||||
lexical_raw = getattr(post, "lexical", None) or ""
|
|
||||||
if lexical_raw:
|
|
||||||
from shared.sx.prettify import json_to_pretty_sx
|
|
||||||
preview_ctx["json_pretty"] = json_to_pretty_sx(lexical_raw)
|
|
||||||
|
|
||||||
# 3. SX rendered preview
|
|
||||||
if sx_content:
|
|
||||||
from shared.sx.parser import parse as sx_parse
|
|
||||||
from shared.sx.html import render as sx_html_render
|
|
||||||
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
|
||||||
try:
|
|
||||||
parsed = sx_parse(sx_content)
|
|
||||||
preview_ctx["sx_rendered"] = sx_html_render(parsed, dict(_COMPONENT_ENV))
|
|
||||||
except Exception:
|
|
||||||
preview_ctx["sx_rendered"] = "<em>Error rendering sx</em>"
|
|
||||||
|
|
||||||
# 4. Lexical rendered preview
|
|
||||||
if lexical_raw:
|
|
||||||
from bp.blog.ghost.lexical_renderer import render_lexical
|
|
||||||
try:
|
|
||||||
preview_ctx["lex_rendered"] = render_lexical(lexical_raw)
|
|
||||||
except Exception:
|
|
||||||
preview_ctx["lex_rendered"] = "<em>Error rendering lexical</em>"
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update(preview_ctx)
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_post_preview_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_post_preview_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/entries/calendar/<int:calendar_id>/")
|
@bp.get("/entries/calendar/<int:calendar_id>/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def calendar_view(slug: str, calendar_id: int):
|
async def calendar_view(slug: str, calendar_id: int):
|
||||||
@@ -330,40 +362,6 @@ def register():
|
|||||||
)
|
)
|
||||||
return sx_response(html)
|
return sx_response(html)
|
||||||
|
|
||||||
@bp.get("/entries/")
|
|
||||||
@require_admin
|
|
||||||
async def entries(slug: str):
|
|
||||||
from ..services.entry_associations import get_post_entry_ids
|
|
||||||
from shared.models.calendars import Calendar
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
associated_entry_ids = await get_post_entry_ids(post_id)
|
|
||||||
|
|
||||||
# Load ALL calendars (not just this post's calendars)
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Calendar)
|
|
||||||
.where(Calendar.deleted_at.is_(None))
|
|
||||||
.order_by(Calendar.name.asc())
|
|
||||||
)
|
|
||||||
all_calendars = result.scalars().all()
|
|
||||||
|
|
||||||
# Load entries and post for each calendar
|
|
||||||
for calendar in all_calendars:
|
|
||||||
await g.s.refresh(calendar, ["entries", "post"])
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_post_entries_page, render_post_entries_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["all_calendars"] = all_calendars
|
|
||||||
tctx["associated_entry_ids"] = associated_entry_ids
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_post_entries_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_post_entries_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/entries/<int:entry_id>/toggle/")
|
@bp.post("/entries/<int:entry_id>/toggle/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def toggle_entry(slug: str, entry_id: int):
|
async def toggle_entry(slug: str, entry_id: int):
|
||||||
@@ -416,36 +414,6 @@ def register():
|
|||||||
|
|
||||||
return sx_response(admin_list + nav_entries_html)
|
return sx_response(admin_list + nav_entries_html)
|
||||||
|
|
||||||
@bp.get("/settings/")
|
|
||||||
@require_post_author
|
|
||||||
async def settings(slug: str):
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post)
|
|
||||||
.where(Post.id == post_id)
|
|
||||||
.options(selectinload(Post.tags))
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
|
|
||||||
ghost_post = _post_to_edit_dict(post) if post else {}
|
|
||||||
save_success = request.args.get("saved") == "1"
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_post_settings_page, render_post_settings_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["ghost_post"] = ghost_post
|
|
||||||
tctx["save_success"] = save_success
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_post_settings_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_post_settings_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/settings/")
|
@bp.post("/settings/")
|
||||||
@require_post_author
|
@require_post_author
|
||||||
async def settings_save(slug: str):
|
async def settings_save(slug: str):
|
||||||
@@ -500,7 +468,7 @@ def register():
|
|||||||
except OptimisticLockError:
|
except OptimisticLockError:
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
return redirect(
|
return redirect(
|
||||||
host_url(url_for("blog.post.admin.settings", slug=slug))
|
host_url(url_for("blog.post.admin.defpage_post_settings", slug=slug))
|
||||||
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -511,46 +479,7 @@ def register():
|
|||||||
await invalidate_tag_cache("post.post_detail")
|
await invalidate_tag_cache("post.post_detail")
|
||||||
|
|
||||||
# Redirect using the (possibly new) slug
|
# Redirect using the (possibly new) slug
|
||||||
return redirect(host_url(url_for("blog.post.admin.settings", slug=post.slug)) + "?saved=1")
|
return redirect(host_url(url_for("blog.post.admin.defpage_post_settings", slug=post.slug)) + "?saved=1")
|
||||||
|
|
||||||
@bp.get("/edit/")
|
|
||||||
@require_post_author
|
|
||||||
async def edit(slug: str):
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from shared.infrastructure.data_client import fetch_data
|
|
||||||
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post)
|
|
||||||
.where(Post.id == post_id)
|
|
||||||
.options(selectinload(Post.tags))
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
|
|
||||||
ghost_post = _post_to_edit_dict(post) if post else {}
|
|
||||||
save_success = request.args.get("saved") == "1"
|
|
||||||
save_error = request.args.get("error", "")
|
|
||||||
|
|
||||||
# Newsletters live in db_account — fetch via HTTP
|
|
||||||
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
|
|
||||||
from types import SimpleNamespace
|
|
||||||
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_post_edit_page, render_post_edit_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["ghost_post"] = ghost_post
|
|
||||||
tctx["save_success"] = save_success
|
|
||||||
tctx["save_error"] = save_error
|
|
||||||
tctx["newsletters"] = newsletters
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_post_edit_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_post_edit_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/edit/")
|
@bp.post("/edit/")
|
||||||
@require_post_author
|
@require_post_author
|
||||||
@@ -575,11 +504,11 @@ def register():
|
|||||||
try:
|
try:
|
||||||
lexical_doc = json.loads(lexical_raw)
|
lexical_doc = json.loads(lexical_raw)
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return redirect(host_url(url_for("blog.post.admin.edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
|
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
|
||||||
|
|
||||||
ok, reason = validate_lexical(lexical_doc)
|
ok, reason = validate_lexical(lexical_doc)
|
||||||
if not ok:
|
if not ok:
|
||||||
return redirect(host_url(url_for("blog.post.admin.edit", slug=slug)) + "?error=" + quote(reason))
|
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote(reason))
|
||||||
|
|
||||||
# Publish workflow
|
# Publish workflow
|
||||||
is_admin = bool((g.get("rights") or {}).get("admin"))
|
is_admin = bool((g.get("rights") or {}).get("admin"))
|
||||||
@@ -615,7 +544,7 @@ def register():
|
|||||||
)
|
)
|
||||||
except OptimisticLockError:
|
except OptimisticLockError:
|
||||||
return redirect(
|
return redirect(
|
||||||
host_url(url_for("blog.post.admin.edit", slug=slug))
|
host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug))
|
||||||
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -631,7 +560,7 @@ def register():
|
|||||||
await invalidate_tag_cache("post.post_detail")
|
await invalidate_tag_cache("post.post_detail")
|
||||||
|
|
||||||
# Redirect to GET (PRG pattern) — use post.slug in case it changed
|
# Redirect to GET (PRG pattern) — use post.slug in case it changed
|
||||||
redirect_url = host_url(url_for("blog.post.admin.edit", slug=post.slug)) + "?saved=1"
|
redirect_url = host_url(url_for("blog.post.admin.defpage_post_edit", slug=post.slug)) + "?saved=1"
|
||||||
if publish_requested_msg:
|
if publish_requested_msg:
|
||||||
redirect_url += "&publish_requested=1"
|
redirect_url += "&publish_requested=1"
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|||||||
@@ -32,25 +32,21 @@ async def _visible_snippets(session):
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("snippets", __name__, url_prefix="/settings/snippets")
|
bp = Blueprint("snippets", __name__, url_prefix="/settings/snippets")
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_login
|
async def _prepare_page_data():
|
||||||
async def list_snippets():
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
"""List snippets visible to the current user."""
|
return
|
||||||
snippets = await _visible_snippets(g.s)
|
snippets = await _visible_snippets(g.s)
|
||||||
is_admin = g.rights.get("admin")
|
is_admin = g.rights.get("admin")
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_snippets_page, render_snippets_oob
|
from sx.sx_components import _snippets_main_panel_sx
|
||||||
|
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
tctx["snippets"] = snippets
|
tctx["snippets"] = snippets
|
||||||
tctx["is_admin"] = is_admin
|
tctx["is_admin"] = is_admin
|
||||||
if not is_htmx_request():
|
g.snippets_content = _snippets_main_panel_sx(tctx)
|
||||||
html = await render_snippets_page(tctx)
|
|
||||||
return await make_response(html)
|
from shared.sx.pages import mount_pages
|
||||||
else:
|
mount_pages(bp, "blog", names=["snippets-page"])
|
||||||
sx_src = await render_snippets_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.delete("/<int:snippet_id>/")
|
@bp.delete("/<int:snippet_id>/")
|
||||||
@require_login
|
@require_login
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ from shared.sx.helpers import (
|
|||||||
search_mobile_sx,
|
search_mobile_sx,
|
||||||
search_desktop_sx,
|
search_desktop_sx,
|
||||||
full_page_sx,
|
full_page_sx,
|
||||||
|
mobile_menu_sx,
|
||||||
|
mobile_root_nav_sx,
|
||||||
|
post_mobile_nav_sx,
|
||||||
|
post_admin_mobile_nav_sx,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Load blog service .sx component definitions + handler definitions
|
# Load blog service .sx component definitions + handler definitions
|
||||||
@@ -76,6 +80,15 @@ def _post_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -
|
|||||||
return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|
return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|
||||||
|
|
||||||
|
|
||||||
|
def _post_admin_mobile_menu(ctx: dict, selected: str = "") -> str:
|
||||||
|
"""Full mobile menu for any post admin page (admin + post + root)."""
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
return mobile_menu_sx(
|
||||||
|
post_admin_mobile_nav_sx(ctx, slug, selected),
|
||||||
|
post_mobile_nav_sx(ctx),
|
||||||
|
mobile_root_nav_sx(ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Settings header (root-header-child -> root-settings-header-child)
|
# Settings header (root-header-child -> root-settings-header-child)
|
||||||
@@ -85,7 +98,7 @@ def _settings_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
"""Settings header row with admin icon and nav links (sx)."""
|
"""Settings header row with admin icon and nav links (sx)."""
|
||||||
from quart import url_for as qurl
|
from quart import url_for as qurl
|
||||||
|
|
||||||
settings_href = qurl("settings.home")
|
settings_href = qurl("settings.defpage_settings_home")
|
||||||
label_sx = sx_call("blog-admin-label")
|
label_sx = sx_call("blog-admin-label")
|
||||||
nav_sx = _settings_nav_sx(ctx)
|
nav_sx = _settings_nav_sx(ctx)
|
||||||
|
|
||||||
@@ -107,10 +120,10 @@ def _settings_nav_sx(ctx: dict) -> str:
|
|||||||
parts = []
|
parts = []
|
||||||
|
|
||||||
for endpoint, icon, label in [
|
for endpoint, icon, label in [
|
||||||
("menu_items.list_menu_items", "bars", "Menu Items"),
|
("menu_items.defpage_menu_items_page", "bars", "Menu Items"),
|
||||||
("snippets.list_snippets", "puzzle-piece", "Snippets"),
|
("snippets.defpage_snippets_page", "puzzle-piece", "Snippets"),
|
||||||
("blog.tag_groups_admin.index", "tags", "Tag Groups"),
|
("blog.tag_groups_admin.defpage_tag_groups_page", "tags", "Tag Groups"),
|
||||||
("settings.cache", "refresh", "Cache"),
|
("settings.defpage_cache_page", "refresh", "Cache"),
|
||||||
]:
|
]:
|
||||||
href = qurl(endpoint)
|
href = qurl(endpoint)
|
||||||
parts.append(sx_call("nav-link",
|
parts.append(sx_call("nav-link",
|
||||||
@@ -679,7 +692,7 @@ def _post_main_panel_sx(ctx: dict) -> str:
|
|||||||
if post.get("status") == "draft":
|
if post.get("status") == "draft":
|
||||||
edit_sx = ""
|
edit_sx = ""
|
||||||
if is_admin or (user and post.get("user_id") == getattr(user, "id", None)):
|
if is_admin or (user and post.get("user_id") == getattr(user, "id", None)):
|
||||||
edit_href = qurl("blog.post.admin.edit", slug=slug)
|
edit_href = qurl("blog.post.admin.defpage_post_edit", slug=slug)
|
||||||
edit_sx = sx_call("blog-detail-edit-link",
|
edit_sx = sx_call("blog-detail-edit-link",
|
||||||
href=edit_href, hx_select=hx_select,
|
href=edit_href, hx_select=hx_select,
|
||||||
)
|
)
|
||||||
@@ -951,7 +964,7 @@ def _tag_groups_main_panel_sx(ctx: dict) -> str:
|
|||||||
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
g_colour = getattr(group, "colour", None) if hasattr(group, "colour") else group.get("colour")
|
||||||
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
g_sort = getattr(group, "sort_order", 0) if hasattr(group, "sort_order") else group.get("sort_order", 0)
|
||||||
|
|
||||||
edit_href = qurl("blog.tag_groups_admin.edit", id=g_id)
|
edit_href = qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id)
|
||||||
|
|
||||||
if g_fi:
|
if g_fi:
|
||||||
icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name)
|
icon = sx_call("blog-tag-group-icon-image", src=g_fi, name=g_name)
|
||||||
@@ -1053,7 +1066,7 @@ async def render_home_page(ctx: dict) -> str:
|
|||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||||
content = _home_main_panel_sx(ctx)
|
content = _home_main_panel_sx(ctx)
|
||||||
meta = _post_meta_sx(ctx)
|
meta = _post_meta_sx(ctx)
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
||||||
meta=meta, menu=menu)
|
meta=meta, menu=menu)
|
||||||
|
|
||||||
@@ -1088,9 +1101,8 @@ async def render_blog_oob(ctx: dict) -> str:
|
|||||||
content = _blog_main_panel_sx(ctx)
|
content = _blog_main_panel_sx(ctx)
|
||||||
aside = _blog_aside_sx(ctx)
|
aside = _blog_aside_sx(ctx)
|
||||||
filter_sx = _blog_filter_sx(ctx)
|
filter_sx = _blog_filter_sx(ctx)
|
||||||
nav = ctx.get("nav_sx", "") or ""
|
|
||||||
return oob_page_sx(oobs=header_oob, content=content, aside=aside,
|
return oob_page_sx(oobs=header_oob, content=content, aside=aside,
|
||||||
filter=filter_sx, menu=nav)
|
filter=filter_sx)
|
||||||
|
|
||||||
|
|
||||||
async def render_blog_cards(ctx: dict) -> str:
|
async def render_blog_cards(ctx: dict) -> str:
|
||||||
@@ -1304,15 +1316,6 @@ async def render_new_post_page(ctx: dict) -> str:
|
|||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
||||||
|
|
||||||
|
|
||||||
async def render_new_post_oob(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
blog_hdr = _blog_header_sx(ctx)
|
|
||||||
rows = "(<> " + root_hdr + " " + blog_hdr + ")"
|
|
||||||
header_oob = _oob_header_sx("root-header-child", "blog-header-child", rows)
|
|
||||||
content = ctx.get("editor_html", "")
|
|
||||||
return oob_page_sx(oobs=header_oob, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post detail ----
|
# ---- Post detail ----
|
||||||
|
|
||||||
async def render_post_page(ctx: dict) -> str:
|
async def render_post_page(ctx: dict) -> str:
|
||||||
@@ -1321,7 +1324,7 @@ async def render_post_page(ctx: dict) -> str:
|
|||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
header_rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||||
content = _post_main_panel_sx(ctx)
|
content = _post_main_panel_sx(ctx)
|
||||||
meta = _post_meta_sx(ctx)
|
meta = _post_meta_sx(ctx)
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx))
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
||||||
meta=meta, menu=menu)
|
meta=meta, menu=menu)
|
||||||
|
|
||||||
@@ -1332,35 +1335,14 @@ async def render_post_oob(ctx: dict) -> str:
|
|||||||
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
rows = "(<> " + root_hdr + " " + post_hdr + ")"
|
||||||
post_oob = _oob_header_sx("root-header-child", "post-header-child", rows)
|
post_oob = _oob_header_sx("root-header-child", "post-header-child", rows)
|
||||||
content = _post_main_panel_sx(ctx)
|
content = _post_main_panel_sx(ctx)
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
menu = mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx))
|
||||||
oobs = post_oob
|
oobs = post_oob
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
||||||
|
|
||||||
|
|
||||||
# ---- Post admin ----
|
# ---- Post admin ----
|
||||||
|
|
||||||
async def render_post_admin_page(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _post_admin_main_panel_sx(ctx)
|
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
|
||||||
menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_post_admin_oob(ctx: dict) -> str:
|
|
||||||
post_hdr_oob = _post_header_sx(ctx, oob=True)
|
|
||||||
admin_oob = _oob_header_sx("post-header-child", "post-admin-header-child",
|
|
||||||
_post_admin_header_sx(ctx))
|
|
||||||
content = _post_admin_main_panel_sx(ctx)
|
|
||||||
menu = ctx.get("nav_sx", "") or ""
|
|
||||||
oobs = "(<> " + post_hdr_oob + " " + admin_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post data ----
|
|
||||||
|
|
||||||
def _post_data_content_sx(ctx: dict) -> str:
|
def _post_data_content_sx(ctx: dict) -> str:
|
||||||
"""Build post data inspector panel natively (replaces _types/post_data/_main_panel.html)."""
|
"""Build post data inspector panel natively (replaces _types/post_data/_main_panel.html)."""
|
||||||
@@ -1478,22 +1460,7 @@ def _post_data_content_sx(ctx: dict) -> str:
|
|||||||
return _raw_html_sx(html)
|
return _raw_html_sx(html)
|
||||||
|
|
||||||
|
|
||||||
async def render_post_data_page(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="data")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _post_data_content_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_post_data_oob(ctx: dict) -> str:
|
|
||||||
admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="data")
|
|
||||||
content = _post_data_content_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=admin_hdr_oob, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post preview ----
|
|
||||||
|
|
||||||
def _preview_main_panel_sx(ctx: dict) -> str:
|
def _preview_main_panel_sx(ctx: dict) -> str:
|
||||||
"""Build the preview panel with 4 expandable sections."""
|
"""Build the preview panel with 4 expandable sections."""
|
||||||
@@ -1540,22 +1507,7 @@ def _preview_main_panel_sx(ctx: dict) -> str:
|
|||||||
return sx_call("blog-preview-panel", sections=SxExpr(f"(<> {inner})"))
|
return sx_call("blog-preview-panel", sections=SxExpr(f"(<> {inner})"))
|
||||||
|
|
||||||
|
|
||||||
async def render_post_preview_page(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="preview")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _preview_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_post_preview_oob(ctx: dict) -> str:
|
|
||||||
admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="preview")
|
|
||||||
content = _preview_main_panel_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=admin_hdr_oob, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post entries ----
|
|
||||||
|
|
||||||
def _post_entries_content_sx(ctx: dict) -> str:
|
def _post_entries_content_sx(ctx: dict) -> str:
|
||||||
"""Build post entries panel natively (replaces _types/post_entries/_main_panel.html)."""
|
"""Build post entries panel natively (replaces _types/post_entries/_main_panel.html)."""
|
||||||
@@ -1613,21 +1565,6 @@ def _post_entries_content_sx(ctx: dict) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def render_post_entries_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="entries")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _post_entries_content_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_post_entries_oob(ctx: dict) -> str:
|
|
||||||
admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="entries")
|
|
||||||
content = _post_entries_content_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=admin_hdr_oob, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Calendar view (for entries browser) ----
|
# ---- Calendar view (for entries browser) ----
|
||||||
|
|
||||||
def render_calendar_view(
|
def render_calendar_view(
|
||||||
@@ -2045,22 +1982,7 @@ def _post_edit_content_sx(ctx: dict) -> str:
|
|||||||
return _raw_html_sx("".join(parts))
|
return _raw_html_sx("".join(parts))
|
||||||
|
|
||||||
|
|
||||||
async def render_post_edit_page(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="edit")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _post_edit_content_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_post_edit_oob(ctx: dict) -> str:
|
|
||||||
admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="edit")
|
|
||||||
content = _post_edit_content_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=admin_hdr_oob, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Post settings ----
|
|
||||||
|
|
||||||
def _post_settings_content_sx(ctx: dict) -> str:
|
def _post_settings_content_sx(ctx: dict) -> str:
|
||||||
"""Build settings form natively (replaces _types/post_settings/_main_panel.html)."""
|
"""Build settings form natively (replaces _types/post_settings/_main_panel.html)."""
|
||||||
@@ -2195,189 +2117,17 @@ def _post_settings_content_sx(ctx: dict) -> str:
|
|||||||
return _raw_html_sx(html)
|
return _raw_html_sx(html)
|
||||||
|
|
||||||
|
|
||||||
async def render_post_settings_page(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = _post_admin_header_sx(ctx, selected="settings")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
content = _post_settings_content_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
async def render_post_settings_oob(ctx: dict) -> str:
|
# ===========================================================================
|
||||||
admin_hdr_oob = _post_admin_header_sx(ctx, oob=True, selected="settings")
|
|
||||||
content = _post_settings_content_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=admin_hdr_oob, content=content)
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
# ---- Settings home ----
|
# ===========================================================================
|
||||||
|
|
||||||
async def render_settings_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + ")"
|
|
||||||
content = _settings_main_panel_sx(ctx)
|
|
||||||
menu = _settings_nav_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content,
|
|
||||||
menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_settings_oob(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
rows = "(<> " + root_hdr + " " + settings_hdr + ")"
|
|
||||||
header_oob = _oob_header_sx("root-header-child", "root-settings-header-child", rows)
|
|
||||||
content = _settings_main_panel_sx(ctx)
|
|
||||||
menu = _settings_nav_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=header_oob, content=content, menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Cache ----
|
|
||||||
|
|
||||||
async def render_cache_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
cache_hdr = _sub_settings_header_sx(
|
|
||||||
"cache-row", "cache-header-child",
|
|
||||||
qurl("settings.cache"), "refresh", "Cache", ctx,
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + cache_hdr + ")"
|
|
||||||
content = _cache_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_cache_oob(ctx: dict) -> str:
|
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
cache_hdr = _sub_settings_header_sx(
|
|
||||||
"cache-row", "cache-header-child",
|
|
||||||
qurl("settings.cache"), "refresh", "Cache", ctx,
|
|
||||||
)
|
|
||||||
cache_oob = _oob_header_sx("root-settings-header-child", "cache-header-child",
|
|
||||||
cache_hdr)
|
|
||||||
content = _cache_main_panel_sx(ctx)
|
|
||||||
oobs = "(<> " + settings_hdr_oob + " " + cache_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Snippets ----
|
|
||||||
|
|
||||||
async def render_snippets_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
snippets_hdr = _sub_settings_header_sx(
|
|
||||||
"snippets-row", "snippets-header-child",
|
|
||||||
qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx,
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + snippets_hdr + ")"
|
|
||||||
content = _snippets_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_snippets_oob(ctx: dict) -> str:
|
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
snippets_hdr = _sub_settings_header_sx(
|
|
||||||
"snippets-row", "snippets-header-child",
|
|
||||||
qurl("snippets.list_snippets"), "puzzle-piece", "Snippets", ctx,
|
|
||||||
)
|
|
||||||
snippets_oob = _oob_header_sx("root-settings-header-child", "snippets-header-child",
|
|
||||||
snippets_hdr)
|
|
||||||
content = _snippets_main_panel_sx(ctx)
|
|
||||||
oobs = "(<> " + settings_hdr_oob + " " + snippets_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Menu items ----
|
|
||||||
|
|
||||||
async def render_menu_items_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
mi_hdr = _sub_settings_header_sx(
|
|
||||||
"menu_items-row", "menu_items-header-child",
|
|
||||||
qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx,
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + mi_hdr + ")"
|
|
||||||
content = _menu_items_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_menu_items_oob(ctx: dict) -> str:
|
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
mi_hdr = _sub_settings_header_sx(
|
|
||||||
"menu_items-row", "menu_items-header-child",
|
|
||||||
qurl("menu_items.list_menu_items"), "bars", "Menu Items", ctx,
|
|
||||||
)
|
|
||||||
mi_oob = _oob_header_sx("root-settings-header-child", "menu_items-header-child",
|
|
||||||
mi_hdr)
|
|
||||||
content = _menu_items_main_panel_sx(ctx)
|
|
||||||
oobs = "(<> " + settings_hdr_oob + " " + mi_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Tag groups ----
|
|
||||||
|
|
||||||
async def render_tag_groups_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
tg_hdr = _sub_settings_header_sx(
|
|
||||||
"tag-groups-row", "tag-groups-header-child",
|
|
||||||
qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx,
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + tg_hdr + ")"
|
|
||||||
content = _tag_groups_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_tag_groups_oob(ctx: dict) -> str:
|
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
tg_hdr = _sub_settings_header_sx(
|
|
||||||
"tag-groups-row", "tag-groups-header-child",
|
|
||||||
qurl("blog.tag_groups_admin.index"), "tags", "Tag Groups", ctx,
|
|
||||||
)
|
|
||||||
tg_oob = _oob_header_sx("root-settings-header-child", "tag-groups-header-child",
|
|
||||||
tg_hdr)
|
|
||||||
content = _tag_groups_main_panel_sx(ctx)
|
|
||||||
oobs = "(<> " + settings_hdr_oob + " " + tg_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Tag group edit ----
|
|
||||||
|
|
||||||
async def render_tag_group_edit_page(ctx: dict) -> str:
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None)
|
|
||||||
tg_hdr = _sub_settings_header_sx(
|
|
||||||
"tag-groups-row", "tag-groups-header-child",
|
|
||||||
qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx,
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + settings_hdr + " " + tg_hdr + ")"
|
|
||||||
content = _tag_groups_edit_main_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_tag_group_edit_oob(ctx: dict) -> str:
|
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
|
||||||
from quart import url_for as qurl
|
|
||||||
g_id = (ctx.get("group") or {}).get("id") or getattr(ctx.get("group"), "id", None)
|
|
||||||
tg_hdr = _sub_settings_header_sx(
|
|
||||||
"tag-groups-row", "tag-groups-header-child",
|
|
||||||
qurl("blog.tag_groups_admin.edit", id=g_id), "tags", "Tag Groups", ctx,
|
|
||||||
)
|
|
||||||
tg_oob = _oob_header_sx("root-settings-header-child", "tag-groups-header-child",
|
|
||||||
tg_hdr)
|
|
||||||
content = _tag_groups_edit_main_panel_sx(ctx)
|
|
||||||
oobs = "(<> " + settings_hdr_oob + " " + tg_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# PUBLIC API — HTMX fragment renderers for POST/PUT/DELETE handlers
|
# PUBLIC API — HTMX fragment renderers for POST/PUT/DELETE handlers
|
||||||
|
|||||||
278
blog/sxc/pages/__init__.py
Normal file
278
blog/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
"""Blog defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_blog_pages() -> None:
|
||||||
|
"""Register blog-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_blog_layouts()
|
||||||
|
_register_blog_helpers()
|
||||||
|
_load_blog_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_blog_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "blog")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_blog_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
# :blog — root + blog header (for new-post, new-page)
|
||||||
|
register_custom_layout("blog", _blog_full, _blog_oob)
|
||||||
|
# :blog-settings — root + settings header (with settings nav menu)
|
||||||
|
register_custom_layout("blog-settings", _settings_full, _settings_oob,
|
||||||
|
mobile_fn=_settings_mobile)
|
||||||
|
# Sub-settings layouts (root + settings + sub header)
|
||||||
|
register_custom_layout("blog-cache", _cache_full, _cache_oob)
|
||||||
|
register_custom_layout("blog-snippets", _snippets_full, _snippets_oob)
|
||||||
|
register_custom_layout("blog-menu-items", _menu_items_full, _menu_items_oob)
|
||||||
|
register_custom_layout("blog-tag-groups", _tag_groups_full, _tag_groups_oob)
|
||||||
|
register_custom_layout("blog-tag-group-edit",
|
||||||
|
_tag_group_edit_full, _tag_group_edit_oob)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Blog layout (root + blog header) ---
|
||||||
|
|
||||||
|
def _blog_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _blog_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
blog_hdr = _blog_header_sx(ctx)
|
||||||
|
return "(<> " + root_hdr + " " + blog_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _blog_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import _blog_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
blog_hdr = _blog_header_sx(ctx)
|
||||||
|
rows = "(<> " + root_hdr + " " + blog_hdr + ")"
|
||||||
|
return oob_header_sx("root-header-child", "blog-header-child", rows)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Settings layout (root + settings header) ---
|
||||||
|
|
||||||
|
def _settings_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
|
return "(<> " + root_hdr + " " + settings_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _settings_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
|
rows = "(<> " + root_hdr + " " + settings_hdr + ")"
|
||||||
|
return oob_header_sx("root-header-child", "root-settings-header-child", rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _settings_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
from sx.sx_components import _settings_nav_sx
|
||||||
|
return _settings_nav_sx(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Sub-settings helpers ---
|
||||||
|
|
||||||
|
def _sub_settings_full(ctx: dict, row_id: str, child_id: str,
|
||||||
|
endpoint: str, icon: str, label: str) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
||||||
|
from quart import url_for as qurl
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
|
sub_hdr = _sub_settings_header_sx(row_id, child_id,
|
||||||
|
qurl(endpoint), icon, label, ctx)
|
||||||
|
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _sub_settings_oob(ctx: dict, row_id: str, child_id: str,
|
||||||
|
endpoint: str, icon: str, label: str) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
||||||
|
from quart import url_for as qurl
|
||||||
|
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
||||||
|
sub_hdr = _sub_settings_header_sx(row_id, child_id,
|
||||||
|
qurl(endpoint), icon, label, ctx)
|
||||||
|
sub_oob = oob_header_sx("root-settings-header-child", child_id, sub_hdr)
|
||||||
|
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
|
# --- Cache ---
|
||||||
|
|
||||||
|
def _cache_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_full(ctx, "cache-row", "cache-header-child",
|
||||||
|
"settings.defpage_cache_page", "refresh", "Cache")
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_oob(ctx, "cache-row", "cache-header-child",
|
||||||
|
"settings.defpage_cache_page", "refresh", "Cache")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Snippets ---
|
||||||
|
|
||||||
|
def _snippets_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
|
||||||
|
"snippets.defpage_snippets_page", "puzzle-piece", "Snippets")
|
||||||
|
|
||||||
|
|
||||||
|
def _snippets_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
|
||||||
|
"snippets.defpage_snippets_page", "puzzle-piece", "Snippets")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Menu Items ---
|
||||||
|
|
||||||
|
def _menu_items_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
|
||||||
|
"menu_items.defpage_menu_items_page", "bars", "Menu Items")
|
||||||
|
|
||||||
|
|
||||||
|
def _menu_items_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
|
||||||
|
"menu_items.defpage_menu_items_page", "bars", "Menu Items")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Tag Groups ---
|
||||||
|
|
||||||
|
def _tag_groups_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
|
||||||
|
"blog.tag_groups_admin.defpage_tag_groups_page", "tags", "Tag Groups")
|
||||||
|
|
||||||
|
|
||||||
|
def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
return _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
|
||||||
|
"blog.tag_groups_admin.defpage_tag_groups_page", "tags", "Tag Groups")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Tag Group Edit ---
|
||||||
|
|
||||||
|
def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from quart import request
|
||||||
|
g_id = (request.view_args or {}).get("id")
|
||||||
|
from quart import url_for as qurl
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
|
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
||||||
|
qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
||||||
|
"tags", "Tag Groups", ctx)
|
||||||
|
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from quart import request
|
||||||
|
g_id = (request.view_args or {}).get("id")
|
||||||
|
from quart import url_for as qurl
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
||||||
|
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
||||||
|
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
||||||
|
qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
||||||
|
"tags", "Tag Groups", ctx)
|
||||||
|
sub_oob = oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
|
||||||
|
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers (sync functions available in .sx defpage expressions)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_blog_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
register_page_helpers("blog", {
|
||||||
|
"editor-content": _h_editor_content,
|
||||||
|
"editor-page-content": _h_editor_page_content,
|
||||||
|
"post-admin-content": _h_post_admin_content,
|
||||||
|
"post-data-content": _h_post_data_content,
|
||||||
|
"post-preview-content": _h_post_preview_content,
|
||||||
|
"post-entries-content": _h_post_entries_content,
|
||||||
|
"post-settings-content": _h_post_settings_content,
|
||||||
|
"post-edit-content": _h_post_edit_content,
|
||||||
|
"settings-content": _h_settings_content,
|
||||||
|
"cache-content": _h_cache_content,
|
||||||
|
"snippets-content": _h_snippets_content,
|
||||||
|
"menu-items-content": _h_menu_items_content,
|
||||||
|
"tag-groups-content": _h_tag_groups_content,
|
||||||
|
"tag-group-edit-content": _h_tag_group_edit_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_editor_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "editor_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_editor_page_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "editor_page_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_admin_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_data_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_data_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_preview_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_preview_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_entries_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_entries_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_settings_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_settings_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_post_edit_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "post_edit_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_settings_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "settings_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_cache_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "cache_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_snippets_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "snippets_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_menu_items_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "menu_items_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_tag_groups_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "tag_groups_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_tag_group_edit_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "tag_group_edit_content", "")
|
||||||
98
blog/sxc/pages/blog.sx
Normal file
98
blog/sxc/pages/blog.sx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
; Blog app defpage declarations
|
||||||
|
; Pages kept as Python: home, index, post-detail (cache_page / complex branching)
|
||||||
|
|
||||||
|
; --- New post/page editors ---
|
||||||
|
|
||||||
|
(defpage new-post
|
||||||
|
:path "/new/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog
|
||||||
|
:content (editor-content))
|
||||||
|
|
||||||
|
(defpage new-page
|
||||||
|
:path "/new-page/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog
|
||||||
|
:content (editor-page-content))
|
||||||
|
|
||||||
|
; --- Post admin pages (nested under /<slug>/admin/) ---
|
||||||
|
|
||||||
|
(defpage post-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:post-admin :selected "admin")
|
||||||
|
:content (post-admin-content))
|
||||||
|
|
||||||
|
(defpage post-data
|
||||||
|
:path "/data/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:post-admin :selected "data")
|
||||||
|
:content (post-data-content))
|
||||||
|
|
||||||
|
(defpage post-preview
|
||||||
|
:path "/preview/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:post-admin :selected "preview")
|
||||||
|
:content (post-preview-content))
|
||||||
|
|
||||||
|
(defpage post-entries
|
||||||
|
:path "/entries/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:post-admin :selected "entries")
|
||||||
|
:content (post-entries-content))
|
||||||
|
|
||||||
|
(defpage post-settings
|
||||||
|
:path "/settings/"
|
||||||
|
:auth :post_author
|
||||||
|
:layout (:post-admin :selected "settings")
|
||||||
|
:content (post-settings-content))
|
||||||
|
|
||||||
|
(defpage post-edit
|
||||||
|
:path "/edit/"
|
||||||
|
:auth :post_author
|
||||||
|
:layout (:post-admin :selected "edit")
|
||||||
|
:content (post-edit-content))
|
||||||
|
|
||||||
|
; --- Settings pages ---
|
||||||
|
|
||||||
|
(defpage settings-home
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog-settings
|
||||||
|
:content (settings-content))
|
||||||
|
|
||||||
|
(defpage cache-page
|
||||||
|
:path "/cache/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog-cache
|
||||||
|
:content (cache-content))
|
||||||
|
|
||||||
|
; --- Snippets ---
|
||||||
|
|
||||||
|
(defpage snippets-page
|
||||||
|
:path "/"
|
||||||
|
:auth :login
|
||||||
|
:layout :blog-snippets
|
||||||
|
:content (snippets-content))
|
||||||
|
|
||||||
|
; --- Menu Items ---
|
||||||
|
|
||||||
|
(defpage menu-items-page
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog-menu-items
|
||||||
|
:content (menu-items-content))
|
||||||
|
|
||||||
|
; --- Tag Groups ---
|
||||||
|
|
||||||
|
(defpage tag-groups-page
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog-tag-groups
|
||||||
|
:content (tag-groups-content))
|
||||||
|
|
||||||
|
(defpage tag-group-edit
|
||||||
|
:path "/<int:id>/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :blog-tag-group-edit
|
||||||
|
:content (tag-group-edit-content))
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{# New Post/Page + Drafts toggle — shown in aside (desktop + mobile) #}
|
{# New Post/Page + Drafts toggle — shown in aside (desktop + mobile) #}
|
||||||
<div class="flex flex-wrap gap-2 px-4 py-3">
|
<div class="flex flex-wrap gap-2 px-4 py-3">
|
||||||
{% if has_access('blog.new_post') %}
|
{% if has_access('blog.defpage_new_post') %}
|
||||||
{% set new_href = url_for('blog.new_post')|host %}
|
{% set new_href = url_for('blog.defpage_new_post')|host %}
|
||||||
<a
|
<a
|
||||||
href="{{ new_href }}"
|
href="{{ new_href }}"
|
||||||
sx-get="{{ new_href }}"
|
sx-get="{{ new_href }}"
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fa fa-plus mr-1"></i> New Post
|
<i class="fa fa-plus mr-1"></i> New Post
|
||||||
</a>
|
</a>
|
||||||
{% set new_page_href = url_for('blog.new_page')|host %}
|
{% set new_page_href = url_for('blog.defpage_new_page')|host %}
|
||||||
<a
|
<a
|
||||||
href="{{ new_page_href }}"
|
href="{{ new_page_href }}"
|
||||||
sx-get="{{ new_page_href }}"
|
sx-get="{{ new_page_href }}"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='tag-groups-edit-row', oob=oob) %}
|
{% call links.menu_row(id='tag-groups-edit-row', oob=oob) %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('blog.tag_groups_admin.edit', id=group.id), 'pencil', group.name, select_colours, aclass='') }}
|
{{ admin_nav_item(url_for('blog.tag_groups_admin.defpage_tag_group_edit', id=group.id), 'pencil', group.name, select_colours, aclass='') }}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='tag-groups-row', oob=oob) %}
|
{% call links.menu_row(id='tag-groups-row', oob=oob) %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('blog.tag_groups_admin.index'), 'tags', 'Tag Groups', select_colours, aclass='') }}
|
{{ admin_nav_item(url_for('blog.tag_groups_admin.defpage_tag_groups_page'), 'tags', 'Tag Groups', select_colours, aclass='') }}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<a href="{{ url_for('blog.tag_groups_admin.edit', id=group.id) }}"
|
<a href="{{ url_for('blog.tag_groups_admin.defpage_tag_group_edit', id=group.id) }}"
|
||||||
class="font-medium text-stone-800 hover:underline">
|
class="font-medium text-stone-800 hover:underline">
|
||||||
{{ group.name }}
|
{{ group.name }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-2xl font-bold text-stone-800">Drafts</h2>
|
<h2 class="text-2xl font-bold text-stone-800">Drafts</h2>
|
||||||
{% set new_href = url_for('blog.new_post')|host %}
|
{% set new_href = url_for('blog.defpage_new_post')|host %}
|
||||||
<a
|
<a
|
||||||
href="{{ new_href }}"
|
href="{{ new_href }}"
|
||||||
sx-get="{{ new_href }}"
|
sx-get="{{ new_href }}"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
{% if drafts %}
|
{% if drafts %}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{% for draft in drafts %}
|
{% for draft in drafts %}
|
||||||
{% set edit_href = url_for('blog.post.admin.edit', slug=draft.slug)|host %}
|
{% set edit_href = url_for('blog.post.admin.defpage_post_edit', slug=draft.slug)|host %}
|
||||||
<a
|
<a
|
||||||
href="{{ edit_href }}"
|
href="{{ edit_href }}"
|
||||||
sx-disable
|
sx-disable
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='menu_items-row', oob=oob) %}
|
{% call links.menu_row(id='menu_items-row', oob=oob) %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('menu_items.list_menu_items'), 'bars', 'Menu Items', select_colours, aclass='') }}
|
{{ admin_nav_item(url_for('menu_items.defpage_menu_items_page'), 'bars', 'Menu Items', select_colours, aclass='') }}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% set is_admin = (g.get("rights") or {}).get("admin") %}
|
{% set is_admin = (g.get("rights") or {}).get("admin") %}
|
||||||
{% if is_admin or (g.user and post.user_id == g.user.id) %}
|
{% if is_admin or (g.user and post.user_id == g.user.id) %}
|
||||||
{% set edit_href = url_for('blog.post.admin.edit', slug=post.slug)|host %}
|
{% set edit_href = url_for('blog.post.admin.defpage_post_edit', slug=post.slug)|host %}
|
||||||
<a
|
<a
|
||||||
href="{{ edit_href }}"
|
href="{{ edit_href }}"
|
||||||
sx-get="{{ edit_href }}"
|
sx-get="{{ edit_href }}"
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Admin link #}
|
{# Admin link #}
|
||||||
{% if post and has_access('blog.post.admin.admin') %}
|
{% if post and has_access('blog.post.admin.defpage_post_admin') %}
|
||||||
{% call links.link(url_for('blog.post.admin.admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_admin', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
payments
|
payments
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_entries', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
entries
|
entries
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_data', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
data
|
data
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
edit
|
edit
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
settings
|
settings
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='post-admin-row', oob=oob) %}
|
{% call links.menu_row(id='post-admin-row', oob=oob) %}
|
||||||
{% call links.link(
|
{% call links.link(
|
||||||
url_for('blog.post.admin.admin', slug=post.slug),
|
url_for('blog.post.admin.defpage_post_admin', slug=post.slug),
|
||||||
hx_select_search) %}
|
hx_select_search) %}
|
||||||
{{ links.admin() }}
|
{{ links.admin() }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
{% block ___app_title %}
|
{% block ___app_title %}
|
||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% call links.menu_row() %}
|
{% call links.menu_row() %}
|
||||||
{% call links.link(url_for('blog.post.admin.data', slug=post.slug), hx_select_search) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_data', slug=post.slug), hx_select_search) %}
|
||||||
<i class="fa fa-database" aria-hidden="true"></i>
|
<i class="fa fa-database" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
data
|
data
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_settings', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
settings
|
settings
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='post_edit-row', oob=oob) %}
|
{% call links.menu_row(id='post_edit-row', oob=oob) %}
|
||||||
{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_edit', slug=post.slug), hx_select_search) %}
|
||||||
<i class="fa fa-pen-to-square" aria-hidden="true"></i>
|
<i class="fa fa-pen-to-square" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
edit
|
edit
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='post_entries-row', oob=oob) %}
|
{% call links.menu_row(id='post_entries-row', oob=oob) %}
|
||||||
{% call links.link(url_for('blog.post.admin.entries', slug=post.slug), hx_select_search) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_entries', slug=post.slug), hx_select_search) %}
|
||||||
<i class="fa fa-clock" aria-hidden="true"></i>
|
<i class="fa fa-clock" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
entries
|
entries
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% call links.link(url_for('blog.post.admin.edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_edit', slug=post.slug), hx_select_search, select_colours, True, aclass=styles.nav_button) %}
|
||||||
<i class="fa fa-pen-to-square" aria-hidden="true"></i>
|
<i class="fa fa-pen-to-square" aria-hidden="true"></i>
|
||||||
edit
|
edit
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='post_settings-row', oob=oob) %}
|
{% call links.menu_row(id='post_settings-row', oob=oob) %}
|
||||||
{% call links.link(url_for('blog.post.admin.settings', slug=post.slug), hx_select_search) %}
|
{% call links.link(url_for('blog.post.admin.defpage_post_settings', slug=post.slug), hx_select_search) %}
|
||||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
settings
|
settings
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('menu_items.list_menu_items'), 'bars', 'Menu Items', select_colours) }}
|
{{ admin_nav_item(url_for('menu_items.defpage_menu_items_page'), 'bars', 'Menu Items', select_colours) }}
|
||||||
{{ admin_nav_item(url_for('snippets.list_snippets'), 'puzzle-piece', 'Snippets', select_colours) }}
|
{{ admin_nav_item(url_for('snippets.defpage_snippets_page'), 'puzzle-piece', 'Snippets', select_colours) }}
|
||||||
{{ admin_nav_item(url_for('blog.tag_groups_admin.index'), 'tags', 'Tag Groups', select_colours) }}
|
{{ admin_nav_item(url_for('blog.tag_groups_admin.defpage_tag_groups_page'), 'tags', 'Tag Groups', select_colours) }}
|
||||||
{{ admin_nav_item(url_for('settings.cache'), 'refresh', 'Cache', select_colours) }}
|
{{ admin_nav_item(url_for('settings.defpage_cache_page'), 'refresh', 'Cache', select_colours) }}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='cache-row', oob=oob) %}
|
{% call links.menu_row(id='cache-row', oob=oob) %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('settings.cache'), 'refresh', 'Cache', select_colours, aclass='') }}
|
{{ admin_nav_item(url_for('settings.defpage_cache_page'), 'refresh', 'Cache', select_colours, aclass='') }}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='root-settings-row', oob=oob) %}
|
{% call links.menu_row(id='root-settings-row', oob=oob) %}
|
||||||
{% call links.link(url_for('settings.home'), hx_select_search) %}
|
{% call links.link(url_for('settings.defpage_settings_home'), hx_select_search) %}
|
||||||
{{ links.admin() }}
|
{{ links.admin() }}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='snippets-row', oob=oob) %}
|
{% call links.menu_row(id='snippets-row', oob=oob) %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('snippets.list_snippets'), 'puzzle-piece', 'Snippets', select_colours, aclass='') }}
|
{{ admin_nav_item(url_for('snippets.defpage_snippets_page'), 'puzzle-piece', 'Snippets', select_colours, aclass='') }}
|
||||||
{% call links.desktop_nav() %}
|
{% call links.desktop_nav() %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% endcall %}
|
{% endcall %}
|
||||||
|
|||||||
27
cart/app.py
27
cart/app.py
@@ -181,6 +181,12 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
g.page_config = _make_page_config(raw_pc) if raw_pc else None
|
g.page_config = _make_page_config(raw_pc) if raw_pc else None
|
||||||
|
|
||||||
|
# Setup defpage routes
|
||||||
|
from sxc.pages import setup_cart_pages
|
||||||
|
setup_cart_pages()
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
|
||||||
# --- Blueprint registration ---
|
# --- Blueprint registration ---
|
||||||
# Static prefixes first, dynamic (page_slug) last
|
# Static prefixes first, dynamic (page_slug) last
|
||||||
|
|
||||||
@@ -191,22 +197,19 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Cart overview at GET /
|
# Cart overview at GET /
|
||||||
app.register_blueprint(
|
overview_bp = register_cart_overview(url_prefix="/")
|
||||||
register_cart_overview(url_prefix="/"),
|
mount_pages(overview_bp, "cart", names=["cart-overview"])
|
||||||
url_prefix="/",
|
app.register_blueprint(overview_bp, url_prefix="/")
|
||||||
)
|
|
||||||
|
|
||||||
# Page admin at /<page_slug>/admin/ (before page_cart catch-all)
|
# Page admin at /<page_slug>/admin/ (before page_cart catch-all)
|
||||||
app.register_blueprint(
|
admin_bp = register_page_admin()
|
||||||
register_page_admin(),
|
mount_pages(admin_bp, "cart", names=["cart-admin", "cart-payments"])
|
||||||
url_prefix="/<page_slug>/admin",
|
app.register_blueprint(admin_bp, url_prefix="/<page_slug>/admin")
|
||||||
)
|
|
||||||
|
|
||||||
# Page cart at /<page_slug>/ (dynamic, matched last)
|
# Page cart at /<page_slug>/ (dynamic, matched last)
|
||||||
app.register_blueprint(
|
page_cart_bp = register_page_cart(url_prefix="/")
|
||||||
register_page_cart(url_prefix="/"),
|
mount_pages(page_cart_bp, "cart", names=["page-cart-view"])
|
||||||
url_prefix="/<page_slug>",
|
app.register_blueprint(page_cart_bp, url_prefix="/<page_slug>")
|
||||||
)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
|
|
||||||
if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
|
if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
|
||||||
# Redirect to overview for HTMX
|
# Redirect to overview for HTMX
|
||||||
return redirect(url_for("cart_overview.overview"))
|
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
||||||
|
|
||||||
return redirect(url_for("cart_overview.overview"))
|
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
||||||
|
|
||||||
@bp.post("/quantity/<int:product_id>/")
|
@bp.post("/quantity/<int:product_id>/")
|
||||||
async def update_quantity(product_id: int):
|
async def update_quantity(product_id: int):
|
||||||
@@ -137,7 +137,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
tickets = await get_ticket_cart_entries(g.s)
|
tickets = await get_ticket_cart_entries(g.s)
|
||||||
|
|
||||||
if not cart and not calendar_entries and not tickets:
|
if not cart and not calendar_entries and not tickets:
|
||||||
return redirect(url_for("cart_overview.overview"))
|
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
||||||
|
|
||||||
product_total = total(cart) or 0
|
product_total = total(cart) or 0
|
||||||
calendar_amount = calendar_total(calendar_entries) or 0
|
calendar_amount = calendar_total(calendar_entries) or 0
|
||||||
@@ -145,7 +145,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
cart_total = product_total + calendar_amount + ticket_amount
|
cart_total = product_total + calendar_amount + ticket_amount
|
||||||
|
|
||||||
if cart_total <= 0:
|
if cart_total <= 0:
|
||||||
return redirect(url_for("cart_overview.overview"))
|
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets)
|
page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets)
|
||||||
|
|||||||
@@ -1,31 +1,26 @@
|
|||||||
# bp/cart/overview_routes.py — Cart overview (list of page carts)
|
# bp/cart/overview_routes.py — Cart overview (list of page carts)
|
||||||
|
# GET / handled by defpage.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, render_template, make_response
|
from quart import Blueprint, g, request
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
from .services import get_cart_grouped_by_page
|
from .services import get_cart_grouped_by_page
|
||||||
|
|
||||||
|
|
||||||
def register(url_prefix: str) -> Blueprint:
|
def register(url_prefix: str) -> Blueprint:
|
||||||
bp = Blueprint("cart_overview", __name__, url_prefix=url_prefix)
|
bp = Blueprint("cart_overview", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def overview():
|
async def _prepare_page_data():
|
||||||
from quart import g
|
"""Load overview data for defpage route."""
|
||||||
|
endpoint = request.endpoint or ""
|
||||||
|
if not endpoint.endswith("defpage_cart_overview"):
|
||||||
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_overview_page, render_overview_oob
|
from sx.sx_components import _overview_main_panel_sx
|
||||||
|
|
||||||
page_groups = await get_cart_grouped_by_page(g.s)
|
page_groups = await get_cart_grouped_by_page(g.s)
|
||||||
ctx = await get_template_context()
|
ctx = await get_template_context()
|
||||||
|
g.overview_content = _overview_main_panel_sx(page_groups, ctx)
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_overview_page(ctx, page_groups)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_overview_oob(ctx, page_groups)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
# bp/cart/page_routes.py — Per-page cart (view + checkout)
|
# bp/cart/page_routes.py — Per-page cart (view + checkout)
|
||||||
|
# GET / handled by defpage.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, redirect, make_response, url_for
|
from quart import Blueprint, g, redirect, make_response, url_for, request
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
from shared.infrastructure.actions import call_action
|
from shared.infrastructure.actions import call_action
|
||||||
from .services import (
|
from .services import (
|
||||||
total,
|
total,
|
||||||
@@ -20,43 +19,25 @@ from .services import current_cart_identity
|
|||||||
def register(url_prefix: str) -> Blueprint:
|
def register(url_prefix: str) -> Blueprint:
|
||||||
bp = Blueprint("page_cart", __name__, url_prefix=url_prefix)
|
bp = Blueprint("page_cart", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def page_view():
|
async def _prepare_page_data():
|
||||||
|
"""Load page cart data for defpage route."""
|
||||||
|
endpoint = request.endpoint or ""
|
||||||
|
if not endpoint.endswith("defpage_page_cart_view"):
|
||||||
|
return
|
||||||
post = g.page_post
|
post = g.page_post
|
||||||
cart = await get_cart_for_page(g.s, post.id)
|
cart = await get_cart_for_page(g.s, post.id)
|
||||||
cal_entries = await get_calendar_entries_for_page(g.s, post.id)
|
cal_entries = await get_calendar_entries_for_page(g.s, post.id)
|
||||||
page_tickets = await get_tickets_for_page(g.s, post.id)
|
page_tickets = await get_tickets_for_page(g.s, post.id)
|
||||||
|
|
||||||
ticket_groups = group_tickets(page_tickets)
|
ticket_groups = group_tickets(page_tickets)
|
||||||
|
|
||||||
tpl_ctx = dict(
|
|
||||||
page_post=post,
|
|
||||||
page_config=getattr(g, "page_config", None),
|
|
||||||
cart=cart,
|
|
||||||
calendar_cart_entries=cal_entries,
|
|
||||||
ticket_cart_entries=page_tickets,
|
|
||||||
ticket_groups=ticket_groups,
|
|
||||||
total=total,
|
|
||||||
calendar_total=calendar_total,
|
|
||||||
ticket_total=ticket_total,
|
|
||||||
)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_page_cart_page, render_page_cart_oob
|
from sx.sx_components import _page_cart_main_panel_sx
|
||||||
|
|
||||||
ctx = await get_template_context()
|
ctx = await get_template_context()
|
||||||
if not is_htmx_request():
|
g.page_cart_content = _page_cart_main_panel_sx(
|
||||||
html = await render_page_cart_page(
|
ctx, cart, cal_entries, page_tickets, ticket_groups,
|
||||||
ctx, post, cart, cal_entries, page_tickets,
|
total, calendar_total, ticket_total,
|
||||||
ticket_groups, total, calendar_total, ticket_total,
|
)
|
||||||
)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_page_cart_oob(
|
|
||||||
ctx, post, cart, cal_entries, page_tickets,
|
|
||||||
ticket_groups, total, calendar_total, ticket_total,
|
|
||||||
)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/checkout/")
|
@bp.post("/checkout/")
|
||||||
async def page_checkout():
|
async def page_checkout():
|
||||||
@@ -67,7 +48,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
page_tickets = await get_tickets_for_page(g.s, post.id)
|
page_tickets = await get_tickets_for_page(g.s, post.id)
|
||||||
|
|
||||||
if not cart and not cal_entries and not page_tickets:
|
if not cart and not cal_entries and not page_tickets:
|
||||||
return redirect(url_for("page_cart.page_view"))
|
return redirect(url_for("page_cart.defpage_page_cart_view"))
|
||||||
|
|
||||||
product_total_val = total(cart) or 0
|
product_total_val = total(cart) or 0
|
||||||
calendar_amount = calendar_total(cal_entries) or 0
|
calendar_amount = calendar_total(cal_entries) or 0
|
||||||
@@ -75,7 +56,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
cart_total = product_total_val + calendar_amount + ticket_amount
|
cart_total = product_total_val + calendar_amount + ticket_amount
|
||||||
|
|
||||||
if cart_total <= 0:
|
if cart_total <= 0:
|
||||||
return redirect(url_for("page_cart.page_view"))
|
return redirect(url_for("page_cart.defpage_page_cart_view"))
|
||||||
|
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
|
|
||||||
|
|||||||
@@ -7,42 +7,28 @@ from quart import (
|
|||||||
from shared.infrastructure.actions import call_action
|
from shared.infrastructure.actions import call_action
|
||||||
from shared.infrastructure.data_client import fetch_data
|
from shared.infrastructure.data_client import fetch_data
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("page_admin", __name__)
|
bp = Blueprint("page_admin", __name__)
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def admin(**kwargs):
|
"""Pre-render admin content for defpage routes."""
|
||||||
from shared.sx.page import get_template_context
|
endpoint = request.endpoint or ""
|
||||||
from sx.sx_components import render_cart_admin_page, render_cart_admin_oob
|
if request.method != "GET":
|
||||||
|
return
|
||||||
ctx = await get_template_context()
|
if endpoint.endswith("defpage_cart_admin"):
|
||||||
page_post = getattr(g, "page_post", None)
|
from shared.sx.page import get_template_context
|
||||||
if not is_htmx_request():
|
from sx.sx_components import _cart_admin_main_panel_sx
|
||||||
html = await render_cart_admin_page(ctx, page_post)
|
ctx = await get_template_context()
|
||||||
return await make_response(html)
|
g.cart_admin_content = _cart_admin_main_panel_sx(ctx)
|
||||||
else:
|
elif endpoint.endswith("defpage_cart_payments"):
|
||||||
sx_src = await render_cart_admin_oob(ctx, page_post)
|
from shared.sx.page import get_template_context
|
||||||
return sx_response(sx_src)
|
from sx.sx_components import _cart_payments_main_panel_sx
|
||||||
|
ctx = await get_template_context()
|
||||||
@bp.get("/payments/")
|
g.cart_payments_content = _cart_payments_main_panel_sx(ctx)
|
||||||
@require_admin
|
|
||||||
async def payments(**kwargs):
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_cart_payments_page, render_cart_payments_oob
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
page_post = getattr(g, "page_post", None)
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_cart_payments_page(ctx, page_post)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_cart_payments_oob(ctx, page_post)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.put("/payments/")
|
@bp.put("/payments/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -587,56 +587,6 @@ def _order_filter_sx(order: Any, list_url: str, recheck_url: str,
|
|||||||
# Public API: Cart overview
|
# Public API: Cart overview
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_overview_page(ctx: dict, page_groups: list) -> str:
|
|
||||||
"""Full page: cart overview."""
|
|
||||||
main = _overview_main_panel_sx(page_groups, ctx)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_overview_oob(ctx: dict, page_groups: list) -> str:
|
|
||||||
"""OOB response for cart overview."""
|
|
||||||
main = _overview_main_panel_sx(page_groups, ctx)
|
|
||||||
oobs = root_header_sx(ctx, oob=True)
|
|
||||||
return oob_page_sx(oobs=oobs, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API: Page cart
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_page_cart_page(ctx: dict, page_post: Any,
|
|
||||||
cart: list, cal_entries: list, tickets: list,
|
|
||||||
ticket_groups: list, total_fn: Any,
|
|
||||||
cal_total_fn: Any, ticket_total_fn: Any) -> str:
|
|
||||||
"""Full page: page-specific cart."""
|
|
||||||
main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups,
|
|
||||||
total_fn, cal_total_fn, ticket_total_fn)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = _cart_header_sx(ctx)
|
|
||||||
page_hdr = _page_cart_header_sx(ctx, page_post)
|
|
||||||
nested = sx_call(
|
|
||||||
"header-child-sx",
|
|
||||||
inner=SxExpr("(<> " + child + " " + sx_call("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr)) + ")"),
|
|
||||||
)
|
|
||||||
header_rows = "(<> " + hdr + " " + nested + ")"
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_page_cart_oob(ctx: dict, page_post: Any,
|
|
||||||
cart: list, cal_entries: list, tickets: list,
|
|
||||||
ticket_groups: list, total_fn: Any,
|
|
||||||
cal_total_fn: Any, ticket_total_fn: Any) -> str:
|
|
||||||
"""OOB response for page cart."""
|
|
||||||
main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups,
|
|
||||||
total_fn, cal_total_fn, ticket_total_fn)
|
|
||||||
child_oob = sx_call("oob-header-sx",
|
|
||||||
parent_id="cart-header-child",
|
|
||||||
row=SxExpr(_page_cart_header_sx(ctx, page_post)))
|
|
||||||
cart_hdr_oob = _cart_header_sx(ctx, oob=True)
|
|
||||||
root_hdr_oob = root_header_sx(ctx, oob=True)
|
|
||||||
oobs = "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -821,7 +771,7 @@ def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
|
|||||||
def _cart_admin_main_panel_sx(ctx: dict) -> str:
|
def _cart_admin_main_panel_sx(ctx: dict) -> str:
|
||||||
"""Admin overview panel -- links to sub-admin pages."""
|
"""Admin overview panel -- links to sub-admin pages."""
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
payments_href = url_for("page_admin.payments")
|
payments_href = url_for("page_admin.defpage_cart_payments")
|
||||||
return (
|
return (
|
||||||
'(div :id "main-panel"'
|
'(div :id "main-panel"'
|
||||||
' (div :class "flex items-center justify-between p-3 border-b"'
|
' (div :class "flex items-center justify-between p-3 border-b"'
|
||||||
@@ -851,47 +801,6 @@ def _cart_payments_main_panel_sx(ctx: dict) -> str:
|
|||||||
checkout_prefix=checkout_prefix)
|
checkout_prefix=checkout_prefix)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API: Cart page admin
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_cart_admin_page(ctx: dict, page_post: Any) -> str:
|
|
||||||
"""Full page: cart page admin overview."""
|
|
||||||
content = _cart_admin_main_panel_sx(ctx)
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = await _post_header_sx(ctx, page_post)
|
|
||||||
admin_hdr = _cart_page_admin_header_sx(ctx, page_post)
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str:
|
|
||||||
"""OOB response: cart page admin overview."""
|
|
||||||
content = _cart_admin_main_panel_sx(ctx)
|
|
||||||
oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True)
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Public API: Cart payments admin
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_cart_payments_page(ctx: dict, page_post: Any) -> str:
|
|
||||||
"""Full page: payments config."""
|
|
||||||
content = _cart_payments_main_panel_sx(ctx)
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = await _post_header_sx(ctx, page_post)
|
|
||||||
admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected="payments")
|
|
||||||
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
|
||||||
return full_page_sx(ctx, header_rows=header_rows, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str:
|
|
||||||
"""OOB response: payments config."""
|
|
||||||
content = _cart_payments_main_panel_sx(ctx)
|
|
||||||
oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True, selected="payments")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
def render_cart_payments_panel(ctx: dict) -> str:
|
def render_cart_payments_panel(ctx: dict) -> str:
|
||||||
"""Render the payments config panel for PUT response."""
|
"""Render the payments config panel for PUT response."""
|
||||||
|
|||||||
0
cart/sxc/__init__.py
Normal file
0
cart/sxc/__init__.py
Normal file
121
cart/sxc/pages/__init__.py
Normal file
121
cart/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Cart defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_cart_pages() -> None:
|
||||||
|
"""Register cart-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_cart_layouts()
|
||||||
|
_register_cart_helpers()
|
||||||
|
_load_cart_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_cart_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "cart")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_cart_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("cart-page", _cart_page_full, _cart_page_oob)
|
||||||
|
register_custom_layout("cart-admin", _cart_admin_full, _cart_admin_oob)
|
||||||
|
|
||||||
|
|
||||||
|
def _cart_page_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
|
||||||
|
|
||||||
|
page_post = ctx.get("page_post")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = _cart_header_sx(ctx)
|
||||||
|
page_hdr = _page_cart_header_sx(ctx, page_post)
|
||||||
|
nested = sx_call(
|
||||||
|
"header-child-sx",
|
||||||
|
inner=SxExpr("(<> " + child + " " + sx_call("header-child-sx", id="cart-header-child", inner=SxExpr(page_hdr)) + ")"),
|
||||||
|
)
|
||||||
|
return "(<> " + root_hdr + " " + nested + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _cart_page_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _cart_header_sx, _page_cart_header_sx
|
||||||
|
|
||||||
|
page_post = ctx.get("page_post")
|
||||||
|
child_oob = sx_call("oob-header-sx",
|
||||||
|
parent_id="cart-header-child",
|
||||||
|
row=SxExpr(_page_cart_header_sx(ctx, page_post)))
|
||||||
|
cart_hdr_oob = _cart_header_sx(ctx, oob=True)
|
||||||
|
root_hdr_oob = root_header_sx(ctx, oob=True)
|
||||||
|
return "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
|
async def _cart_admin_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx
|
||||||
|
from sx.sx_components import _post_header_sx, _cart_page_admin_header_sx
|
||||||
|
|
||||||
|
page_post = ctx.get("page_post")
|
||||||
|
selected = kw.get("selected", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
post_hdr = await _post_header_sx(ctx, page_post)
|
||||||
|
admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected=selected)
|
||||||
|
return "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
async def _cart_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from sx.sx_components import _cart_page_admin_header_sx
|
||||||
|
|
||||||
|
page_post = ctx.get("page_post")
|
||||||
|
selected = kw.get("selected", "")
|
||||||
|
return _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_cart_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
|
||||||
|
register_page_helpers("cart", {
|
||||||
|
"overview-content": _h_overview_content,
|
||||||
|
"page-cart-content": _h_page_cart_content,
|
||||||
|
"cart-admin-content": _h_cart_admin_content,
|
||||||
|
"cart-payments-content": _h_cart_payments_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_overview_content():
|
||||||
|
from quart import g
|
||||||
|
page_groups = getattr(g, "overview_page_groups", [])
|
||||||
|
from sx.sx_components import _overview_main_panel_sx
|
||||||
|
# _overview_main_panel_sx needs ctx for url helpers — use g-based approach
|
||||||
|
# The function reads cart_url from ctx, which we can get from template context
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
import asyncio
|
||||||
|
# Page helpers are sync — we pre-compute in before_request
|
||||||
|
return getattr(g, "overview_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_page_cart_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "page_cart_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_cart_admin_content():
|
||||||
|
from sx.sx_components import _cart_admin_main_panel_sx
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
# Sync helper — _cart_admin_main_panel_sx is sync, but needs ctx
|
||||||
|
# We can pre-compute in before_request, or use get_template_context_sync-like pattern
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "cart_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_cart_payments_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "cart_payments_content", "")
|
||||||
25
cart/sxc/pages/cart.sx
Normal file
25
cart/sxc/pages/cart.sx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
;; Cart app defpage declarations.
|
||||||
|
|
||||||
|
(defpage cart-overview
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :root
|
||||||
|
:content (overview-content))
|
||||||
|
|
||||||
|
(defpage page-cart-view
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :cart-page
|
||||||
|
:content (page-cart-content))
|
||||||
|
|
||||||
|
(defpage cart-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :cart-admin
|
||||||
|
:content (cart-admin-content))
|
||||||
|
|
||||||
|
(defpage cart-payments
|
||||||
|
:path "/payments/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:cart-admin :selected "payments")
|
||||||
|
:content (cart-payments-content))
|
||||||
360
docs/isomorphic-sx-plan.md
Normal file
360
docs/isomorphic-sx-plan.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Isomorphic SX Architecture Migration Plan
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The sx layer already renders full pages client-side — `sx_page()` ships raw sx source + component definitions to the browser, `sx.js` evaluates and renders them. Components are cached in localStorage with a hash-based invalidation protocol (cookie `sx-comp-hash` → server skips sending defs if hash matches).
|
||||||
|
|
||||||
|
**Key insight from the user:** Pages/routes are just components. They belong in the same component registry, cached in localStorage alongside `defcomp` definitions. On navigation, if the client's component hash is current, the server doesn't need to send any s-expression source at all — just data. The client already has the page component cached and renders it locally with fresh data from the API.
|
||||||
|
|
||||||
|
### Target Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
First visit:
|
||||||
|
Server → component defs (including page components) + page data → client caches defs in localStorage
|
||||||
|
|
||||||
|
Subsequent navigation (same session, hash valid):
|
||||||
|
Client has page component cached → fetches only JSON data from /api/data/ → renders locally
|
||||||
|
Server sends: { data: {...} } — zero sx source
|
||||||
|
|
||||||
|
SSR (bots, first paint):
|
||||||
|
Server evaluates the same page component with direct DB queries → sends rendered HTML
|
||||||
|
Client hydrates (binds SxEngine handlers, no re-render)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is React-like data fetching with an s-expression view layer instead of JSX, and the component transport is a content-addressed cache rather than a JS bundle.
|
||||||
|
|
||||||
|
### Data Delivery Modes
|
||||||
|
|
||||||
|
The data side is not a single pattern — it's a spectrum that can be mixed per page and per fragment:
|
||||||
|
|
||||||
|
**Mode A: Server-bundled data** — Server evaluates the page's `:data` slot, resolves all queries (including cross-service `fetch_data` calls), returns one JSON blob. Fewest round-trips. Server aggregates.
|
||||||
|
|
||||||
|
**Mode B: Client-fetched data** — Client evaluates `:data` slot locally. Each `(query ...)` / `(service ...)` hits the relevant service's `/api/data/` endpoint independently. More round-trips but fully decoupled — each service handles its own data.
|
||||||
|
|
||||||
|
**Mode C: Hybrid** — Server bundles same-service data (direct DB). Client fetches cross-service data in parallel from other services' APIs. Mirrors current server pattern: own-domain = SQLAlchemy, cross-domain = `fetch_data()` HTTP.
|
||||||
|
|
||||||
|
The same spectrum applies to **fragments** (`frag` / `fetch_fragment`):
|
||||||
|
|
||||||
|
- **Server-composed:** Server calls `fetch_fragment()` during page evaluation, bakes result into data bundle or renders inline.
|
||||||
|
- **Client-composed:** Client's `(frag ...)` primitive fetches from the service's public fragment endpoint. Fragment returns sx source, client renders locally using cached component defs.
|
||||||
|
- **Mixed:** Stable fragments (nav, auth menu) server-composed; content-specific fragments client-fetched.
|
||||||
|
|
||||||
|
A `(query ...)` or `(frag ...)` call resolves differently depending on execution context (server vs client) but produces the same result. The choice of mode can be per-page, per-fragment, or even per-request.
|
||||||
|
|
||||||
|
## Delivery Order
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1 (Primitive Parity) ──┐
|
||||||
|
├── Phase 4 (Client Data Primitives) ──┐
|
||||||
|
Phase 3 (Public Data API) ───┘ ├── Phase 5 (Data-Only Navigation)
|
||||||
|
Phase 2 (Server-Side Rendering) ────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
Phases 1-3 are independent. Recommended order: **3 → 1 → 2 → 4 → 5**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Primitive Parity
|
||||||
|
|
||||||
|
Align JS and Python primitive sets so the same component source evaluates identically on both sides.
|
||||||
|
|
||||||
|
### 1a: Add missing pure primitives to sx.js
|
||||||
|
|
||||||
|
Add to `PRIMITIVES` in `shared/static/scripts/sx.js`:
|
||||||
|
|
||||||
|
| Primitive | JS implementation |
|
||||||
|
|-----------|-------------------|
|
||||||
|
| `clamp` | `Math.max(lo, Math.min(hi, x))` |
|
||||||
|
| `chunk-every` | partition list into n-size sublists |
|
||||||
|
| `zip-pairs` | `[[coll[0],coll[1]], [coll[2],coll[3]], ...]` |
|
||||||
|
| `dissoc` | shallow copy without specified keys |
|
||||||
|
| `into` | target-type-aware merge |
|
||||||
|
| `format-date` | minimal strftime translator covering `%Y %m %d %b %B %H %M %S` |
|
||||||
|
| `parse-int` | `parseInt` with NaN fallback to default |
|
||||||
|
| `assert` | throw if falsy |
|
||||||
|
|
||||||
|
Fix existing parity gaps: `round` needs optional `ndigits`; `min`/`max` need to accept a single list arg.
|
||||||
|
|
||||||
|
### 1b: Inject `window.__sxConfig` for server-context primitives
|
||||||
|
|
||||||
|
Modify `sx_page()` in `shared/sx/helpers.py` to inject before sx.js:
|
||||||
|
|
||||||
|
```js
|
||||||
|
window.__sxConfig = {
|
||||||
|
appUrls: { blog: "https://blog.rose-ash.com", ... },
|
||||||
|
assetUrl: "https://static...",
|
||||||
|
config: { /* public subset */ },
|
||||||
|
currentUser: { id, username, display_name, avatar } | null,
|
||||||
|
relations: [ /* serialized RelationDef list */ ]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Sources: `ctx` has `blog_url`, `market_url`, etc. `g.user` has user info. `shared/infrastructure/urls.py` has the URL map.
|
||||||
|
|
||||||
|
Add JS primitives reading from `__sxConfig`: `app-url`, `asset-url`, `config`, `current-user`, `relations-from`.
|
||||||
|
|
||||||
|
`url-for` has no JS equivalent — isomorphic code uses `app-url` instead.
|
||||||
|
|
||||||
|
### 1c: Add `defpage` to sx.js evaluator
|
||||||
|
|
||||||
|
Add `defpage` to `SPECIAL_FORMS`. Parse the declaration, store it in `_componentEnv` under `"page:name"` (same registry as components). The page definition includes: name, path pattern, auth requirement, layout spec, and unevaluated AST for data/content/filter/aside/menu slots.
|
||||||
|
|
||||||
|
Since pages live in `_componentEnv`, they're automatically included in the component hash, cached in localStorage, and skipped when the hash matches. No separate `<script data-pages>` block needed — they ship with components.
|
||||||
|
|
||||||
|
**Files:** `shared/static/scripts/sx.js`, `shared/sx/helpers.py`
|
||||||
|
|
||||||
|
**Verify:** `(format-date "2024-03-15" "%d %b %Y")` produces same output in Python and JS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Server-Side Rendering (SSR)
|
||||||
|
|
||||||
|
Full-page HTML rendering on the server for SEO and first-paint.
|
||||||
|
|
||||||
|
### 2a: Add `render_mode` to `execute_page()`
|
||||||
|
|
||||||
|
In `shared/sx/pages.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def execute_page(..., render_mode: str = "client") -> str:
|
||||||
|
```
|
||||||
|
|
||||||
|
When `render_mode="server"`:
|
||||||
|
- Evaluate all slots via `async_render()` (→ HTML) instead of `async_eval_to_sx()` (→ sx source)
|
||||||
|
- Layout headers also rendered to HTML
|
||||||
|
- Pass to new `ssr_page()` instead of `sx_page()`
|
||||||
|
|
||||||
|
### 2b: Create `ssr_page()` in helpers.py
|
||||||
|
|
||||||
|
Wraps pre-rendered HTML in a document shell:
|
||||||
|
- Same `<head>` (CSS, CSRF, meta)
|
||||||
|
- Rendered HTML inline in `<body>` — no `<script type="text/sx" data-mount>`
|
||||||
|
- Still ships component defs in `<script type="text/sx" data-components>` (client needs them for subsequent navigation)
|
||||||
|
- Still includes sx.js + body.js (for SPA takeover after first paint)
|
||||||
|
- Adds `<meta name="sx-ssr" content="true">`
|
||||||
|
- Injects `__sxConfig` (Phase 1b)
|
||||||
|
|
||||||
|
### 2c: SSR trigger
|
||||||
|
|
||||||
|
Utility `should_ssr(request)`:
|
||||||
|
- Bot UA patterns → SSR
|
||||||
|
- `?_render=server` → SSR (debug)
|
||||||
|
- `SX-Request: true` header → always client
|
||||||
|
- Per-page opt-in via `defpage :ssr true`
|
||||||
|
- Default → client (current behavior)
|
||||||
|
|
||||||
|
### 2d: Hydration in sx.js
|
||||||
|
|
||||||
|
When sx.js detects `<meta name="sx-ssr">`:
|
||||||
|
- Skip `Sx.mount()` — DOM already correct
|
||||||
|
- Run `SxEngine.process(document.body)` — bind sx-get/post handlers
|
||||||
|
- Run `Sx.hydrate()` — process `[data-sx]` elements
|
||||||
|
- Load component defs into registry (for subsequent navigations)
|
||||||
|
|
||||||
|
**Files:** `shared/sx/pages.py`, `shared/sx/helpers.py`, `shared/static/scripts/sx.js`
|
||||||
|
|
||||||
|
**Verify:** Googlebot UA → response has rendered HTML, no `<script data-mount>`. Normal UA → unchanged behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Public Data API
|
||||||
|
|
||||||
|
Expose browser-accessible JSON endpoints mirroring internal `/internal/data/` queries.
|
||||||
|
|
||||||
|
### 3a: Shared blueprint factory
|
||||||
|
|
||||||
|
New `shared/sx/api_data.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def create_public_data_blueprint(service_name: str) -> Blueprint:
|
||||||
|
"""Session-authed public data blueprint at /api/data/"""
|
||||||
|
```
|
||||||
|
|
||||||
|
Queries registered with auth level: `"public"`, `"login"`, `"admin"`. Validates session (not HMAC). Returns JSON.
|
||||||
|
|
||||||
|
### 3b: Extract and share handler implementations
|
||||||
|
|
||||||
|
Refactor `bp/data/routes.py` per service — separate query logic from HMAC auth. Same function serves both internal and public paths.
|
||||||
|
|
||||||
|
### 3c: Per-service public data blueprints
|
||||||
|
|
||||||
|
New `bp/api_data/routes.py` per service:
|
||||||
|
|
||||||
|
| Service | Public queries | Auth |
|
||||||
|
|---------|---------------|------|
|
||||||
|
| blog | `post-by-slug`, `post-by-id`, `search-posts` | public |
|
||||||
|
| market | `products-by-ids`, `marketplaces-for-container` | public |
|
||||||
|
| events | `visible-entries-for-period`, `calendars-for-container`, `entries-for-page` | public |
|
||||||
|
| cart | `cart-summary`, `cart-items` | login |
|
||||||
|
| likes | `is-liked`, `liked-slugs` | login |
|
||||||
|
| account | `newsletters` | public |
|
||||||
|
|
||||||
|
Admin queries and write-actions stay internal only.
|
||||||
|
|
||||||
|
### 3d: Public fragment endpoints
|
||||||
|
|
||||||
|
The existing internal fragment system (`/internal/fragments/<type>`, HMAC-signed) needs public equivalents. Each service already has `create_handler_blueprint()` mounting defhandler fragments. Add a parallel public endpoint:
|
||||||
|
|
||||||
|
`GET /api/fragments/<type>?params...` — session-authed, returns `text/sx` (same wire format the client already handles via SxEngine).
|
||||||
|
|
||||||
|
This can reuse the same `execute_handler()` machinery — the only difference is auth (session vs HMAC). The blueprint factory in `shared/sx/api_data.py` can handle both data and fragment registration:
|
||||||
|
|
||||||
|
```python
|
||||||
|
bp.register_fragment("container-cards", handler_fn, auth="public")
|
||||||
|
```
|
||||||
|
|
||||||
|
The client's `(frag ...)` primitive then fetches from these public endpoints instead of the HMAC-signed internal ones.
|
||||||
|
|
||||||
|
### 3e: Register in app factories
|
||||||
|
|
||||||
|
Each service's `app.py` registers the new blueprint.
|
||||||
|
|
||||||
|
**Files:** New `shared/sx/api_data.py`, new `{service}/bp/api_data/routes.py` per service, `{service}/app.py`
|
||||||
|
|
||||||
|
**Verify:** `curl /api/data/post-by-slug?slug=test` → JSON. `curl /api/fragments/container-cards?type=page&id=1` → sx source. Login-gated query without session → 401.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Client Data Primitives
|
||||||
|
|
||||||
|
Async data-fetching in sx.js so I/O primitives work client-side via the public API.
|
||||||
|
|
||||||
|
### 4a: Async evaluator — `sxEvalAsync()`
|
||||||
|
|
||||||
|
New function in `sx.js` returning a `Promise`. Mirrors `async_eval.py`:
|
||||||
|
- Literals/symbols → `Promise.resolve(syncValue)`
|
||||||
|
- I/O primitives (`query`, `service`, `frag`, etc.) → `fetch()` calls to `/api/data/`
|
||||||
|
- Control flow → sequential async with short-circuit
|
||||||
|
- `map`/`filter` with I/O → `Promise.all`
|
||||||
|
|
||||||
|
### 4b: I/O primitive dispatch
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
IO_PRIMITIVES = {
|
||||||
|
"query": (svc, name, kw) => fetch(__sxConfig.appUrls[svc] + "/api/data/" + name + "?" + params(kw), {credentials:"include"}).then(r=>r.json()),
|
||||||
|
"service": (method, kw) => fetch("/api/data/" + method + "?" + params(kw), {credentials:"include"}).then(r=>r.json()),
|
||||||
|
"frag": (svc, type, kw) => fetch(__sxConfig.appUrls[svc] + "/api/fragments/" + type + "?" + params(kw), {credentials:"include"}).then(r=>r.text()),
|
||||||
|
"current-user": () => Promise.resolve(__sxConfig.currentUser),
|
||||||
|
"request-arg": (name) => Promise.resolve(new URLSearchParams(location.search).get(name)),
|
||||||
|
"request-path": () => Promise.resolve(location.pathname),
|
||||||
|
"nav-tree": () => fetch("/api/data/nav-tree", {credentials:"include"}).then(r=>r.json()),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4c: Async DOM renderer — `renderDOMAsync()`
|
||||||
|
|
||||||
|
Two-pass (avoids restructuring sync renderer):
|
||||||
|
1. Walk AST, collect I/O call sites with placeholders
|
||||||
|
2. `Promise.all` to resolve all I/O in parallel
|
||||||
|
3. Substitute resolved values into AST
|
||||||
|
4. Call existing sync `renderDOM()` on resolved tree
|
||||||
|
|
||||||
|
### 4d: Wire into `Sx.mount()`
|
||||||
|
|
||||||
|
Detect I/O nodes. If present → async path. Otherwise → existing sync path (zero overhead for pure components).
|
||||||
|
|
||||||
|
**Files:** `shared/static/scripts/sx.js` (major addition)
|
||||||
|
|
||||||
|
**Verify:** Page with `(query "blog" "post-by-slug" :slug "test")` in sx source → client fetches `/api/data/post-by-slug?slug=test`, renders result.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Data-Only Navigation
|
||||||
|
|
||||||
|
When the client already has page components cached, navigation requires only a data fetch — no sx source from the server.
|
||||||
|
|
||||||
|
### 5a: Page components in the registry
|
||||||
|
|
||||||
|
`defpage` definitions are already in `_componentEnv` (Phase 1c) and cached in localStorage with the component hash. On navigation, if the hash is valid, the client has all page definitions locally.
|
||||||
|
|
||||||
|
Build a `_pageRegistry` mapping URL path patterns → page definitions, populated when `defpage` forms are evaluated. Path patterns (`/posts/<slug>/`) converted to regex matchers for URL matching.
|
||||||
|
|
||||||
|
### 5b: Navigation intercept
|
||||||
|
|
||||||
|
Extend SxEngine's link click handler:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Extract URL path from clicked link
|
||||||
|
2. Match against _pageRegistry
|
||||||
|
3. If matched:
|
||||||
|
a. Evaluate :data slot via sxEvalAsync() → parallel API fetches
|
||||||
|
b. Render :content/:filter/:aside via renderDOMAsync()
|
||||||
|
c. Morph into existing ~app-body (headers persist, slots update)
|
||||||
|
d. Push history state
|
||||||
|
e. Update document title
|
||||||
|
4. If not matched → existing server fetch (graceful fallback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5c: Data delivery — flexible per page
|
||||||
|
|
||||||
|
Three modes available (see Context section). The page definition can declare its preference:
|
||||||
|
|
||||||
|
```scheme
|
||||||
|
(defpage blog-post
|
||||||
|
:path "/posts/<slug>/"
|
||||||
|
:data-mode :server ; :server (bundled), :client (fetch individually), :hybrid
|
||||||
|
:data (query "blog" "post-by-slug" :slug slug)
|
||||||
|
:content (~post-detail post))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mode :server** — Client sends `SX-Page: blog-post` header on navigation. Server evaluates `:data` slot (all queries, including cross-service), returns single JSON blob:
|
||||||
|
```python
|
||||||
|
if request.headers.get("SX-Page"):
|
||||||
|
data = await evaluate_data_slot(page_def, url_params)
|
||||||
|
return jsonify(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mode :client** — Client evaluates `:data` slot locally via `sxEvalAsync()`. Each `(query ...)` hits `/api/data/` independently. Each `(frag ...)` hits `/api/fragments/`. No server data endpoint needed.
|
||||||
|
|
||||||
|
**Mode :hybrid** — Server bundles own-service data (direct DB). Client fetches cross-service data and fragments in parallel. The `:data` slot is split: server evaluates local queries, returns partial bundle + a manifest of remaining queries. Client resolves the rest.
|
||||||
|
|
||||||
|
Default mode can be `:server` (fewest round-trips, simplest). Pages opt into `:client` or `:hybrid` when they want more decoupling or when cross-service data is heavy and benefits from parallel client fetches.
|
||||||
|
|
||||||
|
### 5d: Popstate handling
|
||||||
|
|
||||||
|
On browser back/forward:
|
||||||
|
1. Check `_pageRegistry` for popped URL
|
||||||
|
2. If matched → client render (same as 5b)
|
||||||
|
3. If not → existing server fetch + morph
|
||||||
|
|
||||||
|
### 5e: Graceful fallback
|
||||||
|
|
||||||
|
Routes not in `_pageRegistry` fall through to server fetch. Partially migrated apps work — Python-only routes use server fetch, defpage routes get SPA behavior. No big-bang cutover.
|
||||||
|
|
||||||
|
**Files:** `shared/static/scripts/sx.js`, `shared/sx/helpers.py`, `shared/sx/pages.py`
|
||||||
|
|
||||||
|
**Verify:** Playwright: load page → click link to defpage route → assert no HTML response fetched (only JSON) → content correct → URL updated → back button works.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary: The Full Lifecycle
|
||||||
|
|
||||||
|
```
|
||||||
|
1. App startup: Python loads .sx files → defcomp + defpage registered in _COMPONENT_ENV
|
||||||
|
→ hash computed
|
||||||
|
|
||||||
|
2. First visit: Server sends HTML shell + component/page defs + __sxConfig + page sx source
|
||||||
|
Client evaluates, renders, caches defs in localStorage, sets cookie
|
||||||
|
|
||||||
|
3. Return visit: Cookie hash matches → server sends HTML shell with empty <script data-components>
|
||||||
|
Client loads defs from localStorage → renders page
|
||||||
|
|
||||||
|
4. SPA navigation: Client matches URL against _pageRegistry
|
||||||
|
→ fetches data from /api/data/ (or server data-only endpoint)
|
||||||
|
→ renders page component locally with fresh data
|
||||||
|
→ morphs DOM, pushes history
|
||||||
|
→ zero sx source transferred
|
||||||
|
|
||||||
|
5. Bot/SSR: Server detects bot UA → evaluates page server-side with direct DB queries
|
||||||
|
→ sends rendered HTML + component defs
|
||||||
|
→ client hydrates (binds handlers, no re-render)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration per Service
|
||||||
|
|
||||||
|
Each service migrates independently, no coordination needed:
|
||||||
|
1. Add public data blueprint (Phase 3) — immediate standalone value
|
||||||
|
2. Convert remaining Jinja routes to `defpage` — already in progress
|
||||||
|
3. Enable SSR for bots (Phase 2) — per-page opt-in
|
||||||
|
4. Client data primitives (Phase 4) — global once sx.js updated
|
||||||
|
5. Data-only navigation (Phase 5) — automatic for any `defpage` route
|
||||||
@@ -78,6 +78,10 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# --- defpage setup ---
|
||||||
|
from sxc.pages import setup_events_pages
|
||||||
|
setup_events_pages()
|
||||||
|
|
||||||
# All events: / — global view across all pages
|
# All events: / — global view across all pages
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
register_all_events(),
|
register_all_events(),
|
||||||
@@ -169,11 +173,16 @@ def create_app() -> "Quart":
|
|||||||
|
|
||||||
# Tickets blueprint — user-facing ticket views and QR codes
|
# Tickets blueprint — user-facing ticket views and QR codes
|
||||||
from bp.tickets.routes import register as register_tickets
|
from bp.tickets.routes import register as register_tickets
|
||||||
app.register_blueprint(register_tickets())
|
tickets_bp = register_tickets()
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(tickets_bp, "events", names=["my-tickets", "ticket-detail"])
|
||||||
|
app.register_blueprint(tickets_bp)
|
||||||
|
|
||||||
# Ticket admin — check-in interface (admin only)
|
# Ticket admin — check-in interface (admin only)
|
||||||
from bp.ticket_admin.routes import register as register_ticket_admin
|
from bp.ticket_admin.routes import register as register_ticket_admin
|
||||||
app.register_blueprint(register_ticket_admin())
|
ticket_admin_bp = register_ticket_admin()
|
||||||
|
mount_pages(ticket_admin_bp, "events", names=["ticket-admin"])
|
||||||
|
app.register_blueprint(ticket_admin_bp)
|
||||||
|
|
||||||
# --- oEmbed endpoint ---
|
# --- oEmbed endpoint ---
|
||||||
@app.get("/oembed")
|
@app.get("/oembed")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g
|
request, Blueprint, g
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -14,23 +14,18 @@ from shared.sx.helpers import sx_response
|
|||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
# ---------- Pages ----------
|
|
||||||
@bp.get("/")
|
|
||||||
@require_admin
|
|
||||||
async def admin(calendar_slug: str, **kwargs):
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
|
@bp.before_request
|
||||||
|
async def _prepare_page_data():
|
||||||
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_calendar_admin_page, render_calendar_admin_oob
|
from sx.sx_components import _calendar_admin_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
g.calendar_admin_content = _calendar_admin_main_panel_html(ctx)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
from shared.sx.pages import mount_pages
|
||||||
if not is_htmx_request():
|
mount_pages(bp, "events", names=["calendar-admin"])
|
||||||
html = await render_calendar_admin_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_calendar_admin_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/description/")
|
@bp.get("/description/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -1,29 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
make_response, Blueprint
|
request, Blueprint, g
|
||||||
)
|
)
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
# ---------- Pages ----------
|
@bp.before_request
|
||||||
@bp.get("/")
|
async def _prepare_page_data():
|
||||||
@require_admin
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
async def admin(entry_id: int, **kwargs):
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_entry_admin_page, render_entry_admin_oob
|
from sx.sx_components import _entry_admin_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
g.entry_admin_content = _entry_admin_main_panel_html(ctx)
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "events", names=["entry-admin"])
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_entry_admin_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_entry_admin_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -238,20 +238,18 @@ def register():
|
|||||||
"user_ticket_counts_by_type": user_ticket_counts_by_type,
|
"user_ticket_counts_by_type": user_ticket_counts_by_type,
|
||||||
"container_nav": container_nav,
|
"container_nav": container_nav,
|
||||||
}
|
}
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def get(entry_id: int, **rest):
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_entry_page, render_entry_oob
|
from sx.sx_components import _entry_main_panel_html, _entry_nav_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
g.entry_content = _entry_main_panel_html(ctx)
|
||||||
|
g.entry_menu = _entry_nav_html(ctx)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
from shared.sx.pages import mount_pages
|
||||||
if not is_htmx_request():
|
mount_pages(bp, "events", names=["entry-detail"])
|
||||||
html = await render_entry_page(tctx)
|
|
||||||
return await make_response(html, 200)
|
|
||||||
else:
|
|
||||||
sx_src = await render_entry_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -435,10 +433,10 @@ def register():
|
|||||||
nav_oob = await get_day_nav_oob(year, month, day)
|
nav_oob = await get_day_nav_oob(year, month, day)
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_entry_page
|
from sx.sx_components import _entry_main_panel_html
|
||||||
|
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
html = await render_entry_page(tctx)
|
html = _entry_main_panel_html(tctx)
|
||||||
return sx_response(html + nav_oob)
|
return sx_response(html + nav_oob)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
render_template, make_response, Blueprint
|
request, Blueprint, g
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
# ---------- Pages ----------
|
|
||||||
@bp.get("/")
|
|
||||||
@require_admin
|
|
||||||
async def admin(year: int, month: int, day: int, **kwargs):
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
@bp.before_request
|
||||||
from sx.sx_components import render_day_admin_page, render_day_admin_oob
|
async def _prepare_page_data():
|
||||||
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
|
return
|
||||||
|
from sx.sx_components import _day_admin_main_panel_html
|
||||||
|
g.day_admin_content = _day_admin_main_panel_html({})
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "events", names=["day-admin"])
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_day_admin_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_day_admin_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ from .services.markets import (
|
|||||||
soft_delete as svc_soft_delete,
|
soft_delete as svc_soft_delete,
|
||||||
)
|
)
|
||||||
|
|
||||||
from shared.browser.app.redis_cacher import cache_page, clear_cache
|
from shared.browser.app.redis_cacher import clear_cache
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
@@ -22,18 +21,17 @@ def register():
|
|||||||
async def inject_root():
|
async def inject_root():
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def home(**kwargs):
|
async def _prepare_page_data():
|
||||||
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_markets_page, render_markets_oob
|
from sx.sx_components import _markets_main_panel_html
|
||||||
|
|
||||||
ctx = await get_template_context()
|
ctx = await get_template_context()
|
||||||
if not is_htmx_request():
|
g.markets_content = _markets_main_panel_html(ctx)
|
||||||
html = await render_markets_page(ctx)
|
|
||||||
return await make_response(html)
|
from shared.sx.pages import mount_pages
|
||||||
else:
|
mount_pages(bp, "events", names=["events-markets"])
|
||||||
sx_src = await render_markets_oob(ctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/new/")
|
@bp.post("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, jsonify
|
request, make_response, Blueprint, g, jsonify
|
||||||
)
|
)
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
@@ -23,33 +23,32 @@ from shared.browser.app.utils import (
|
|||||||
parse_time,
|
parse_time,
|
||||||
parse_cost
|
parse_cost
|
||||||
)
|
)
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("slot", __name__, url_prefix='/<int:slot_id>')
|
bp = Blueprint("slot", __name__, url_prefix='/<int:slot_id>')
|
||||||
|
|
||||||
# ---------- Pages ----------
|
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def get(slot_id: int, **kwargs):
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
slot = await svc_get_slot(g.s, slot_id)
|
return
|
||||||
|
slot_id = (request.view_args or {}).get("slot_id")
|
||||||
|
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
|
||||||
if not slot:
|
if not slot:
|
||||||
return await make_response("Not found", 404)
|
from quart import abort
|
||||||
|
abort(404)
|
||||||
from shared.sx.page import get_template_context
|
g.slot = slot
|
||||||
from sx.sx_components import render_slot_page, render_slot_oob
|
calendar = getattr(g, "calendar", None)
|
||||||
|
from sx.sx_components import render_slot_main_panel
|
||||||
|
g.slot_content = render_slot_main_panel(slot, calendar)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
@bp.context_processor
|
||||||
if not is_htmx_request():
|
async def _inject_slot():
|
||||||
html = await render_slot_page(tctx)
|
return {"slot": getattr(g, "slot", None)}
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_slot_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "events", names=["slot-detail"])
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, jsonify
|
request, Blueprint, g, jsonify
|
||||||
)
|
)
|
||||||
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
@@ -19,21 +19,16 @@ from shared.browser.app.utils import (
|
|||||||
parse_time,
|
parse_time,
|
||||||
parse_cost
|
parse_cost
|
||||||
)
|
)
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("slots", __name__, url_prefix='/slots')
|
bp = Blueprint("slots", __name__, url_prefix='/slots')
|
||||||
|
|
||||||
# ---------- Pages ----------
|
|
||||||
|
|
||||||
bp.register_blueprint(
|
bp.register_blueprint(
|
||||||
register_slot()
|
register_slot()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@bp.context_processor
|
@bp.context_processor
|
||||||
async def get_slots():
|
async def get_slots():
|
||||||
calendar = getattr(g, "calendar", None)
|
calendar = getattr(g, "calendar", None)
|
||||||
@@ -43,19 +38,17 @@ def register():
|
|||||||
}
|
}
|
||||||
return {"slots": []}
|
return {"slots": []}
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def get(**kwargs):
|
async def _prepare_page_data():
|
||||||
from shared.sx.page import get_template_context
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
from sx.sx_components import render_slots_page, render_slots_oob
|
return
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
tctx = await get_template_context()
|
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
|
||||||
if not is_htmx_request():
|
from sx.sx_components import render_slots_table
|
||||||
html = await render_slots_page(tctx)
|
g.slots_content = render_slots_table(slots, calendar)
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_slots_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "events", names=["slots-listing"])
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
Blueprint, g, request, render_template, make_response, jsonify,
|
Blueprint, g, request, make_response,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
@@ -34,12 +34,10 @@ logger = logging.getLogger(__name__)
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
|
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def dashboard():
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
"""Ticket admin dashboard with QR scanner and recent tickets."""
|
return
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
# Get recent tickets
|
# Get recent tickets
|
||||||
result = await g.s.execute(
|
result = await g.s.execute(
|
||||||
select(Ticket)
|
select(Ticket)
|
||||||
@@ -72,15 +70,9 @@ def register() -> Blueprint:
|
|||||||
}
|
}
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_ticket_admin_page, render_ticket_admin_oob
|
from sx.sx_components import _ticket_admin_main_panel_html
|
||||||
|
|
||||||
ctx = await get_template_context()
|
ctx = await get_template_context()
|
||||||
if not is_htmx_request():
|
g.ticket_admin_content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
||||||
html = await render_ticket_admin_page(ctx, tickets, stats)
|
|
||||||
return await make_response(html, 200)
|
|
||||||
else:
|
|
||||||
sx_src = await render_ticket_admin_oob(ctx, tickets, stats)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/entry/<int:entry_id>/")
|
@bp.get("/entry/<int:entry_id>/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, jsonify
|
request, make_response, Blueprint, g, jsonify
|
||||||
)
|
)
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
@@ -16,30 +16,37 @@ from .services.ticket import (
|
|||||||
from ..ticket_types.services.tickets import (
|
from ..ticket_types.services.tickets import (
|
||||||
list_ticket_types as svc_list_ticket_types,
|
list_ticket_types as svc_list_ticket_types,
|
||||||
)
|
)
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("ticket_type", __name__, url_prefix='/<int:ticket_type_id>')
|
bp = Blueprint("ticket_type", __name__, url_prefix='/<int:ticket_type_id>')
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def get(ticket_type_id: int, **kwargs):
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
"""View a single ticket type."""
|
return
|
||||||
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id)
|
ticket_type_id = (request.view_args or {}).get("ticket_type_id")
|
||||||
|
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
|
||||||
if not ticket_type:
|
if not ticket_type:
|
||||||
return await make_response("Not found", 404)
|
from quart import abort
|
||||||
from shared.sx.page import get_template_context
|
abort(404)
|
||||||
from sx.sx_components import render_ticket_type_page, render_ticket_type_oob
|
g.ticket_type = ticket_type
|
||||||
|
entry = getattr(g, "entry", None)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
va = request.view_args or {}
|
||||||
|
from sx.sx_components import render_ticket_type_main_panel
|
||||||
|
g.ticket_type_content = render_ticket_type_main_panel(
|
||||||
|
ticket_type, entry, calendar,
|
||||||
|
va.get("day"), va.get("month"), va.get("year"),
|
||||||
|
)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
@bp.context_processor
|
||||||
if not is_htmx_request():
|
async def _inject_ticket_type():
|
||||||
html = await render_ticket_type_page(tctx)
|
return {"ticket_type": getattr(g, "ticket_type", None)}
|
||||||
return await make_response(html)
|
|
||||||
else:
|
from shared.sx.pages import mount_pages
|
||||||
sx_src = await render_ticket_type_oob(tctx)
|
mount_pages(bp, "events", names=["ticket-type-detail"])
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, jsonify
|
request, Blueprint, g, jsonify
|
||||||
)
|
)
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
@@ -14,7 +14,6 @@ from .services.tickets import (
|
|||||||
|
|
||||||
from ..ticket_type.routes import register as register_ticket_type
|
from ..ticket_type.routes import register as register_ticket_type
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
@@ -36,19 +35,22 @@ def register():
|
|||||||
}
|
}
|
||||||
return {"ticket_types": []}
|
return {"ticket_types": []}
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def get(**kwargs):
|
async def _prepare_page_data():
|
||||||
"""List all ticket types for the current entry."""
|
if "defpage_" not in (request.endpoint or ""):
|
||||||
from shared.sx.page import get_template_context
|
return
|
||||||
from sx.sx_components import render_ticket_types_page, render_ticket_types_oob
|
entry = getattr(g, "entry", None)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
|
||||||
|
va = request.view_args or {}
|
||||||
|
from sx.sx_components import render_ticket_types_table
|
||||||
|
g.ticket_types_content = render_ticket_types_table(
|
||||||
|
ticket_types, entry, calendar,
|
||||||
|
va.get("day"), va.get("month"), va.get("year"),
|
||||||
|
)
|
||||||
|
|
||||||
tctx = await get_template_context()
|
from shared.sx.pages import mount_pages
|
||||||
if not is_htmx_request():
|
mount_pages(bp, "events", names=["ticket-types-listing"])
|
||||||
html = await render_ticket_types_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_ticket_types_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
Blueprint, g, request, render_template, make_response,
|
Blueprint, g, request, make_response,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
@@ -39,59 +39,43 @@ logger = logging.getLogger(__name__)
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("tickets", __name__, url_prefix="/tickets")
|
bp = Blueprint("tickets", __name__, url_prefix="/tickets")
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def my_tickets():
|
async def _prepare_page_data():
|
||||||
"""List all tickets for the current user/session."""
|
ep = request.endpoint or ""
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
if "defpage_my_tickets" in ep:
|
||||||
|
ident = current_cart_identity()
|
||||||
ident = current_cart_identity()
|
tickets = await get_user_tickets(
|
||||||
tickets = await get_user_tickets(
|
g.s,
|
||||||
g.s,
|
user_id=ident["user_id"],
|
||||||
user_id=ident["user_id"],
|
session_id=ident["session_id"],
|
||||||
session_id=ident["session_id"],
|
)
|
||||||
)
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _tickets_main_panel_html
|
||||||
from shared.sx.page import get_template_context
|
ctx = await get_template_context()
|
||||||
from sx.sx_components import render_tickets_page, render_tickets_oob
|
g.tickets_content = _tickets_main_panel_html(ctx, tickets)
|
||||||
|
elif "defpage_ticket_detail" in ep:
|
||||||
ctx = await get_template_context()
|
code = (request.view_args or {}).get("code")
|
||||||
if not is_htmx_request():
|
ticket = await get_ticket_by_code(g.s, code) if code else None
|
||||||
html = await render_tickets_page(ctx, tickets)
|
if not ticket:
|
||||||
return await make_response(html, 200)
|
from quart import abort
|
||||||
else:
|
abort(404)
|
||||||
sx_src = await render_tickets_oob(ctx, tickets)
|
# Verify ownership
|
||||||
return sx_response(sx_src)
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"] is not None:
|
||||||
@bp.get("/<code>/")
|
if ticket.user_id != ident["user_id"]:
|
||||||
async def ticket_detail(code: str):
|
from quart import abort
|
||||||
"""View a single ticket with QR code."""
|
abort(404)
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
elif ident["session_id"] is not None:
|
||||||
|
if ticket.session_id != ident["session_id"]:
|
||||||
ticket = await get_ticket_by_code(g.s, code)
|
from quart import abort
|
||||||
if not ticket:
|
abort(404)
|
||||||
return await make_response("Ticket not found", 404)
|
else:
|
||||||
|
from quart import abort
|
||||||
# Verify ownership
|
abort(404)
|
||||||
ident = current_cart_identity()
|
from shared.sx.page import get_template_context
|
||||||
if ident["user_id"] is not None:
|
from sx.sx_components import _ticket_detail_panel_html
|
||||||
if ticket.user_id != ident["user_id"]:
|
ctx = await get_template_context()
|
||||||
return await make_response("Ticket not found", 404)
|
g.ticket_detail_content = _ticket_detail_panel_html(ctx, ticket)
|
||||||
elif ident["session_id"] is not None:
|
|
||||||
if ticket.session_id != ident["session_id"]:
|
|
||||||
return await make_response("Ticket not found", 404)
|
|
||||||
else:
|
|
||||||
return await make_response("Ticket not found", 404)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_ticket_detail_page, render_ticket_detail_oob
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_ticket_detail_page(ctx, ticket)
|
|
||||||
return await make_response(html, 200)
|
|
||||||
else:
|
|
||||||
sx_src = await render_ticket_detail_oob(ctx, ticket)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/buy/")
|
@bp.post("/buy/")
|
||||||
@clear_cache(tag="calendars", tag_scope="all")
|
@clear_cache(tag="calendars", tag_scope="all")
|
||||||
|
|||||||
@@ -191,11 +191,11 @@ def _calendar_nav_sx(ctx: dict) -> str:
|
|||||||
select_colours = ctx.get("select_colours", "")
|
select_colours = ctx.get("select_colours", "")
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
slots_href = url_for("calendar.slots.get", calendar_slug=cal_slug)
|
slots_href = url_for("calendar.slots.defpage_slots_listing", calendar_slug=cal_slug)
|
||||||
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
|
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
|
||||||
label="Slots", select_colours=select_colours))
|
label="Slots", select_colours=select_colours))
|
||||||
if is_admin:
|
if is_admin:
|
||||||
admin_href = url_for("calendar.admin.admin", calendar_slug=cal_slug)
|
admin_href = url_for("calendar.admin.defpage_calendar_admin", calendar_slug=cal_slug)
|
||||||
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
|
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
|
||||||
select_colours=select_colours))
|
select_colours=select_colours))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -319,7 +319,7 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
nav_parts = []
|
nav_parts = []
|
||||||
if cal_slug:
|
if cal_slug:
|
||||||
for endpoint, label in [
|
for endpoint, label in [
|
||||||
("calendar.slots.get", "slots"),
|
("calendar.slots.defpage_slots_listing", "slots"),
|
||||||
("calendar.admin.calendar_description_edit", "description"),
|
("calendar.admin.calendar_description_edit", "description"),
|
||||||
]:
|
]:
|
||||||
href = url_for(endpoint, calendar_slug=cal_slug)
|
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||||
@@ -339,7 +339,7 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||||
"""Build the markets section header row."""
|
"""Build the markets section header row."""
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
link_href = url_for("markets.home")
|
link_href = url_for("markets.defpage_events_markets")
|
||||||
return sx_call("menu-row-sx", id="markets-row", level=3,
|
return sx_call("menu-row-sx", id="markets-row", level=3,
|
||||||
link_href=link_href,
|
link_href=link_href,
|
||||||
link_label_content=SxExpr(sx_call("events-markets-label")),
|
link_label_content=SxExpr(sx_call("events-markets-label")),
|
||||||
@@ -594,7 +594,7 @@ def _day_row_html(ctx: dict, entry) -> str:
|
|||||||
# Slot/Time
|
# Slot/Time
|
||||||
slot = getattr(entry, "slot", None)
|
slot = getattr(entry, "slot", None)
|
||||||
if slot:
|
if slot:
|
||||||
slot_href = url_for("calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=slot.id)
|
slot_href = url_for("calendar.slots.slot.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_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 ""
|
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
||||||
slot_html = sx_call("events-day-row-slot",
|
slot_html = sx_call("events-day-row-slot",
|
||||||
@@ -774,7 +774,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
|||||||
ticket_cards = []
|
ticket_cards = []
|
||||||
if tickets:
|
if tickets:
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
href = url_for("tickets.ticket_detail", code=ticket.code)
|
href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
|
||||||
entry = getattr(ticket, "entry", None)
|
entry = getattr(ticket, "entry", None)
|
||||||
entry_name = entry.name if entry else "Unknown event"
|
entry_name = entry.name if entry else "Unknown event"
|
||||||
tt = getattr(ticket, "ticket_type", None)
|
tt = getattr(ticket, "ticket_type", None)
|
||||||
@@ -819,7 +819,7 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
|||||||
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
||||||
header_bg = bg_map.get(state, "bg-stone-50")
|
header_bg = bg_map.get(state, "bg-stone-50")
|
||||||
entry_name = entry.name if entry else "Ticket"
|
entry_name = entry.name if entry else "Ticket"
|
||||||
back_href = url_for("tickets.my_tickets")
|
back_href = url_for("tickets.defpage_my_tickets")
|
||||||
|
|
||||||
# Badge with larger sizing
|
# Badge with larger sizing
|
||||||
badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
||||||
@@ -1400,42 +1400,7 @@ async def render_day_oob(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Day admin
|
# Calendar admin helper
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_day_admin_page(ctx: dict) -> str:
|
|
||||||
"""Full page: day admin."""
|
|
||||||
content = _day_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
|
||||||
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
|
||||||
+ _day_admin_header_sx(ctx))
|
|
||||||
hdr = root_hdr + post_hdr + header_child_sx(child)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_day_admin_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: day admin."""
|
|
||||||
content = _day_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
|
||||||
+ _calendar_header_sx(ctx, oob=True))
|
|
||||||
oobs += oob_header_sx("day-header-child", "day-admin-header-child",
|
|
||||||
_day_admin_header_sx(ctx))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"day-row", "day-header-child",
|
|
||||||
"day-admin-row", "day-admin-header-child")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Calendar admin
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
|
def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
|
||||||
@@ -1445,140 +1410,6 @@ def _events_post_admin_header_sx(ctx: dict, *, oob: bool = False,
|
|||||||
return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|
return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
|
||||||
|
|
||||||
|
|
||||||
async def render_calendar_admin_page(ctx: dict) -> str:
|
|
||||||
"""Full page: calendar admin."""
|
|
||||||
content = _calendar_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
|
||||||
child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx)
|
|
||||||
hdr = root_hdr + post_hdr + header_child_sx(child)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_calendar_admin_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: calendar admin."""
|
|
||||||
content = _calendar_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
|
||||||
+ _calendar_header_sx(ctx, oob=True))
|
|
||||||
oobs += oob_header_sx("calendar-header-child", "calendar-admin-header-child",
|
|
||||||
_calendar_admin_header_sx(ctx))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"calendar-admin-row", "calendar-admin-header-child")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Slots
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_slots_page(ctx: dict) -> str:
|
|
||||||
"""Full page: slots listing."""
|
|
||||||
from quart import g
|
|
||||||
slots = ctx.get("slots") or []
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
content = render_slots_table(slots, calendar)
|
|
||||||
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
|
||||||
child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx)
|
|
||||||
hdr = root_hdr + post_hdr + header_child_sx(child)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_slots_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: slots listing."""
|
|
||||||
slots = ctx.get("slots") or []
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
content = render_slots_table(slots, calendar)
|
|
||||||
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
|
||||||
+ _calendar_admin_header_sx(ctx, oob=True))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"calendar-admin-row", "calendar-admin-header-child")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Tickets
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_tickets_page(ctx: dict, tickets: list) -> str:
|
|
||||||
"""Full page: my tickets."""
|
|
||||||
content = _tickets_main_panel_html(ctx, tickets)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_tickets_oob(ctx: dict, tickets: list) -> str:
|
|
||||||
"""OOB response: my tickets."""
|
|
||||||
content = _tickets_main_panel_html(ctx, tickets)
|
|
||||||
return oob_page_sx(content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_detail_page(ctx: dict, ticket) -> str:
|
|
||||||
"""Full page: ticket detail with QR."""
|
|
||||||
content = _ticket_detail_panel_html(ctx, ticket)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_detail_oob(ctx: dict, ticket) -> str:
|
|
||||||
"""OOB response: ticket detail."""
|
|
||||||
content = _ticket_detail_panel_html(ctx, ticket)
|
|
||||||
return oob_page_sx(content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Ticket admin
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_ticket_admin_page(ctx: dict, tickets: list, stats: dict) -> str:
|
|
||||||
"""Full page: ticket admin dashboard."""
|
|
||||||
content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_admin_oob(ctx: dict, tickets: list, stats: dict) -> str:
|
|
||||||
"""OOB response: ticket admin dashboard."""
|
|
||||||
content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
|
||||||
return oob_page_sx(content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Markets
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_markets_page(ctx: dict) -> str:
|
|
||||||
"""Full page: markets listing."""
|
|
||||||
content = _markets_main_panel_html(ctx)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = _post_header_sx(ctx) + _markets_header_sx(ctx)
|
|
||||||
hdr += header_child_sx(child)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_markets_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: markets listing."""
|
|
||||||
content = _markets_main_panel_html(ctx)
|
|
||||||
oobs = _post_header_sx(ctx, oob=True)
|
|
||||||
oobs += oob_header_sx("post-header-child", "markets-header-child",
|
|
||||||
_markets_header_sx(ctx))
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# POST / PUT / DELETE response components
|
# POST / PUT / DELETE response components
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -1939,36 +1770,6 @@ def _entry_nav_html(ctx: dict) -> str:
|
|||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Entry page / OOB rendering
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_entry_page(ctx: dict) -> str:
|
|
||||||
"""Full page: entry detail."""
|
|
||||||
content = _entry_main_panel_html(ctx)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = (_post_header_sx(ctx)
|
|
||||||
+ _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
|
||||||
+ _entry_header_html(ctx))
|
|
||||||
hdr += header_child_sx(child)
|
|
||||||
nav_html = _entry_nav_html(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_entry_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: entry detail."""
|
|
||||||
content = _entry_main_panel_html(ctx)
|
|
||||||
oobs = _day_header_sx(ctx, oob=True)
|
|
||||||
oobs += oob_header_sx("day-header-child", "entry-header-child",
|
|
||||||
_entry_header_html(ctx))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"day-row", "day-header-child",
|
|
||||||
"entry-row", "entry-header-child")
|
|
||||||
nav_html = _entry_nav_html(ctx)
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Entry optioned (confirm/decline/provisional response)
|
# Entry optioned (confirm/decline/provisional response)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -2364,7 +2165,7 @@ def render_slots_table(slots, calendar) -> str:
|
|||||||
rows_html = ""
|
rows_html = ""
|
||||||
if slots:
|
if slots:
|
||||||
for s in slots:
|
for s in slots:
|
||||||
slot_href = url_for("calendar.slots.slot.get", calendar_slug=cal_slug, slot_id=s.id)
|
slot_href = url_for("calendar.slots.slot.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)
|
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
||||||
desc = getattr(s, "description", "") or ""
|
desc = getattr(s, "description", "") or ""
|
||||||
|
|
||||||
@@ -2508,7 +2309,7 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
|||||||
|
|
||||||
tickets_html = ""
|
tickets_html = ""
|
||||||
for ticket in created_tickets:
|
for ticket in created_tickets:
|
||||||
href = url_for("tickets.ticket_detail", code=ticket.code)
|
href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
|
||||||
tickets_html += sx_call("events-buy-result-ticket",
|
tickets_html += sx_call("events-buy-result-ticket",
|
||||||
href=href, code_short=ticket.code[:12] + "...")
|
href=href, code_short=ticket.code[:12] + "...")
|
||||||
|
|
||||||
@@ -2518,7 +2319,7 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
|||||||
remaining_html = sx_call("events-buy-result-remaining",
|
remaining_html = sx_call("events-buy-result-remaining",
|
||||||
text=f"{remaining} ticket{r_suffix} remaining")
|
text=f"{remaining} ticket{r_suffix} remaining")
|
||||||
|
|
||||||
my_href = url_for("tickets.my_tickets")
|
my_href = url_for("tickets.defpage_my_tickets")
|
||||||
|
|
||||||
return cart_html + sx_call("events-buy-result",
|
return cart_html + sx_call("events-buy-result",
|
||||||
entry_id=str(entry.id),
|
entry_id=str(entry.id),
|
||||||
@@ -2610,7 +2411,7 @@ def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket
|
|||||||
return _adj_form(1, sx_call("events-adjust-cart-plus"),
|
return _adj_form(1, sx_call("events-adjust-cart-plus"),
|
||||||
extra_cls="flex items-center")
|
extra_cls="flex items-center")
|
||||||
|
|
||||||
my_tickets_href = url_for("tickets.my_tickets")
|
my_tickets_href = url_for("tickets.defpage_my_tickets")
|
||||||
minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
|
minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
|
||||||
cart_icon = sx_call("events-adjust-cart-icon",
|
cart_icon = sx_call("events-adjust-cart-icon",
|
||||||
href=my_tickets_href, count=str(count))
|
href=my_tickets_href, count=str(count))
|
||||||
@@ -2960,40 +2761,6 @@ def _entry_admin_main_panel_html(ctx: dict) -> str:
|
|||||||
is_selected=False)
|
is_selected=False)
|
||||||
|
|
||||||
|
|
||||||
async def render_entry_admin_page(ctx: dict) -> str:
|
|
||||||
"""Full page: entry admin."""
|
|
||||||
content = _entry_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
|
||||||
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
|
||||||
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx))
|
|
||||||
hdr = root_hdr + post_hdr + header_child_sx(child)
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_entry_admin_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: entry admin."""
|
|
||||||
content = _entry_admin_main_panel_html(ctx)
|
|
||||||
ctx = await _ensure_container_nav(ctx)
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
|
||||||
+ _entry_header_html(ctx, oob=True))
|
|
||||||
oobs += oob_header_sx("entry-header-child", "entry-admin-header-child",
|
|
||||||
_entry_admin_header_html(ctx))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"day-row", "day-header-child",
|
|
||||||
"entry-row", "entry-header-child",
|
|
||||||
"entry-admin-row", "entry-admin-header-child")
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Slot page / OOB (extends slots)
|
# Slot page / OOB (extends slots)
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -3027,45 +2794,6 @@ def _slot_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
child_id="slot-header-child", oob=oob)
|
child_id="slot-header-child", oob=oob)
|
||||||
|
|
||||||
|
|
||||||
async def render_slot_page(ctx: dict) -> str:
|
|
||||||
"""Full page: slot detail (extends slots page)."""
|
|
||||||
slot = ctx.get("slot")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
if not slot or not calendar:
|
|
||||||
return ""
|
|
||||||
content = render_slot_main_panel(slot, calendar)
|
|
||||||
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
root_hdr = root_header_sx(ctx)
|
|
||||||
post_hdr = _post_header_sx(ctx)
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
|
||||||
child = (admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx)
|
|
||||||
+ _slot_header_html(ctx))
|
|
||||||
hdr = root_hdr + post_hdr + header_child_sx(child)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_slot_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: slot detail."""
|
|
||||||
slot = ctx.get("slot")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
if not slot or not calendar:
|
|
||||||
return ""
|
|
||||||
content = render_slot_main_panel(slot, calendar)
|
|
||||||
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
|
||||||
+ _calendar_admin_header_sx(ctx, oob=True))
|
|
||||||
oobs += oob_header_sx("calendar-admin-header-child", "slot-header-child",
|
|
||||||
_slot_header_html(ctx))
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child",
|
|
||||||
"calendar-row", "calendar-header-child",
|
|
||||||
"calendar-admin-row", "calendar-admin-header-child",
|
|
||||||
"slot-row", "slot-header-child")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Slot edit form
|
# Slot edit form
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
@@ -3243,40 +2971,6 @@ def _ticket_types_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob)
|
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child", oob=oob)
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_types_page(ctx: dict) -> str:
|
|
||||||
"""Full page: ticket types listing (extends entry admin)."""
|
|
||||||
ticket_types = ctx.get("ticket_types") or []
|
|
||||||
entry = ctx.get("entry")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
day = ctx.get("day")
|
|
||||||
month = ctx.get("month")
|
|
||||||
year = ctx.get("year")
|
|
||||||
content = render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = (_post_header_sx(ctx)
|
|
||||||
+ _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
|
||||||
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx)
|
|
||||||
+ _ticket_types_header_html(ctx))
|
|
||||||
hdr += header_child_sx(child)
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_types_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: ticket types listing."""
|
|
||||||
ticket_types = ctx.get("ticket_types") or []
|
|
||||||
entry = ctx.get("entry")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
day = ctx.get("day")
|
|
||||||
month = ctx.get("month")
|
|
||||||
year = ctx.get("year")
|
|
||||||
content = render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
|
|
||||||
oobs = _entry_admin_header_html(ctx, oob=True)
|
|
||||||
oobs += oob_header_sx("entry-admin-header-child", "ticket_types-header-child",
|
|
||||||
_ticket_types_header_html(ctx))
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Ticket type page / OOB
|
# Ticket type page / OOB
|
||||||
@@ -3317,41 +3011,6 @@ def _ticket_type_header_html(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob)
|
nav=SxExpr(nav_html) if nav_html else None, child_id="ticket_type-header-child-inner", oob=oob)
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_type_page(ctx: dict) -> str:
|
|
||||||
"""Full page: single ticket type detail (extends ticket types)."""
|
|
||||||
ticket_type = ctx.get("ticket_type")
|
|
||||||
entry = ctx.get("entry")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
day = ctx.get("day")
|
|
||||||
month = ctx.get("month")
|
|
||||||
year = ctx.get("year")
|
|
||||||
content = render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = (_post_header_sx(ctx)
|
|
||||||
+ _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
|
||||||
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx)
|
|
||||||
+ _ticket_types_header_html(ctx) + _ticket_type_header_html(ctx))
|
|
||||||
hdr += header_child_sx(child)
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_ticket_type_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: single ticket type detail."""
|
|
||||||
ticket_type = ctx.get("ticket_type")
|
|
||||||
entry = ctx.get("entry")
|
|
||||||
calendar = ctx.get("calendar")
|
|
||||||
day = ctx.get("day")
|
|
||||||
month = ctx.get("month")
|
|
||||||
year = ctx.get("year")
|
|
||||||
content = render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
|
|
||||||
oobs = _ticket_types_header_html(ctx, oob=True)
|
|
||||||
oobs += oob_header_sx("ticket_types-header-child", "ticket_type-header-child",
|
|
||||||
_ticket_type_header_html(ctx))
|
|
||||||
nav_html = sx_call("events-admin-placeholder-nav")
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=nav_html)
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Ticket type edit form
|
# Ticket type edit form
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
406
events/sxc/pages/__init__.py
Normal file
406
events/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
"""Events defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_events_pages() -> None:
|
||||||
|
"""Register events-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_events_layouts()
|
||||||
|
_register_events_helpers()
|
||||||
|
_load_events_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_events_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "events")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_events_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("events-calendar-admin", _cal_admin_full, _cal_admin_oob)
|
||||||
|
register_custom_layout("events-slots", _slots_full, _slots_oob)
|
||||||
|
register_custom_layout("events-slot", _slot_full, _slot_oob)
|
||||||
|
register_custom_layout("events-day-admin", _day_admin_full, _day_admin_oob)
|
||||||
|
register_custom_layout("events-entry", _entry_full, _entry_oob)
|
||||||
|
register_custom_layout("events-entry-admin", _entry_admin_full, _entry_admin_oob)
|
||||||
|
register_custom_layout("events-ticket-types", _ticket_types_full, _ticket_types_oob)
|
||||||
|
register_custom_layout("events-ticket-type", _ticket_type_full, _ticket_type_oob)
|
||||||
|
register_custom_layout("events-markets", _markets_full, _markets_oob)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) ---
|
||||||
|
|
||||||
|
async def _cal_admin_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _post_header_sx,
|
||||||
|
_calendar_header_sx, _calendar_admin_header_sx,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
post_hdr = _post_header_sx(ctx)
|
||||||
|
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
||||||
|
child = admin_hdr + _calendar_header_sx(ctx) + _calendar_admin_header_sx(ctx)
|
||||||
|
return root_hdr + post_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
async def _cal_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import post_admin_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _calendar_header_sx,
|
||||||
|
_calendar_admin_header_sx, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||||
|
+ _calendar_header_sx(ctx, oob=True))
|
||||||
|
oobs += oob_header_sx("calendar-header-child", "calendar-admin-header-child",
|
||||||
|
_calendar_admin_header_sx(ctx))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"post-admin-row", "post-admin-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"calendar-admin-row", "calendar-admin-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Slots layout (same full as cal-admin but different OOB) ---
|
||||||
|
|
||||||
|
async def _slots_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
return await _cal_admin_full({**ctx, "is_admin_section": True}, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
async def _slots_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import post_admin_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _calendar_admin_header_sx, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||||
|
+ _calendar_admin_header_sx(ctx, oob=True))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"post-admin-row", "post-admin-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"calendar-admin-row", "calendar-admin-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Slot detail layout (extends cal-admin with slot header) ---
|
||||||
|
|
||||||
|
async def _slot_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _post_header_sx,
|
||||||
|
_calendar_header_sx, _calendar_admin_header_sx, _slot_header_html,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
post_hdr = _post_header_sx(ctx)
|
||||||
|
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
||||||
|
child = (admin_hdr + _calendar_header_sx(ctx)
|
||||||
|
+ _calendar_admin_header_sx(ctx) + _slot_header_html(ctx))
|
||||||
|
return root_hdr + post_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
async def _slot_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import post_admin_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _calendar_admin_header_sx,
|
||||||
|
_slot_header_html, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav({**ctx, "is_admin_section": True})
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||||
|
+ _calendar_admin_header_sx(ctx, oob=True))
|
||||||
|
oobs += oob_header_sx("calendar-admin-header-child", "slot-header-child",
|
||||||
|
_slot_header_html(ctx))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"post-admin-row", "post-admin-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"calendar-admin-row", "calendar-admin-header-child",
|
||||||
|
"slot-row", "slot-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) ---
|
||||||
|
|
||||||
|
async def _day_admin_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _post_header_sx,
|
||||||
|
_calendar_header_sx, _day_header_sx, _day_admin_header_sx,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
post_hdr = _post_header_sx(ctx)
|
||||||
|
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
||||||
|
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
||||||
|
+ _day_admin_header_sx(ctx))
|
||||||
|
return root_hdr + post_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
async def _day_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import post_admin_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _calendar_header_sx,
|
||||||
|
_day_admin_header_sx, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||||
|
+ _calendar_header_sx(ctx, oob=True))
|
||||||
|
oobs += oob_header_sx("day-header-child", "day-admin-header-child",
|
||||||
|
_day_admin_header_sx(ctx))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"post-admin-row", "post-admin-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"day-row", "day-header-child",
|
||||||
|
"day-admin-row", "day-admin-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Entry layout (root + child(post + cal + day + entry), + menu) ---
|
||||||
|
|
||||||
|
def _entry_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_post_header_sx, _calendar_header_sx,
|
||||||
|
_day_header_sx, _entry_header_html,
|
||||||
|
)
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
|
||||||
|
+ _day_header_sx(ctx) + _entry_header_html(ctx))
|
||||||
|
return root_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_day_header_sx, _entry_header_html, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
oobs = _day_header_sx(ctx, oob=True)
|
||||||
|
oobs += oob_header_sx("day-header-child", "entry-header-child",
|
||||||
|
_entry_header_html(ctx))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"day-row", "day-header-child",
|
||||||
|
"entry-row", "entry-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) ---
|
||||||
|
|
||||||
|
async def _entry_admin_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _post_header_sx,
|
||||||
|
_calendar_header_sx, _day_header_sx,
|
||||||
|
_entry_header_html, _entry_admin_header_html,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
post_hdr = _post_header_sx(ctx)
|
||||||
|
admin_hdr = post_admin_header_sx(ctx, slug, selected="calendars")
|
||||||
|
child = (admin_hdr + _calendar_header_sx(ctx) + _day_header_sx(ctx)
|
||||||
|
+ _entry_header_html(ctx) + _entry_admin_header_html(ctx))
|
||||||
|
return root_hdr + post_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
async def _entry_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import post_admin_header_sx, oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ensure_container_nav, _entry_header_html,
|
||||||
|
_entry_admin_header_html, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
ctx = await _ensure_container_nav(ctx)
|
||||||
|
slug = (ctx.get("post") or {}).get("slug", "")
|
||||||
|
oobs = (post_admin_header_sx(ctx, slug, oob=True, selected="calendars")
|
||||||
|
+ _entry_header_html(ctx, oob=True))
|
||||||
|
oobs += oob_header_sx("entry-header-child", "entry-admin-header-child",
|
||||||
|
_entry_admin_header_html(ctx))
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"post-admin-row", "post-admin-header-child",
|
||||||
|
"calendar-row", "calendar-header-child",
|
||||||
|
"day-row", "day-header-child",
|
||||||
|
"entry-row", "entry-header-child",
|
||||||
|
"entry-admin-row", "entry-admin-header-child")
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Ticket types layout (extends entry admin with ticket-types header, + menu) ---
|
||||||
|
|
||||||
|
def _ticket_types_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_post_header_sx, _calendar_header_sx, _day_header_sx,
|
||||||
|
_entry_header_html, _entry_admin_header_html,
|
||||||
|
_ticket_types_header_html,
|
||||||
|
)
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
|
||||||
|
+ _day_header_sx(ctx) + _entry_header_html(ctx)
|
||||||
|
+ _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx))
|
||||||
|
return root_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _ticket_types_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_entry_admin_header_html, _ticket_types_header_html, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
oobs = _entry_admin_header_html(ctx, oob=True)
|
||||||
|
oobs += oob_header_sx("entry-admin-header-child", "ticket_types-header-child",
|
||||||
|
_ticket_types_header_html(ctx))
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) ---
|
||||||
|
|
||||||
|
def _ticket_type_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_post_header_sx, _calendar_header_sx, _day_header_sx,
|
||||||
|
_entry_header_html, _entry_admin_header_html,
|
||||||
|
_ticket_types_header_html, _ticket_type_header_html,
|
||||||
|
)
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = (_post_header_sx(ctx) + _calendar_header_sx(ctx)
|
||||||
|
+ _day_header_sx(ctx) + _entry_header_html(ctx)
|
||||||
|
+ _entry_admin_header_html(ctx) + _ticket_types_header_html(ctx)
|
||||||
|
+ _ticket_type_header_html(ctx))
|
||||||
|
return root_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _ticket_type_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_ticket_types_header_html, _ticket_type_header_html,
|
||||||
|
)
|
||||||
|
oobs = _ticket_types_header_html(ctx, oob=True)
|
||||||
|
oobs += oob_header_sx("ticket_types-header-child", "ticket_type-header-child",
|
||||||
|
_ticket_type_header_html(ctx))
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# --- Markets layout (root + child(post + markets)) ---
|
||||||
|
|
||||||
|
def _markets_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import _post_header_sx, _markets_header_sx
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = _post_header_sx(ctx) + _markets_header_sx(ctx)
|
||||||
|
return root_hdr + header_child_sx(child)
|
||||||
|
|
||||||
|
|
||||||
|
def _markets_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import _post_header_sx, _markets_header_sx
|
||||||
|
oobs = _post_header_sx(ctx, oob=True)
|
||||||
|
oobs += oob_header_sx("post-header-child", "markets-header-child",
|
||||||
|
_markets_header_sx(ctx))
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_calendar_admin_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "calendar_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_day_admin_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "day_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_slots_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "slots_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_slot_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "slot_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_entry_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "entry_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_entry_menu():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "entry_menu", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_entry_admin_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "entry_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_admin_menu():
|
||||||
|
from shared.sx.helpers import sx_call
|
||||||
|
return sx_call("events-admin-placeholder-nav")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_ticket_types_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "ticket_types_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_ticket_type_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "ticket_type_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_tickets_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "tickets_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_ticket_detail_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "ticket_detail_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_ticket_admin_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "ticket_admin_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_markets_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "markets_content", "")
|
||||||
89
events/sxc/pages/events.sx
Normal file
89
events/sxc/pages/events.sx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
;; Events pages — mounted on various nested blueprints
|
||||||
|
|
||||||
|
;; Calendar admin (mounted on calendar.admin bp)
|
||||||
|
(defpage calendar-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-calendar-admin
|
||||||
|
:content (calendar-admin-content))
|
||||||
|
|
||||||
|
;; Day admin (mounted on day.admin bp)
|
||||||
|
(defpage day-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-day-admin
|
||||||
|
:content (day-admin-content))
|
||||||
|
|
||||||
|
;; Slots listing (mounted on slots bp)
|
||||||
|
(defpage slots-listing
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :events-slots
|
||||||
|
:content (slots-content))
|
||||||
|
|
||||||
|
;; Slot detail (mounted on slot bp)
|
||||||
|
(defpage slot-detail
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-slot
|
||||||
|
:content (slot-content))
|
||||||
|
|
||||||
|
;; Entry detail (mounted on calendar_entry bp)
|
||||||
|
(defpage entry-detail
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-entry
|
||||||
|
:content (entry-content)
|
||||||
|
:menu (entry-menu))
|
||||||
|
|
||||||
|
;; Entry admin (mounted on calendar_entry.admin bp)
|
||||||
|
(defpage entry-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-entry-admin
|
||||||
|
:content (entry-admin-content)
|
||||||
|
:menu (admin-menu))
|
||||||
|
|
||||||
|
;; Ticket types listing (mounted on ticket_types bp)
|
||||||
|
(defpage ticket-types-listing
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :events-ticket-types
|
||||||
|
:content (ticket-types-content)
|
||||||
|
:menu (admin-menu))
|
||||||
|
|
||||||
|
;; Ticket type detail (mounted on ticket_type bp)
|
||||||
|
(defpage ticket-type-detail
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :events-ticket-type
|
||||||
|
:content (ticket-type-content)
|
||||||
|
:menu (admin-menu))
|
||||||
|
|
||||||
|
;; My tickets (mounted on tickets bp)
|
||||||
|
(defpage my-tickets
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :root
|
||||||
|
:content (tickets-content))
|
||||||
|
|
||||||
|
;; Ticket detail (mounted on tickets bp)
|
||||||
|
(defpage ticket-detail
|
||||||
|
:path "/<code>/"
|
||||||
|
:auth :public
|
||||||
|
:layout :root
|
||||||
|
:content (ticket-detail-content))
|
||||||
|
|
||||||
|
;; Ticket admin dashboard (mounted on ticket_admin bp)
|
||||||
|
(defpage ticket-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout :root
|
||||||
|
:content (ticket-admin-content))
|
||||||
|
|
||||||
|
;; Markets (mounted on markets bp)
|
||||||
|
(defpage events-markets
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :events-markets
|
||||||
|
:content (markets-content))
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- Desktop nav -->
|
<!-- Desktop nav -->
|
||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% call links.link(
|
{% call links.link(
|
||||||
url_for('calendar.slots.get', calendar_slug=calendar.slug),
|
url_for('calendar.slots.defpage_slots_listing', calendar_slug=calendar.slug),
|
||||||
hx_select_search,
|
hx_select_search,
|
||||||
select_colours,
|
select_colours,
|
||||||
True,
|
True,
|
||||||
@@ -14,5 +14,5 @@
|
|||||||
{% endcall %}
|
{% endcall %}
|
||||||
{% if g.rights.admin %}
|
{% if g.rights.admin %}
|
||||||
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
{% from 'macros/admin_nav.html' import admin_nav_item %}
|
||||||
{{ admin_nav_item(url_for('calendar.admin.admin', calendar_slug=calendar.slug)) }}
|
{{ admin_nav_item(url_for('calendar.admin.defpage_calendar_admin', calendar_slug=calendar.slug)) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<div class="text-xs font-medium">
|
<div class="text-xs font-medium">
|
||||||
{% call links.link(
|
{% call links.link(
|
||||||
url_for(
|
url_for(
|
||||||
'calendar.slots.slot.get',
|
'calendar.slots.slot.defpage_slot_detail',
|
||||||
calendar_slug=calendar.slug,
|
calendar_slug=calendar.slug,
|
||||||
slot_id=entry.slot.id
|
slot_id=entry.slot.id
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% import 'macros/links.html' as links %}
|
{% import 'macros/links.html' as links %}
|
||||||
{% macro header_row(oob=False) %}
|
{% macro header_row(oob=False) %}
|
||||||
{% call links.menu_row(id='markets-row', oob=oob) %}
|
{% call links.menu_row(id='markets-row', oob=oob) %}
|
||||||
{% call links.link(url_for('markets.home'), hx_select_search) %}
|
{% call links.link(url_for('markets.defpage_events_markets'), hx_select_search) %}
|
||||||
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
|
<i class="fa fa-shopping-bag" aria-hidden="true"></i>
|
||||||
<div>
|
<div>
|
||||||
Markets
|
Markets
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
class="relative inline-flex items-center justify-center text-emerald-700"
|
class="relative inline-flex items-center justify-center text-emerald-700"
|
||||||
href="{{ url_for('tickets.my_tickets') }}"
|
href="{{ url_for('tickets.defpage_my_tickets') }}"
|
||||||
>
|
>
|
||||||
<span class="relative inline-flex items-center justify-center">
|
<span class="relative inline-flex items-center justify-center">
|
||||||
<i class="fa-solid fa-shopping-cart text-2xl" aria-hidden="true"></i>
|
<i class="fa-solid fa-shopping-cart text-2xl" aria-hidden="true"></i>
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
class="relative inline-flex items-center justify-center text-emerald-700"
|
class="relative inline-flex items-center justify-center text-emerald-700"
|
||||||
href="{{ url_for('tickets.my_tickets') }}"
|
href="{{ url_for('tickets.defpage_my_tickets') }}"
|
||||||
>
|
>
|
||||||
<span class="relative inline-flex items-center justify-center">
|
<span class="relative inline-flex items-center justify-center">
|
||||||
<i class="fa-solid fa-shopping-cart text-2xl" aria-hidden="true"></i>
|
<i class="fa-solid fa-shopping-cart text-2xl" aria-hidden="true"></i>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="space-y-2 mb-4">
|
<div class="space-y-2 mb-4">
|
||||||
{% for ticket in created_tickets %}
|
{% for ticket in created_tickets %}
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('tickets.ticket_detail', code=ticket.code) }}"
|
href="{{ url_for('tickets.defpage_ticket_detail', code=ticket.code) }}"
|
||||||
class="flex items-center justify-between p-2 rounded-lg bg-white border border-emerald-100 hover:border-emerald-300 transition text-sm"
|
class="flex items-center justify-between p-2 rounded-lg bg-white border border-emerald-100 hover:border-emerald-300 transition text-sm"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<div class="mt-3 flex gap-2">
|
<div class="mt-3 flex gap-2">
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('tickets.my_tickets') }}"
|
href="{{ url_for('tickets.defpage_my_tickets') }}"
|
||||||
class="text-sm text-emerald-700 hover:text-emerald-900 underline"
|
class="text-sm text-emerald-700 hover:text-emerald-900 underline"
|
||||||
>
|
>
|
||||||
View all my tickets
|
View all my tickets
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<section id="ticket-detail" class="{{styles.list_container}} max-w-lg mx-auto">
|
<section id="ticket-detail" class="{{styles.list_container}} max-w-lg mx-auto">
|
||||||
|
|
||||||
{# Back link #}
|
{# Back link #}
|
||||||
<a href="{{ url_for('tickets.my_tickets') }}"
|
<a href="{{ url_for('tickets.defpage_my_tickets') }}"
|
||||||
class="inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4">
|
class="inline-flex items-center gap-1 text-sm text-stone-500 hover:text-stone-700 mb-4">
|
||||||
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
<i class="fa fa-arrow-left" aria-hidden="true"></i>
|
||||||
Back to my tickets
|
Back to my tickets
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{% for ticket in tickets %}
|
{% for ticket in tickets %}
|
||||||
<a
|
<a
|
||||||
href="{{ url_for('tickets.ticket_detail', code=ticket.code) }}"
|
href="{{ url_for('tickets.defpage_ticket_detail', code=ticket.code) }}"
|
||||||
class="block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
|
class="block rounded-xl border border-stone-200 bg-white p-4 hover:shadow-md transition"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
|||||||
@@ -84,11 +84,20 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# --- defpage setup ---
|
||||||
|
from sxc.pages import setup_federation_pages
|
||||||
|
setup_federation_pages()
|
||||||
|
|
||||||
# --- blueprints ---
|
# --- blueprints ---
|
||||||
# Well-known + actors (webfinger, inbox, outbox, etc.) are now handled
|
# Well-known + actors (webfinger, inbox, outbox, etc.) are now handled
|
||||||
# by the shared AP blueprint registered in create_base_app().
|
# by the shared AP blueprint registered in create_base_app().
|
||||||
app.register_blueprint(register_identity_bp())
|
app.register_blueprint(register_identity_bp())
|
||||||
app.register_blueprint(register_social_bp())
|
|
||||||
|
social_bp = register_social_bp()
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(social_bp, "federation")
|
||||||
|
app.register_blueprint(social_bp)
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
|
||||||
# --- home page ---
|
# --- home page ---
|
||||||
|
|||||||
@@ -32,18 +32,103 @@ def register(url_prefix="/social"):
|
|||||||
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
||||||
g._social_actor = actor
|
g._social_actor = actor
|
||||||
|
|
||||||
# -- Timeline -------------------------------------------------------------
|
@bp.before_request
|
||||||
|
async def _prepare_page_data():
|
||||||
|
"""Pre-render content for defpage routes."""
|
||||||
|
endpoint = request.endpoint or ""
|
||||||
|
|
||||||
@bp.get("/")
|
if endpoint.endswith("defpage_home_timeline"):
|
||||||
async def home_timeline():
|
actor = _require_actor()
|
||||||
if not g.get("user"):
|
items = await services.federation.get_home_timeline(g.s, actor.id)
|
||||||
return redirect(url_for("auth.login_form"))
|
from sx.sx_components import _timeline_content_sx
|
||||||
actor = _require_actor()
|
g.home_timeline_content = _timeline_content_sx(items, "home", actor)
|
||||||
items = await services.federation.get_home_timeline(g.s, actor.id)
|
|
||||||
from shared.sx.page import get_template_context
|
elif endpoint.endswith("defpage_public_timeline"):
|
||||||
from sx.sx_components import render_timeline_page
|
actor = getattr(g, "_social_actor", None)
|
||||||
ctx = await get_template_context()
|
items = await services.federation.get_public_timeline(g.s)
|
||||||
return await render_timeline_page(ctx, items, "home", actor)
|
from sx.sx_components import _timeline_content_sx
|
||||||
|
g.public_timeline_content = _timeline_content_sx(items, "public", actor)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_compose_form"):
|
||||||
|
actor = _require_actor()
|
||||||
|
from sx.sx_components import _compose_content_sx
|
||||||
|
reply_to = request.args.get("reply_to")
|
||||||
|
g.compose_content = _compose_content_sx(actor, reply_to)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_search"):
|
||||||
|
actor = getattr(g, "_social_actor", None)
|
||||||
|
query = request.args.get("q", "").strip()
|
||||||
|
actors_list = []
|
||||||
|
total = 0
|
||||||
|
followed_urls: set[str] = set()
|
||||||
|
if query:
|
||||||
|
actors_list, total = await services.federation.search_actors(g.s, query)
|
||||||
|
if actor:
|
||||||
|
following, _ = await services.federation.get_following(
|
||||||
|
g.s, actor.preferred_username, page=1, per_page=1000,
|
||||||
|
)
|
||||||
|
followed_urls = {a.actor_url for a in following}
|
||||||
|
from sx.sx_components import _search_content_sx
|
||||||
|
g.search_content = _search_content_sx(query, actors_list, total, 1, followed_urls, actor)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_following_list"):
|
||||||
|
actor = _require_actor()
|
||||||
|
actors_list, total = await services.federation.get_following(
|
||||||
|
g.s, actor.preferred_username,
|
||||||
|
)
|
||||||
|
from sx.sx_components import _following_content_sx
|
||||||
|
g.following_content = _following_content_sx(actors_list, total, actor)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_followers_list"):
|
||||||
|
actor = _require_actor()
|
||||||
|
actors_list, total = await services.federation.get_followers_paginated(
|
||||||
|
g.s, actor.preferred_username,
|
||||||
|
)
|
||||||
|
following, _ = await services.federation.get_following(
|
||||||
|
g.s, actor.preferred_username, page=1, per_page=1000,
|
||||||
|
)
|
||||||
|
followed_urls = {a.actor_url for a in following}
|
||||||
|
from sx.sx_components import _followers_content_sx
|
||||||
|
g.followers_content = _followers_content_sx(actors_list, total, followed_urls, actor)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_actor_timeline"):
|
||||||
|
actor = getattr(g, "_social_actor", None)
|
||||||
|
actor_id = request.view_args.get("id")
|
||||||
|
from shared.models.federation import RemoteActor
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
remote = (
|
||||||
|
await g.s.execute(
|
||||||
|
sa_select(RemoteActor).where(RemoteActor.id == actor_id)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if not remote:
|
||||||
|
abort(404)
|
||||||
|
from shared.services.federation_impl import _remote_actor_to_dto
|
||||||
|
remote_dto = _remote_actor_to_dto(remote)
|
||||||
|
items = await services.federation.get_actor_timeline(g.s, actor_id)
|
||||||
|
is_following = False
|
||||||
|
if actor:
|
||||||
|
from shared.models.federation import APFollowing
|
||||||
|
existing = (
|
||||||
|
await g.s.execute(
|
||||||
|
sa_select(APFollowing).where(
|
||||||
|
APFollowing.actor_profile_id == actor.id,
|
||||||
|
APFollowing.remote_actor_id == actor_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
is_following = existing is not None
|
||||||
|
from sx.sx_components import _actor_timeline_content_sx
|
||||||
|
g.actor_timeline_content = _actor_timeline_content_sx(remote_dto, items, is_following, actor)
|
||||||
|
|
||||||
|
elif endpoint.endswith("defpage_notifications"):
|
||||||
|
actor = _require_actor()
|
||||||
|
items = await services.federation.get_notifications(g.s, actor.id)
|
||||||
|
await services.federation.mark_notifications_read(g.s, actor.id)
|
||||||
|
from sx.sx_components import _notifications_content_sx
|
||||||
|
g.notifications_content = _notifications_content_sx(items)
|
||||||
|
|
||||||
|
# -- Timeline pagination ---------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/timeline")
|
@bp.get("/timeline")
|
||||||
async def home_timeline_page():
|
async def home_timeline_page():
|
||||||
@@ -62,15 +147,6 @@ def register(url_prefix="/social"):
|
|||||||
sx_src = await render_timeline_items(items, "home", actor)
|
sx_src = await render_timeline_items(items, "home", actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
@bp.get("/public")
|
|
||||||
async def public_timeline():
|
|
||||||
items = await services.federation.get_public_timeline(g.s)
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_timeline_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_timeline_page(ctx, items, "public", actor)
|
|
||||||
|
|
||||||
@bp.get("/public/timeline")
|
@bp.get("/public/timeline")
|
||||||
async def public_timeline_page():
|
async def public_timeline_page():
|
||||||
before_str = request.args.get("before")
|
before_str = request.args.get("before")
|
||||||
@@ -86,16 +162,7 @@ def register(url_prefix="/social"):
|
|||||||
sx_src = await render_timeline_items(items, "public", actor)
|
sx_src = await render_timeline_items(items, "public", actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
# -- Compose --------------------------------------------------------------
|
# -- Compose ---------------------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/compose")
|
|
||||||
async def compose_form():
|
|
||||||
actor = _require_actor()
|
|
||||||
reply_to = request.args.get("reply_to")
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_compose_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_compose_page(ctx, actor, reply_to)
|
|
||||||
|
|
||||||
@bp.post("/compose")
|
@bp.post("/compose")
|
||||||
async def compose_submit():
|
async def compose_submit():
|
||||||
@@ -103,7 +170,7 @@ def register(url_prefix="/social"):
|
|||||||
form = await request.form
|
form = await request.form
|
||||||
content = form.get("content", "").strip()
|
content = form.get("content", "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
return redirect(url_for("social.compose_form"))
|
return redirect(url_for("social.defpage_compose_form"))
|
||||||
|
|
||||||
visibility = form.get("visibility", "public")
|
visibility = form.get("visibility", "public")
|
||||||
in_reply_to = form.get("in_reply_to") or None
|
in_reply_to = form.get("in_reply_to") or None
|
||||||
@@ -114,45 +181,26 @@ def register(url_prefix="/social"):
|
|||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
in_reply_to=in_reply_to,
|
in_reply_to=in_reply_to,
|
||||||
)
|
)
|
||||||
return redirect(url_for("social.home_timeline"))
|
return redirect(url_for("social.defpage_home_timeline"))
|
||||||
|
|
||||||
@bp.post("/delete/<int:post_id>")
|
@bp.post("/delete/<int:post_id>")
|
||||||
async def delete_post(post_id: int):
|
async def delete_post(post_id: int):
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
await services.federation.delete_local_post(g.s, actor.id, post_id)
|
await services.federation.delete_local_post(g.s, actor.id, post_id)
|
||||||
return redirect(url_for("social.home_timeline"))
|
return redirect(url_for("social.defpage_home_timeline"))
|
||||||
|
|
||||||
# -- Search + Follow ------------------------------------------------------
|
# -- Search + Follow -------------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/search")
|
|
||||||
async def search():
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
query = request.args.get("q", "").strip()
|
|
||||||
actors = []
|
|
||||||
total = 0
|
|
||||||
followed_urls: set[str] = set()
|
|
||||||
if query:
|
|
||||||
actors, total = await services.federation.search_actors(g.s, query)
|
|
||||||
if actor:
|
|
||||||
following, _ = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username, page=1, per_page=1000,
|
|
||||||
)
|
|
||||||
followed_urls = {a.actor_url for a in following}
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_search_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor)
|
|
||||||
|
|
||||||
@bp.get("/search/page")
|
@bp.get("/search/page")
|
||||||
async def search_page():
|
async def search_page():
|
||||||
actor = getattr(g, "_social_actor", None)
|
actor = getattr(g, "_social_actor", None)
|
||||||
query = request.args.get("q", "").strip()
|
query = request.args.get("q", "").strip()
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
actors = []
|
actors_list = []
|
||||||
total = 0
|
total = 0
|
||||||
followed_urls: set[str] = set()
|
followed_urls: set[str] = set()
|
||||||
if query:
|
if query:
|
||||||
actors, total = await services.federation.search_actors(
|
actors_list, total = await services.federation.search_actors(
|
||||||
g.s, query, page=page,
|
g.s, query, page=page,
|
||||||
)
|
)
|
||||||
if actor:
|
if actor:
|
||||||
@@ -161,7 +209,7 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
followed_urls = {a.actor_url for a in following}
|
followed_urls = {a.actor_url for a in following}
|
||||||
from sx.sx_components import render_search_results
|
from sx.sx_components import render_search_results
|
||||||
sx_src = await render_search_results(actors, query, page, followed_urls, actor)
|
sx_src = await render_search_results(actors_list, query, page, followed_urls, actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
@bp.post("/follow")
|
@bp.post("/follow")
|
||||||
@@ -175,7 +223,7 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
||||||
return await _actor_card_response(actor, remote_actor_url, is_followed=True)
|
return await _actor_card_response(actor, remote_actor_url, is_followed=True)
|
||||||
return redirect(request.referrer or url_for("social.search"))
|
return redirect(request.referrer or url_for("social.defpage_search"))
|
||||||
|
|
||||||
@bp.post("/unfollow")
|
@bp.post("/unfollow")
|
||||||
async def unfollow():
|
async def unfollow():
|
||||||
@@ -188,7 +236,7 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
||||||
return await _actor_card_response(actor, remote_actor_url, is_followed=False)
|
return await _actor_card_response(actor, remote_actor_url, is_followed=False)
|
||||||
return redirect(request.referrer or url_for("social.search"))
|
return redirect(request.referrer or url_for("social.defpage_search"))
|
||||||
|
|
||||||
async def _actor_card_response(actor, remote_actor_url, is_followed):
|
async def _actor_card_response(actor, remote_actor_url, is_followed):
|
||||||
"""Re-render a single actor card after follow/unfollow via HTMX."""
|
"""Re-render a single actor card after follow/unfollow via HTMX."""
|
||||||
@@ -198,7 +246,6 @@ def register(url_prefix="/social"):
|
|||||||
if not remote_dto:
|
if not remote_dto:
|
||||||
return Response("", status=200)
|
return Response("", status=200)
|
||||||
followed_urls = {remote_actor_url} if is_followed else set()
|
followed_urls = {remote_actor_url} if is_followed else set()
|
||||||
# Detect list context from referer
|
|
||||||
referer = request.referrer or ""
|
referer = request.referrer or ""
|
||||||
if "/followers" in referer:
|
if "/followers" in referer:
|
||||||
list_type = "followers"
|
list_type = "followers"
|
||||||
@@ -207,7 +254,7 @@ def register(url_prefix="/social"):
|
|||||||
from sx.sx_components import render_actor_card
|
from sx.sx_components import render_actor_card
|
||||||
return sx_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
|
return sx_response(render_actor_card(remote_dto, actor, followed_urls, list_type=list_type))
|
||||||
|
|
||||||
# -- Interactions ---------------------------------------------------------
|
# -- Interactions ----------------------------------------------------------
|
||||||
|
|
||||||
@bp.post("/like")
|
@bp.post("/like")
|
||||||
async def like():
|
async def like():
|
||||||
@@ -216,7 +263,6 @@ def register(url_prefix="/social"):
|
|||||||
object_id = form.get("object_id", "")
|
object_id = form.get("object_id", "")
|
||||||
author_inbox = form.get("author_inbox", "")
|
author_inbox = form.get("author_inbox", "")
|
||||||
await services.federation.like_post(g.s, actor.id, object_id, author_inbox)
|
await services.federation.like_post(g.s, actor.id, object_id, author_inbox)
|
||||||
# Return updated buttons for HTMX
|
|
||||||
return await _interaction_buttons_response(actor, object_id, author_inbox)
|
return await _interaction_buttons_response(actor, object_id, author_inbox)
|
||||||
|
|
||||||
@bp.post("/unlike")
|
@bp.post("/unlike")
|
||||||
@@ -250,7 +296,6 @@ def register(url_prefix="/social"):
|
|||||||
"""Re-render interaction buttons after a like/boost action."""
|
"""Re-render interaction buttons after a like/boost action."""
|
||||||
from shared.models.federation import APInteraction, APRemotePost, APActivity
|
from shared.models.federation import APInteraction, APRemotePost, APActivity
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from shared.services.federation_impl import SqlFederationService
|
|
||||||
|
|
||||||
svc = services.federation
|
svc = services.federation
|
||||||
post_type, post_id = await svc._resolve_post(g.s, object_id)
|
post_type, post_id = await svc._resolve_post(g.s, object_id)
|
||||||
@@ -304,51 +349,24 @@ def register(url_prefix="/social"):
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
))
|
))
|
||||||
|
|
||||||
# -- Following / Followers ------------------------------------------------
|
# -- Following / Followers pagination --------------------------------------
|
||||||
|
|
||||||
@bp.get("/following")
|
|
||||||
async def following_list():
|
|
||||||
actor = _require_actor()
|
|
||||||
actors, total = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username,
|
|
||||||
)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_following_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_following_page(ctx, actors, total, actor)
|
|
||||||
|
|
||||||
@bp.get("/following/page")
|
@bp.get("/following/page")
|
||||||
async def following_list_page():
|
async def following_list_page():
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
actors, total = await services.federation.get_following(
|
actors_list, total = await services.federation.get_following(
|
||||||
g.s, actor.preferred_username, page=page,
|
g.s, actor.preferred_username, page=page,
|
||||||
)
|
)
|
||||||
from sx.sx_components import render_following_items
|
from sx.sx_components import render_following_items
|
||||||
sx_src = await render_following_items(actors, page, actor)
|
sx_src = await render_following_items(actors_list, page, actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
@bp.get("/followers")
|
|
||||||
async def followers_list():
|
|
||||||
actor = _require_actor()
|
|
||||||
actors, total = await services.federation.get_followers_paginated(
|
|
||||||
g.s, actor.preferred_username,
|
|
||||||
)
|
|
||||||
# Build set of followed actor URLs to show Follow Back vs Unfollow
|
|
||||||
following, _ = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username, page=1, per_page=1000,
|
|
||||||
)
|
|
||||||
followed_urls = {a.actor_url for a in following}
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_followers_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_followers_page(ctx, actors, total, followed_urls, actor)
|
|
||||||
|
|
||||||
@bp.get("/followers/page")
|
@bp.get("/followers/page")
|
||||||
async def followers_list_page():
|
async def followers_list_page():
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
page = request.args.get("page", 1, type=int)
|
page = request.args.get("page", 1, type=int)
|
||||||
actors, total = await services.federation.get_followers_paginated(
|
actors_list, total = await services.federation.get_followers_paginated(
|
||||||
g.s, actor.preferred_username, page=page,
|
g.s, actor.preferred_username, page=page,
|
||||||
)
|
)
|
||||||
following, _ = await services.federation.get_following(
|
following, _ = await services.federation.get_following(
|
||||||
@@ -356,43 +374,9 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
followed_urls = {a.actor_url for a in following}
|
followed_urls = {a.actor_url for a in following}
|
||||||
from sx.sx_components import render_followers_items
|
from sx.sx_components import render_followers_items
|
||||||
sx_src = await render_followers_items(actors, page, followed_urls, actor)
|
sx_src = await render_followers_items(actors_list, page, followed_urls, actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
@bp.get("/actor/<int:id>")
|
|
||||||
async def actor_timeline(id: int):
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
# Get remote actor info
|
|
||||||
from shared.models.federation import RemoteActor
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
remote = (
|
|
||||||
await g.s.execute(
|
|
||||||
sa_select(RemoteActor).where(RemoteActor.id == id)
|
|
||||||
)
|
|
||||||
).scalar_one_or_none()
|
|
||||||
if not remote:
|
|
||||||
abort(404)
|
|
||||||
from shared.services.federation_impl import _remote_actor_to_dto
|
|
||||||
remote_dto = _remote_actor_to_dto(remote)
|
|
||||||
items = await services.federation.get_actor_timeline(g.s, id)
|
|
||||||
# Check if we follow this actor
|
|
||||||
is_following = False
|
|
||||||
if actor:
|
|
||||||
from shared.models.federation import APFollowing
|
|
||||||
existing = (
|
|
||||||
await g.s.execute(
|
|
||||||
sa_select(APFollowing).where(
|
|
||||||
APFollowing.actor_profile_id == actor.id,
|
|
||||||
APFollowing.remote_actor_id == id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).scalar_one_or_none()
|
|
||||||
is_following = existing is not None
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_actor_timeline_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_actor_timeline_page(ctx, remote_dto, items, is_following, actor)
|
|
||||||
|
|
||||||
@bp.get("/actor/<int:id>/timeline")
|
@bp.get("/actor/<int:id>/timeline")
|
||||||
async def actor_timeline_page(id: int):
|
async def actor_timeline_page(id: int):
|
||||||
actor = getattr(g, "_social_actor", None)
|
actor = getattr(g, "_social_actor", None)
|
||||||
@@ -410,17 +394,7 @@ def register(url_prefix="/social"):
|
|||||||
sx_src = await render_actor_timeline_items(items, id, actor)
|
sx_src = await render_actor_timeline_items(items, id, actor)
|
||||||
return sx_response(sx_src)
|
return sx_response(sx_src)
|
||||||
|
|
||||||
# -- Notifications --------------------------------------------------------
|
# -- Notifications ---------------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/notifications")
|
|
||||||
async def notifications():
|
|
||||||
actor = _require_actor()
|
|
||||||
items = await services.federation.get_notifications(g.s, actor.id)
|
|
||||||
await services.federation.mark_notifications_read(g.s, actor.id)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_notifications_page
|
|
||||||
ctx = await get_template_context()
|
|
||||||
return await render_notifications_page(ctx, items, actor)
|
|
||||||
|
|
||||||
@bp.get("/notifications/count")
|
@bp.get("/notifications/count")
|
||||||
async def notification_count():
|
async def notification_count():
|
||||||
@@ -440,6 +414,6 @@ def register(url_prefix="/social"):
|
|||||||
async def mark_read():
|
async def mark_read():
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
await services.federation.mark_notifications_read(g.s, actor.id)
|
await services.federation.mark_notifications_read(g.s, actor.id)
|
||||||
return redirect(url_for("social.notifications"))
|
return redirect(url_for("social.defpage_notifications"))
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -35,12 +35,12 @@ def _social_nav_sx(actor: Any) -> str:
|
|||||||
return sx_call("federation-nav-choose-username", url=choose_url)
|
return sx_call("federation-nav-choose-username", url=choose_url)
|
||||||
|
|
||||||
links = [
|
links = [
|
||||||
("social.home_timeline", "Timeline"),
|
("social.defpage_home_timeline", "Timeline"),
|
||||||
("social.public_timeline", "Public"),
|
("social.defpage_public_timeline", "Public"),
|
||||||
("social.compose_form", "Compose"),
|
("social.defpage_compose_form", "Compose"),
|
||||||
("social.following_list", "Following"),
|
("social.defpage_following_list", "Following"),
|
||||||
("social.followers_list", "Followers"),
|
("social.defpage_followers_list", "Followers"),
|
||||||
("social.search", "Search"),
|
("social.defpage_search", "Search"),
|
||||||
]
|
]
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
@@ -51,7 +51,7 @@ def _social_nav_sx(actor: Any) -> str:
|
|||||||
parts.append(f'(a :href {serialize(href)} :class {serialize(cls)} {serialize(label)})')
|
parts.append(f'(a :href {serialize(href)} :class {serialize(cls)} {serialize(label)})')
|
||||||
|
|
||||||
# Notifications with live badge
|
# Notifications with live badge
|
||||||
notif_url = url_for("social.notifications")
|
notif_url = url_for("social.defpage_notifications")
|
||||||
notif_count_url = url_for("social.notification_count")
|
notif_count_url = url_for("social.notification_count")
|
||||||
notif_bold = " font-bold" if request.path == notif_url else ""
|
notif_bold = " font-bold" if request.path == notif_url else ""
|
||||||
parts.append(sx_call(
|
parts.append(sx_call(
|
||||||
@@ -122,7 +122,7 @@ def _interaction_buttons_sx(item: Any, actor: Any) -> str:
|
|||||||
boost_action = url_for("social.boost")
|
boost_action = url_for("social.boost")
|
||||||
boost_cls = "hover:text-green-600"
|
boost_cls = "hover:text-green-600"
|
||||||
|
|
||||||
reply_url = url_for("social.compose_form", reply_to=oid) if oid else ""
|
reply_url = url_for("social.defpage_compose_form", reply_to=oid) if oid else ""
|
||||||
reply_sx = sx_call("federation-reply-link", url=reply_url) if reply_url else ""
|
reply_sx = sx_call("federation-reply-link", url=reply_url) if reply_url else ""
|
||||||
|
|
||||||
like_form = sx_call(
|
like_form = sx_call(
|
||||||
@@ -260,7 +260,7 @@ def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
|
|||||||
if (list_type in ("following", "search")) and aid:
|
if (list_type in ("following", "search")) and aid:
|
||||||
name_sx = sx_call(
|
name_sx = sx_call(
|
||||||
"federation-actor-name-link",
|
"federation-actor-name-link",
|
||||||
href=url_for("social.actor_timeline", id=aid),
|
href=url_for("social.defpage_actor_timeline", id=aid),
|
||||||
name=str(escape(display_name)),
|
name=str(escape(display_name)),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -436,32 +436,28 @@ async def render_check_email_page(ctx: dict) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public API: Timeline
|
# Content builders (used by defpage before_request)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_timeline_page(ctx: dict, items: list, timeline_type: str,
|
def _timeline_content_sx(items: list, timeline_type: str, actor: Any) -> str:
|
||||||
actor: Any) -> str:
|
"""Build timeline content SX string."""
|
||||||
"""Full page: timeline (home or public)."""
|
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
|
|
||||||
label = "Home" if timeline_type == "home" else "Public"
|
label = "Home" if timeline_type == "home" else "Public"
|
||||||
compose_sx = ""
|
compose_sx = ""
|
||||||
if actor:
|
if actor:
|
||||||
compose_url = url_for("social.compose_form")
|
compose_url = url_for("social.defpage_compose_form")
|
||||||
compose_sx = sx_call("federation-compose-button", url=compose_url)
|
compose_sx = sx_call("federation-compose-button", url=compose_url)
|
||||||
|
|
||||||
timeline_sx = _timeline_items_sx(items, timeline_type, actor)
|
timeline_sx = _timeline_items_sx(items, timeline_type, actor)
|
||||||
|
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-timeline-page",
|
"federation-timeline-page",
|
||||||
label=label,
|
label=label,
|
||||||
compose=SxExpr(compose_sx) if compose_sx else None,
|
compose=SxExpr(compose_sx) if compose_sx else None,
|
||||||
timeline=SxExpr(timeline_sx) if timeline_sx else None,
|
timeline=SxExpr(timeline_sx) if timeline_sx else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title=f"{label} Timeline \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
async def render_timeline_items(items: list, timeline_type: str,
|
async def render_timeline_items(items: list, timeline_type: str,
|
||||||
actor: Any, actor_id: int | None = None) -> str:
|
actor: Any, actor_id: int | None = None) -> str:
|
||||||
@@ -469,12 +465,8 @@ async def render_timeline_items(items: list, timeline_type: str,
|
|||||||
return _timeline_items_sx(items, timeline_type, actor, actor_id)
|
return _timeline_items_sx(items, timeline_type, actor, actor_id)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def _compose_content_sx(actor: Any, reply_to: str | None) -> str:
|
||||||
# Public API: Compose
|
"""Build compose form content SX string."""
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_compose_page(ctx: dict, actor: Any, reply_to: str | None) -> str:
|
|
||||||
"""Full page: compose form."""
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
from shared.browser.app.csrf import generate_csrf_token
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
|
|
||||||
@@ -488,26 +480,19 @@ async def render_compose_page(ctx: dict, actor: Any, reply_to: str | None) -> st
|
|||||||
reply_to=str(escape(reply_to)),
|
reply_to=str(escape(reply_to)),
|
||||||
)
|
)
|
||||||
|
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-compose-form",
|
"federation-compose-form",
|
||||||
action=action, csrf=csrf,
|
action=action, csrf=csrf,
|
||||||
reply=SxExpr(reply_sx) if reply_sx else None,
|
reply=SxExpr(reply_sx) if reply_sx else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title="Compose \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
def _search_content_sx(query: str, actors: list, total: int,
|
||||||
# ---------------------------------------------------------------------------
|
page: int, followed_urls: set, actor: Any) -> str:
|
||||||
# Public API: Search
|
"""Build search page content SX string."""
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_search_page(ctx: dict, query: str, actors: list, total: int,
|
|
||||||
page: int, followed_urls: set, actor: Any) -> str:
|
|
||||||
"""Full page: search."""
|
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
|
|
||||||
search_url = url_for("social.search")
|
search_url = url_for("social.defpage_search")
|
||||||
search_page_url = url_for("social.search_page")
|
search_page_url = url_for("social.search_page")
|
||||||
|
|
||||||
results_sx = _search_results_sx(actors, query, page, followed_urls, actor)
|
results_sx = _search_results_sx(actors, query, page, followed_urls, actor)
|
||||||
@@ -527,7 +512,7 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int,
|
|||||||
text=f"No results found for <strong>{escape(query)}</strong>",
|
text=f"No results found for <strong>{escape(query)}</strong>",
|
||||||
)
|
)
|
||||||
|
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-search-page",
|
"federation-search-page",
|
||||||
search_url=search_url, search_page_url=search_page_url,
|
search_url=search_url, search_page_url=search_page_url,
|
||||||
query=str(escape(query)),
|
query=str(escape(query)),
|
||||||
@@ -535,9 +520,6 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int,
|
|||||||
results=SxExpr(results_sx) if results_sx else None,
|
results=SxExpr(results_sx) if results_sx else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title="Search \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
async def render_search_results(actors: list, query: str, page: int,
|
async def render_search_results(actors: list, query: str, page: int,
|
||||||
followed_urls: set, actor: Any) -> str:
|
followed_urls: set, actor: Any) -> str:
|
||||||
@@ -545,21 +527,14 @@ async def render_search_results(actors: list, query: str, page: int,
|
|||||||
return _search_results_sx(actors, query, page, followed_urls, actor)
|
return _search_results_sx(actors, query, page, followed_urls, actor)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def _following_content_sx(actors: list, total: int, actor: Any) -> str:
|
||||||
# Public API: Following / Followers
|
"""Build following list content SX string."""
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_following_page(ctx: dict, actors: list, total: int,
|
|
||||||
actor: Any) -> str:
|
|
||||||
"""Full page: following list."""
|
|
||||||
items_sx = _actor_list_items_sx(actors, 1, "following", set(), actor)
|
items_sx = _actor_list_items_sx(actors, 1, "following", set(), actor)
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-actor-list-page",
|
"federation-actor-list-page",
|
||||||
title="Following", count_str=f"({total})",
|
title="Following", count_str=f"({total})",
|
||||||
items=SxExpr(items_sx) if items_sx else None,
|
items=SxExpr(items_sx) if items_sx else None,
|
||||||
)
|
)
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title="Following \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
async def render_following_items(actors: list, page: int, actor: Any) -> str:
|
async def render_following_items(actors: list, page: int, actor: Any) -> str:
|
||||||
@@ -567,17 +542,15 @@ async def render_following_items(actors: list, page: int, actor: Any) -> str:
|
|||||||
return _actor_list_items_sx(actors, page, "following", set(), actor)
|
return _actor_list_items_sx(actors, page, "following", set(), actor)
|
||||||
|
|
||||||
|
|
||||||
async def render_followers_page(ctx: dict, actors: list, total: int,
|
def _followers_content_sx(actors: list, total: int,
|
||||||
followed_urls: set, actor: Any) -> str:
|
followed_urls: set, actor: Any) -> str:
|
||||||
"""Full page: followers list."""
|
"""Build followers list content SX string."""
|
||||||
items_sx = _actor_list_items_sx(actors, 1, "followers", followed_urls, actor)
|
items_sx = _actor_list_items_sx(actors, 1, "followers", followed_urls, actor)
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-actor-list-page",
|
"federation-actor-list-page",
|
||||||
title="Followers", count_str=f"({total})",
|
title="Followers", count_str=f"({total})",
|
||||||
items=SxExpr(items_sx) if items_sx else None,
|
items=SxExpr(items_sx) if items_sx else None,
|
||||||
)
|
)
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title="Followers \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
async def render_followers_items(actors: list, page: int,
|
async def render_followers_items(actors: list, page: int,
|
||||||
@@ -586,13 +559,9 @@ async def render_followers_items(actors: list, page: int,
|
|||||||
return _actor_list_items_sx(actors, page, "followers", followed_urls, actor)
|
return _actor_list_items_sx(actors, page, "followers", followed_urls, actor)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def _actor_timeline_content_sx(remote_actor: Any, items: list,
|
||||||
# Public API: Actor timeline
|
is_following: bool, actor: Any) -> str:
|
||||||
# ---------------------------------------------------------------------------
|
"""Build actor timeline content SX string."""
|
||||||
|
|
||||||
async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list,
|
|
||||||
is_following: bool, actor: Any) -> str:
|
|
||||||
"""Full page: remote actor timeline."""
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
from shared.browser.app.csrf import generate_csrf_token
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
|
|
||||||
@@ -640,15 +609,12 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list,
|
|||||||
follow=SxExpr(follow_sx) if follow_sx else None,
|
follow=SxExpr(follow_sx) if follow_sx else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
content = sx_call(
|
return sx_call(
|
||||||
"federation-actor-timeline-layout",
|
"federation-actor-timeline-layout",
|
||||||
header=SxExpr(header_sx),
|
header=SxExpr(header_sx),
|
||||||
timeline=SxExpr(timeline_sx) if timeline_sx else None,
|
timeline=SxExpr(timeline_sx) if timeline_sx else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title=f"{display_name} \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
async def render_actor_timeline_items(items: list, actor_id: int,
|
async def render_actor_timeline_items(items: list, actor_id: int,
|
||||||
actor: Any) -> str:
|
actor: Any) -> str:
|
||||||
@@ -656,13 +622,8 @@ async def render_actor_timeline_items(items: list, actor_id: int,
|
|||||||
return _timeline_items_sx(items, "actor", actor, actor_id)
|
return _timeline_items_sx(items, "actor", actor, actor_id)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def _notifications_content_sx(notifications: list) -> str:
|
||||||
# Public API: Notifications
|
"""Build notifications content SX string."""
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def render_notifications_page(ctx: dict, notifications: list,
|
|
||||||
actor: Any) -> str:
|
|
||||||
"""Full page: notifications."""
|
|
||||||
if not notifications:
|
if not notifications:
|
||||||
notif_sx = sx_call("empty-state", message="No notifications yet.",
|
notif_sx = sx_call("empty-state", message="No notifications yet.",
|
||||||
cls="text-stone-500")
|
cls="text-stone-500")
|
||||||
@@ -673,9 +634,7 @@ async def render_notifications_page(ctx: dict, notifications: list,
|
|||||||
items=SxExpr(items_sx),
|
items=SxExpr(items_sx),
|
||||||
)
|
)
|
||||||
|
|
||||||
content = sx_call("federation-notifications-page", notifs=SxExpr(notif_sx))
|
return sx_call("federation-notifications-page", notifs=SxExpr(notif_sx))
|
||||||
return _social_page(ctx, actor, content=content,
|
|
||||||
title="Notifications \u2014 Rose Ash")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
109
federation/sxc/pages/__init__.py
Normal file
109
federation/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""Federation defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_federation_pages() -> None:
|
||||||
|
"""Register federation-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_federation_layouts()
|
||||||
|
_register_federation_helpers()
|
||||||
|
_load_federation_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_federation_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "federation")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_federation_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("social", _social_full, _social_oob)
|
||||||
|
|
||||||
|
|
||||||
|
def _social_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import _social_header_sx
|
||||||
|
|
||||||
|
actor = ctx.get("actor")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
social_hdr = _social_header_sx(actor)
|
||||||
|
child = header_child_sx(social_hdr)
|
||||||
|
return "(<> " + root_hdr + " " + child + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _social_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _social_header_sx
|
||||||
|
|
||||||
|
actor = ctx.get("actor")
|
||||||
|
social_hdr = _social_header_sx(actor)
|
||||||
|
child_oob = sx_call("oob-header-sx",
|
||||||
|
parent_id="root-header-child",
|
||||||
|
row=SxExpr(social_hdr))
|
||||||
|
root_hdr_oob = root_header_sx(ctx, oob=True)
|
||||||
|
return "(<> " + child_oob + " " + root_hdr_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_federation_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
|
||||||
|
register_page_helpers("federation", {
|
||||||
|
"home-timeline-content": _h_home_timeline_content,
|
||||||
|
"public-timeline-content": _h_public_timeline_content,
|
||||||
|
"compose-content": _h_compose_content,
|
||||||
|
"search-content": _h_search_content,
|
||||||
|
"following-content": _h_following_content,
|
||||||
|
"followers-content": _h_followers_content,
|
||||||
|
"actor-timeline-content": _h_actor_timeline_content,
|
||||||
|
"notifications-content": _h_notifications_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_home_timeline_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "home_timeline_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_public_timeline_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "public_timeline_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_compose_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "compose_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_search_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "search_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_following_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "following_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_followers_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "followers_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_actor_timeline_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "actor_timeline_content", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _h_notifications_content():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "notifications_content", "")
|
||||||
49
federation/sxc/pages/social.sx
Normal file
49
federation/sxc/pages/social.sx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
;; Federation social pages
|
||||||
|
|
||||||
|
(defpage home-timeline
|
||||||
|
:path "/"
|
||||||
|
:auth :login
|
||||||
|
:layout :social
|
||||||
|
:content (home-timeline-content))
|
||||||
|
|
||||||
|
(defpage public-timeline
|
||||||
|
:path "/public"
|
||||||
|
:auth :public
|
||||||
|
:layout :social
|
||||||
|
:content (public-timeline-content))
|
||||||
|
|
||||||
|
(defpage compose-form
|
||||||
|
:path "/compose"
|
||||||
|
:auth :login
|
||||||
|
:layout :social
|
||||||
|
:content (compose-content))
|
||||||
|
|
||||||
|
(defpage search
|
||||||
|
:path "/search"
|
||||||
|
:auth :public
|
||||||
|
:layout :social
|
||||||
|
:content (search-content))
|
||||||
|
|
||||||
|
(defpage following-list
|
||||||
|
:path "/following"
|
||||||
|
:auth :login
|
||||||
|
:layout :social
|
||||||
|
:content (following-content))
|
||||||
|
|
||||||
|
(defpage followers-list
|
||||||
|
:path "/followers"
|
||||||
|
:auth :login
|
||||||
|
:layout :social
|
||||||
|
:content (followers-content))
|
||||||
|
|
||||||
|
(defpage actor-timeline
|
||||||
|
:path "/actor/<int:id>"
|
||||||
|
:auth :public
|
||||||
|
:layout :social
|
||||||
|
:content (actor-timeline-content))
|
||||||
|
|
||||||
|
(defpage notifications
|
||||||
|
:path "/notifications"
|
||||||
|
:auth :login
|
||||||
|
:layout :social
|
||||||
|
:content (notifications-content))
|
||||||
@@ -4,32 +4,32 @@
|
|||||||
<div class="w-full flex flex-row items-center gap-2 flex-wrap">
|
<div class="w-full flex flex-row items-center gap-2 flex-wrap">
|
||||||
{% if actor %}
|
{% if actor %}
|
||||||
<nav class="flex gap-3 text-sm items-center flex-wrap">
|
<nav class="flex gap-3 text-sm items-center flex-wrap">
|
||||||
<a href="{{ url_for('social.home_timeline') }}"
|
<a href="{{ url_for('social.defpage_home_timeline') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.home_timeline') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_home_timeline') %}font-bold{% endif %}">
|
||||||
Timeline
|
Timeline
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.public_timeline') }}"
|
<a href="{{ url_for('social.defpage_public_timeline') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.public_timeline') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_public_timeline') %}font-bold{% endif %}">
|
||||||
Public
|
Public
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.compose_form') }}"
|
<a href="{{ url_for('social.defpage_compose_form') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.compose_form') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_compose_form') %}font-bold{% endif %}">
|
||||||
Compose
|
Compose
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.following_list') }}"
|
<a href="{{ url_for('social.defpage_following_list') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.following_list') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_following_list') %}font-bold{% endif %}">
|
||||||
Following
|
Following
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.followers_list') }}"
|
<a href="{{ url_for('social.defpage_followers_list') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.followers_list') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_followers_list') %}font-bold{% endif %}">
|
||||||
Followers
|
Followers
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.search') }}"
|
<a href="{{ url_for('social.defpage_search') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.search') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 {% if request.path == url_for('social.defpage_search') %}font-bold{% endif %}">
|
||||||
Search
|
Search
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for('social.notifications') }}"
|
<a href="{{ url_for('social.defpage_notifications') }}"
|
||||||
class="px-2 py-1 rounded hover:bg-stone-200 relative {% if request.path == url_for('social.notifications') %}font-bold{% endif %}">
|
class="px-2 py-1 rounded hover:bg-stone-200 relative {% if request.path == url_for('social.defpage_notifications') %}font-bold{% endif %}">
|
||||||
Notifications
|
Notifications
|
||||||
<span sx-get="{{ url_for('social.notification_count') }}" sx-trigger="load, every 30s" sx-swap="innerHTML"
|
<span sx-get="{{ url_for('social.notification_count') }}" sx-trigger="load, every 30s" sx-swap="innerHTML"
|
||||||
class="absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"></span>
|
class="absolute -top-2 -right-3 text-xs bg-red-500 text-white rounded-full px-1 empty:hidden"></span>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
{% if list_type == "following" and a.id %}
|
{% if list_type == "following" and a.id %}
|
||||||
<a href="{{ url_for('social.actor_timeline', id=a.id) }}" class="font-semibold text-stone-900 hover:underline">
|
<a href="{{ url_for('social.defpage_actor_timeline', id=a.id) }}" class="font-semibold text-stone-900 hover:underline">
|
||||||
{{ a.display_name or a.preferred_username }}
|
{{ a.display_name or a.preferred_username }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if oid %}
|
{% if oid %}
|
||||||
<a href="{{ url_for('social.compose_form', reply_to=oid) }}"
|
<a href="{{ url_for('social.defpage_compose_form', reply_to=oid) }}"
|
||||||
class="hover:text-stone-700">Reply</a>
|
class="hover:text-stone-700">Reply</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
{% if a.id %}
|
{% if a.id %}
|
||||||
<a href="{{ url_for('social.actor_timeline', id=a.id) }}" class="font-semibold text-stone-900 hover:underline">
|
<a href="{{ url_for('social.defpage_actor_timeline', id=a.id) }}" class="font-semibold text-stone-900 hover:underline">
|
||||||
{{ a.display_name or a.preferred_username }}
|
{{ a.display_name or a.preferred_username }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
{% block social_content %}
|
{% block social_content %}
|
||||||
<h1 class="text-2xl font-bold mb-6">Search</h1>
|
<h1 class="text-2xl font-bold mb-6">Search</h1>
|
||||||
|
|
||||||
<form method="get" action="{{ url_for('social.search') }}" class="mb-6"
|
<form method="get" action="{{ url_for('social.defpage_search') }}" class="mb-6"
|
||||||
sx-get="{{ url_for('social.search_page') }}"
|
sx-get="{{ url_for('social.defpage_search_page') }}"
|
||||||
sx-target="#search-results"
|
sx-target="#search-results"
|
||||||
sx-push-url="{{ url_for('social.search') }}">
|
sx-push-url="{{ url_for('social.defpage_search') }}">
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input type="text" name="q" value="{{ query }}"
|
<input type="text" name="q" value="{{ query }}"
|
||||||
class="flex-1 border border-stone-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"
|
class="flex-1 border border-stone-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-bold">{{ "Home" if timeline_type == "home" else "Public" }} Timeline</h1>
|
<h1 class="text-2xl font-bold">{{ "Home" if timeline_type == "home" else "Public" }} Timeline</h1>
|
||||||
{% if actor %}
|
{% if actor %}
|
||||||
<a href="{{ url_for('social.compose_form') }}"
|
<a href="{{ url_for('social.defpage_compose_form') }}"
|
||||||
class="bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700">
|
class="bg-stone-800 text-white px-4 py-2 rounded hover:bg-stone-700">
|
||||||
Compose
|
Compose
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -99,25 +99,30 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
# Setup defpage routes
|
||||||
|
from sxc.pages import setup_market_pages
|
||||||
|
setup_market_pages()
|
||||||
|
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
|
||||||
# All markets: / — global view across all pages
|
# All markets: / — global view across all pages
|
||||||
app.register_blueprint(
|
all_markets_bp = register_all_markets()
|
||||||
register_all_markets(),
|
mount_pages(all_markets_bp, "market", names=["all-markets-index"])
|
||||||
url_prefix="/",
|
app.register_blueprint(all_markets_bp, url_prefix="/")
|
||||||
)
|
|
||||||
|
|
||||||
# Page markets: /<slug>/ — markets for a single page
|
# Page markets: /<slug>/ — markets for a single page
|
||||||
app.register_blueprint(
|
page_markets_bp = register_page_markets()
|
||||||
register_page_markets(),
|
mount_pages(page_markets_bp, "market", names=["page-markets-index"])
|
||||||
url_prefix="/<slug>",
|
app.register_blueprint(page_markets_bp, url_prefix="/<slug>")
|
||||||
)
|
|
||||||
|
|
||||||
# Page admin: /<slug>/admin/ — post-level admin for markets
|
# Page admin: /<slug>/admin/ — post-level admin for markets
|
||||||
app.register_blueprint(
|
page_admin_bp = register_page_admin()
|
||||||
register_page_admin(),
|
mount_pages(page_admin_bp, "market", names=["page-admin"])
|
||||||
url_prefix="/<slug>/admin",
|
app.register_blueprint(page_admin_bp, url_prefix="/<slug>/admin")
|
||||||
)
|
|
||||||
|
|
||||||
# Market blueprint nested under post slug: /<page_slug>/<market_slug>/
|
# Market blueprint nested under post slug: /<page_slug>/<market_slug>/
|
||||||
|
# Defpages for market-home and market-admin are mounted inside their
|
||||||
|
# respective nested blueprints (browse and admin register functions).
|
||||||
app.register_blueprint(
|
app.register_blueprint(
|
||||||
register_market_bp(
|
register_market_bp(
|
||||||
url_prefix="/",
|
url_prefix="/",
|
||||||
|
|||||||
@@ -2,70 +2,57 @@
|
|||||||
All-markets blueprint — shows markets across ALL pages.
|
All-markets blueprint — shows markets across ALL pages.
|
||||||
|
|
||||||
Mounted at / (root of market app). No slug context.
|
Mounted at / (root of market app). No slug context.
|
||||||
|
GET / handled by defpage. GET /all-markets is pagination fragment.
|
||||||
Routes:
|
|
||||||
GET / — full page with first page of markets
|
|
||||||
GET /all-markets — HTMX fragment for infinite scroll
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, render_template, make_response
|
from quart import Blueprint, g, request
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from shared.infrastructure.data_client import fetch_data
|
from shared.infrastructure.data_client import fetch_data
|
||||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_markets(page, per_page=20):
|
||||||
|
"""Load all markets + page info for container badges."""
|
||||||
|
markets, has_more = await services.market.list_marketplaces(
|
||||||
|
g.s, page=page, per_page=per_page,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Batch-load page info for container_ids
|
||||||
|
page_info = {}
|
||||||
|
if markets:
|
||||||
|
post_ids = list({
|
||||||
|
m.container_id for m in markets
|
||||||
|
if m.container_type == "page"
|
||||||
|
})
|
||||||
|
if post_ids:
|
||||||
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||||
|
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||||
|
required=False) or []
|
||||||
|
for raw_p in raw_posts:
|
||||||
|
p = dto_from_dict(PostDTO, raw_p)
|
||||||
|
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||||
|
|
||||||
|
return markets, has_more, page_info
|
||||||
|
|
||||||
|
|
||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("all_markets", __name__)
|
bp = Blueprint("all_markets", __name__)
|
||||||
|
|
||||||
async def _load_markets(page, per_page=20):
|
@bp.before_request
|
||||||
"""Load all markets + page info for container badges."""
|
async def _prepare_page_data():
|
||||||
markets, has_more = await services.market.list_marketplaces(
|
"""Load all-markets data for defpage routes."""
|
||||||
g.s, page=page, per_page=per_page,
|
endpoint = request.endpoint or ""
|
||||||
)
|
if not endpoint.endswith("defpage_all_markets_index"):
|
||||||
|
return
|
||||||
# Batch-load page info for container_ids
|
|
||||||
page_info = {}
|
|
||||||
if markets:
|
|
||||||
post_ids = list({
|
|
||||||
m.container_id for m in markets
|
|
||||||
if m.container_type == "page"
|
|
||||||
})
|
|
||||||
if post_ids:
|
|
||||||
raw_posts = await fetch_data("blog", "posts-by-ids",
|
|
||||||
params={"ids": ",".join(str(i) for i in post_ids)},
|
|
||||||
required=False) or []
|
|
||||||
for raw_p in raw_posts:
|
|
||||||
p = dto_from_dict(PostDTO, raw_p)
|
|
||||||
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
|
||||||
|
|
||||||
return markets, has_more, page_info
|
|
||||||
|
|
||||||
@bp.get("/")
|
|
||||||
async def index():
|
|
||||||
page = int(request.args.get("page", 1))
|
page = int(request.args.get("page", 1))
|
||||||
markets, has_more, page_info = await _load_markets(page)
|
markets, has_more, page_info = await _load_markets(page)
|
||||||
|
g.all_markets_data = {
|
||||||
ctx = dict(
|
"markets": markets, "has_more": has_more,
|
||||||
markets=markets,
|
"page_info": page_info, "page": page,
|
||||||
has_more=has_more,
|
}
|
||||||
page_info=page_info,
|
|
||||||
page=page,
|
|
||||||
)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_all_markets_page, render_all_markets_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if is_htmx_request():
|
|
||||||
sx_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
else:
|
|
||||||
html = await render_all_markets_page(tctx, markets, has_more, page_info, page)
|
|
||||||
return await make_response(html, 200)
|
|
||||||
|
|
||||||
@bp.get("/all-markets")
|
@bp.get("/all-markets")
|
||||||
async def markets_fragment():
|
async def markets_fragment():
|
||||||
|
|||||||
@@ -5,17 +5,13 @@ from quart import (
|
|||||||
g,
|
g,
|
||||||
Blueprint,
|
Blueprint,
|
||||||
abort,
|
abort,
|
||||||
render_template,
|
|
||||||
render_template_string,
|
|
||||||
make_response,
|
make_response,
|
||||||
current_app,
|
|
||||||
)
|
)
|
||||||
from shared.config import config
|
from shared.config import config
|
||||||
from .services.nav import category_context, get_nav
|
from .services.nav import category_context, get_nav
|
||||||
from .services.blacklist.category import is_category_blocked
|
from .services.blacklist.category import is_category_blocked
|
||||||
|
|
||||||
from .services import (
|
from .services import (
|
||||||
_hx_fragment_request,
|
|
||||||
_productInfo,
|
_productInfo,
|
||||||
_vary,
|
_vary,
|
||||||
_current_url_without_page,
|
_current_url_without_page,
|
||||||
@@ -33,27 +29,9 @@ def register():
|
|||||||
register_product(),
|
register_product(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@browse_bp.get("/")
|
# Mount defpage for market home (GET /)
|
||||||
@cache_page(tag="browse")
|
from shared.sx.pages import mount_pages
|
||||||
async def home():
|
mount_pages(browse_bp, "market", names=["market-home"])
|
||||||
"""
|
|
||||||
Market landing page.
|
|
||||||
Uses the post data hydrated by the app-level before_request (g.post_data).
|
|
||||||
"""
|
|
||||||
p_data = getattr(g, "post_data", None) or {}
|
|
||||||
|
|
||||||
# Determine which template to use based on request type
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_market_home_page, render_market_home_oob
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
ctx.update(p_data)
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_market_home_page(ctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_market_home_oob(ctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@browse_bp.get("/all/")
|
@browse_bp.get("/all/")
|
||||||
@cache_page(tag="browse")
|
@cache_page(tag="browse")
|
||||||
|
|||||||
@@ -1,31 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import Blueprint
|
||||||
render_template, make_response, Blueprint
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
# ---------- Pages ----------
|
|
||||||
@bp.get("/")
|
|
||||||
@require_admin
|
|
||||||
async def admin():
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
# Mount defpage for market admin (GET /)
|
||||||
from sx.sx_components import render_market_admin_page, render_market_admin_oob
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "market", names=["market-admin"])
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_market_admin_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
sx_src = await render_market_admin_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import unicodedata
|
|||||||
from quart import make_response, request, g, Blueprint
|
from quart import make_response, request, g, Blueprint
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
@@ -27,19 +26,16 @@ def _slugify(value: str, max_len: int = 255) -> str:
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("page_admin", __name__)
|
bp = Blueprint("page_admin", __name__)
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
@require_admin
|
async def _prepare_page_data():
|
||||||
async def admin(**kwargs):
|
"""Pre-render page admin content for defpage (async helper)."""
|
||||||
|
endpoint = request.endpoint or ""
|
||||||
|
if request.method != "GET" or not endpoint.endswith("defpage_page_admin"):
|
||||||
|
return
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import render_page_admin_page, render_page_admin_oob
|
from sx.sx_components import _markets_admin_panel_sx
|
||||||
|
ctx = await get_template_context()
|
||||||
tctx = await get_template_context()
|
g.page_admin_content = await _markets_admin_panel_sx(ctx)
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_page_admin_page(tctx)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
sx_src = await render_page_admin_oob(tctx)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.post("/new/")
|
@bp.post("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
|
|||||||
@@ -2,55 +2,40 @@
|
|||||||
Page-markets blueprint — shows markets for a single page.
|
Page-markets blueprint — shows markets for a single page.
|
||||||
|
|
||||||
Mounted at /<slug> (page-scoped). Requires g.post_data from hydrate_post.
|
Mounted at /<slug> (page-scoped). Requires g.post_data from hydrate_post.
|
||||||
|
GET / handled by defpage. GET /page-markets is pagination fragment.
|
||||||
Routes:
|
|
||||||
GET /<slug>/ — full page scoped to this page
|
|
||||||
GET /<slug>/page-markets — HTMX fragment for infinite scroll
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, render_template, make_response
|
from quart import Blueprint, g, request
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from shared.services.registry import services
|
from shared.services.registry import services
|
||||||
|
|
||||||
|
|
||||||
|
async def _load_markets(post_id, page, per_page=20):
|
||||||
|
"""Load markets for this page's container."""
|
||||||
|
markets, has_more = await services.market.list_marketplaces(
|
||||||
|
g.s, "page", post_id, page=page, per_page=per_page,
|
||||||
|
)
|
||||||
|
return markets, has_more
|
||||||
|
|
||||||
|
|
||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("page_markets", __name__)
|
bp = Blueprint("page_markets", __name__)
|
||||||
|
|
||||||
async def _load_markets(post_id, page, per_page=20):
|
@bp.before_request
|
||||||
"""Load markets for this page's container."""
|
async def _prepare_page_data():
|
||||||
markets, has_more = await services.market.list_marketplaces(
|
"""Load page-markets data for defpage routes."""
|
||||||
g.s, "page", post_id, page=page, per_page=per_page,
|
endpoint = request.endpoint or ""
|
||||||
)
|
if not endpoint.endswith("defpage_page_markets_index"):
|
||||||
return markets, has_more
|
return
|
||||||
|
|
||||||
@bp.get("/")
|
|
||||||
async def index():
|
|
||||||
post = g.post_data["post"]
|
post = g.post_data["post"]
|
||||||
page = int(request.args.get("page", 1))
|
page = int(request.args.get("page", 1))
|
||||||
|
|
||||||
markets, has_more = await _load_markets(post["id"], page)
|
markets, has_more = await _load_markets(post["id"], page)
|
||||||
|
g.page_markets_data = {
|
||||||
ctx = dict(
|
"markets": markets, "has_more": has_more,
|
||||||
markets=markets,
|
"page": page, "post_slug": post.get("slug", ""),
|
||||||
has_more=has_more,
|
}
|
||||||
page_info={},
|
|
||||||
page=page,
|
|
||||||
)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import render_page_markets_page, render_page_markets_oob
|
|
||||||
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["post"] = post
|
|
||||||
if is_htmx_request():
|
|
||||||
sx_src = await render_page_markets_oob(tctx, markets, has_more, page)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
else:
|
|
||||||
html = await render_page_markets_page(tctx, markets, has_more, page)
|
|
||||||
return await make_response(html, 200)
|
|
||||||
|
|
||||||
@bp.get("/page-markets")
|
@bp.get("/page-markets")
|
||||||
async def markets_fragment():
|
async def markets_fragment():
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ def _market_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
sub_div=SxExpr(sub_div) if sub_div else None,
|
sub_div=SxExpr(sub_div) if sub_div else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
link_href = url_for("market.browse.home")
|
link_href = url_for("market.browse.defpage_market_home")
|
||||||
|
|
||||||
# Build desktop nav from categories
|
# Build desktop nav from categories
|
||||||
categories = ctx.get("categories", {})
|
categories = ctx.get("categories", {})
|
||||||
@@ -159,7 +159,7 @@ def _desktop_category_nav_sx(ctx: dict, categories: dict, qs: str,
|
|||||||
|
|
||||||
admin_sx = ""
|
admin_sx = ""
|
||||||
if rights and rights.get("admin"):
|
if rights and rights.get("admin"):
|
||||||
admin_href = prefix + url_for("market.admin.admin")
|
admin_href = prefix + url_for("market.admin.defpage_market_admin")
|
||||||
admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select)
|
admin_sx = sx_call("market-admin-link", href=admin_href, hx_select=hx_select)
|
||||||
|
|
||||||
return sx_call("market-desktop-category-nav",
|
return sx_call("market-desktop-category-nav",
|
||||||
@@ -1203,46 +1203,6 @@ def _no_markets_sx(message: str = "No markets available") -> str:
|
|||||||
cls="px-3 py-12 text-center text-stone-400")
|
cls="px-3 py-12 text-center text-stone-400")
|
||||||
|
|
||||||
|
|
||||||
async def render_all_markets_page(ctx: dict, markets: list, has_more: bool,
|
|
||||||
page_info: dict, page: int) -> str:
|
|
||||||
"""Full page: all markets listing."""
|
|
||||||
from quart import url_for
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
|
||||||
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
|
|
||||||
|
|
||||||
if markets:
|
|
||||||
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
|
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx()
|
|
||||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_all_markets_oob(ctx: dict, markets: list, has_more: bool,
|
|
||||||
page_info: dict, page: int) -> str:
|
|
||||||
"""OOB response: all markets listing."""
|
|
||||||
from quart import url_for
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
|
||||||
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
|
|
||||||
|
|
||||||
if markets:
|
|
||||||
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
|
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx()
|
|
||||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
|
||||||
|
|
||||||
oobs = root_header_sx(ctx, oob=True)
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_all_markets_cards(markets: list, has_more: bool,
|
async def render_all_markets_cards(markets: list, has_more: bool,
|
||||||
page_info: dict, page: int) -> str:
|
page_info: dict, page: int) -> str:
|
||||||
"""Pagination fragment: all markets cards."""
|
"""Pagination fragment: all markets cards."""
|
||||||
@@ -1258,54 +1218,6 @@ async def render_all_markets_cards(markets: list, has_more: bool,
|
|||||||
# Page markets
|
# Page markets
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_page_markets_page(ctx: dict, markets: list, has_more: bool,
|
|
||||||
page: int) -> str:
|
|
||||||
"""Full page: page-scoped markets listing."""
|
|
||||||
from quart import url_for
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
|
||||||
post = ctx.get("post", {})
|
|
||||||
post_slug = post.get("slug", "")
|
|
||||||
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
|
|
||||||
|
|
||||||
if markets:
|
|
||||||
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
|
|
||||||
show_page_badge=False, post_slug=post_slug)
|
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx("No markets for this page")
|
|
||||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
hdr = "(<> " + hdr + " " + header_child_sx(_post_header_sx(ctx)) + ")"
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_page_markets_oob(ctx: dict, markets: list, has_more: bool,
|
|
||||||
page: int) -> str:
|
|
||||||
"""OOB response: page-scoped markets."""
|
|
||||||
from quart import url_for
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
|
||||||
post = ctx.get("post", {})
|
|
||||||
post_slug = post.get("slug", "")
|
|
||||||
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
|
|
||||||
|
|
||||||
if markets:
|
|
||||||
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
|
|
||||||
show_page_badge=False, post_slug=post_slug)
|
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx("No markets for this page")
|
|
||||||
content = "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
|
||||||
|
|
||||||
oobs = _oob_header_sx("post-header-child", "market-header-child", "")
|
|
||||||
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_page_markets_cards(markets: list, has_more: bool,
|
async def render_page_markets_cards(markets: list, has_more: bool,
|
||||||
page: int, post_slug: str) -> str:
|
page: int, post_slug: str) -> str:
|
||||||
"""Pagination fragment: page-scoped markets cards."""
|
"""Pagination fragment: page-scoped markets cards."""
|
||||||
@@ -1322,31 +1234,6 @@ async def render_page_markets_cards(markets: list, has_more: bool,
|
|||||||
# Market landing page
|
# Market landing page
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_market_home_page(ctx: dict) -> str:
|
|
||||||
"""Full page: market landing page (post content)."""
|
|
||||||
post = ctx.get("post") or {}
|
|
||||||
content = _market_landing_content_sx(post)
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")"
|
|
||||||
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
|
|
||||||
menu = _mobile_nav_panel_sx(ctx)
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content, menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_market_home_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: market landing page."""
|
|
||||||
post = ctx.get("post") or {}
|
|
||||||
content = _market_landing_content_sx(post)
|
|
||||||
|
|
||||||
oobs = _oob_header_sx("post-header-child", "market-header-child",
|
|
||||||
_market_header_sx(ctx))
|
|
||||||
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " "
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"market-row", "market-header-child") + ")"
|
|
||||||
menu = _mobile_nav_panel_sx(ctx)
|
|
||||||
return oob_page_sx(oobs=oobs, content=content, menu=menu)
|
|
||||||
|
|
||||||
|
|
||||||
def _market_landing_content_sx(post: dict) -> str:
|
def _market_landing_content_sx(post: dict) -> str:
|
||||||
"""Build market landing page content as sx."""
|
"""Build market landing page content as sx."""
|
||||||
@@ -1485,29 +1372,6 @@ def _product_admin_header_sx(ctx: dict, d: dict, *, oob: bool = False) -> str:
|
|||||||
# Market admin
|
# Market admin
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_market_admin_page(ctx: dict) -> str:
|
|
||||||
"""Full page: market admin."""
|
|
||||||
content = '"market admin"'
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " "
|
|
||||||
child += _market_admin_header_sx(ctx, selected="markets") + ")"
|
|
||||||
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_market_admin_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: market admin."""
|
|
||||||
content = '"market admin"'
|
|
||||||
|
|
||||||
oobs = "(<> " + _market_header_sx(ctx, oob=True) + " "
|
|
||||||
oobs += _oob_header_sx("market-header-child", "market-admin-header-child",
|
|
||||||
_market_admin_header_sx(ctx, selected="markets")) + " "
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"market-row", "market-header-child",
|
|
||||||
"market-admin-row", "market-admin-header-child") + ")"
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
def _market_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str:
|
def _market_admin_header_sx(ctx: dict, *, oob: bool = False, selected: str = "") -> str:
|
||||||
"""Build market admin header row — delegates to shared helper."""
|
"""Build market admin header row — delegates to shared helper."""
|
||||||
@@ -1586,28 +1450,6 @@ async def render_markets_admin_list_panel(ctx: dict) -> str:
|
|||||||
return await _markets_admin_panel_sx(ctx)
|
return await _markets_admin_panel_sx(ctx)
|
||||||
|
|
||||||
|
|
||||||
async def render_page_admin_page(ctx: dict) -> str:
|
|
||||||
"""Full page: page-level market admin."""
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
admin_hdr = post_admin_header_sx(ctx, slug, selected="markets")
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
child = "(<> " + _post_header_sx(ctx) + " " + admin_hdr + ")"
|
|
||||||
hdr = "(<> " + hdr + " " + header_child_sx(child) + ")"
|
|
||||||
content = await _markets_admin_panel_sx(ctx)
|
|
||||||
content = '(div :id "main-panel" ' + content + ')'
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_page_admin_oob(ctx: dict) -> str:
|
|
||||||
"""OOB response: page-level market admin."""
|
|
||||||
slug = (ctx.get("post") or {}).get("slug", "")
|
|
||||||
oobs = "(<> " + post_admin_header_sx(ctx, slug, oob=True, selected="markets") + " "
|
|
||||||
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
|
||||||
"post-admin-row", "post-admin-header-child") + ")"
|
|
||||||
content = await _markets_admin_panel_sx(ctx)
|
|
||||||
content = '(div :id "main-panel" ' + content + ')'
|
|
||||||
return oob_page_sx(oobs=oobs, content=content)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Public API: POST handler fragment renderers
|
# Public API: POST handler fragment renderers
|
||||||
|
|||||||
0
market/sxc/__init__.py
Normal file
0
market/sxc/__init__.py
Normal file
170
market/sxc/pages/__init__.py
Normal file
170
market/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
"""Market defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_market_pages() -> None:
|
||||||
|
"""Register market-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_market_layouts()
|
||||||
|
_register_market_helpers()
|
||||||
|
_load_market_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_market_page_files() -> None:
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "market")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_market_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("market", _market_full, _market_oob, _market_mobile)
|
||||||
|
register_custom_layout("market-admin", _market_admin_full, _market_admin_oob)
|
||||||
|
|
||||||
|
|
||||||
|
def _market_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import _post_header_sx, _market_header_sx
|
||||||
|
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + ")"
|
||||||
|
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _market_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import _post_header_sx, _market_header_sx, _clear_deeper_oob
|
||||||
|
|
||||||
|
oobs = oob_header_sx("post-header-child", "market-header-child",
|
||||||
|
_market_header_sx(ctx))
|
||||||
|
oobs = "(<> " + oobs + " " + _post_header_sx(ctx, oob=True) + " "
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"market-row", "market-header-child") + ")"
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
def _market_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
from sx.sx_components import _mobile_nav_panel_sx
|
||||||
|
return _mobile_nav_panel_sx(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def _market_admin_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_post_header_sx, _market_header_sx, _market_admin_header_sx,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = kw.get("selected", "")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
child = "(<> " + _post_header_sx(ctx) + " " + _market_header_sx(ctx) + " "
|
||||||
|
child += _market_admin_header_sx(ctx, selected=selected) + ")"
|
||||||
|
return "(<> " + root_hdr + " " + header_child_sx(child) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _market_admin_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import oob_header_sx
|
||||||
|
from sx.sx_components import (
|
||||||
|
_market_header_sx, _market_admin_header_sx, _clear_deeper_oob,
|
||||||
|
)
|
||||||
|
|
||||||
|
selected = kw.get("selected", "")
|
||||||
|
oobs = "(<> " + _market_header_sx(ctx, oob=True) + " "
|
||||||
|
oobs += oob_header_sx("market-header-child", "market-admin-header-child",
|
||||||
|
_market_admin_header_sx(ctx, selected=selected)) + " "
|
||||||
|
oobs += _clear_deeper_oob("post-row", "post-header-child",
|
||||||
|
"market-row", "market-header-child",
|
||||||
|
"market-admin-row", "market-admin-header-child") + ")"
|
||||||
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_market_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
|
||||||
|
register_page_helpers("market", {
|
||||||
|
"all-markets-content": _h_all_markets_content,
|
||||||
|
"page-markets-content": _h_page_markets_content,
|
||||||
|
"page-admin-content": _h_page_admin_content,
|
||||||
|
"market-home-content": _h_market_home_content,
|
||||||
|
"market-admin-content": _h_market_admin_content,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_all_markets_content():
|
||||||
|
from quart import g, url_for, request
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
|
||||||
|
data = getattr(g, "all_markets_data", None)
|
||||||
|
if not data:
|
||||||
|
from sx.sx_components import _no_markets_sx
|
||||||
|
return _no_markets_sx()
|
||||||
|
|
||||||
|
markets = data["markets"]
|
||||||
|
has_more = data["has_more"]
|
||||||
|
page_info = data["page_info"]
|
||||||
|
page = data["page"]
|
||||||
|
|
||||||
|
prefix = route_prefix()
|
||||||
|
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
|
||||||
|
|
||||||
|
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
|
||||||
|
if markets:
|
||||||
|
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
|
||||||
|
content = _markets_grid(cards)
|
||||||
|
else:
|
||||||
|
content = _no_markets_sx()
|
||||||
|
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _h_page_markets_content():
|
||||||
|
from quart import g, url_for
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
|
||||||
|
data = getattr(g, "page_markets_data", None)
|
||||||
|
if not data:
|
||||||
|
from sx.sx_components import _no_markets_sx
|
||||||
|
return _no_markets_sx("No markets for this page")
|
||||||
|
|
||||||
|
markets = data["markets"]
|
||||||
|
has_more = data["has_more"]
|
||||||
|
page = data["page"]
|
||||||
|
post_slug = data.get("post_slug", "")
|
||||||
|
|
||||||
|
prefix = route_prefix()
|
||||||
|
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
|
||||||
|
|
||||||
|
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
|
||||||
|
if markets:
|
||||||
|
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
|
||||||
|
show_page_badge=False, post_slug=post_slug)
|
||||||
|
content = _markets_grid(cards)
|
||||||
|
else:
|
||||||
|
content = _no_markets_sx("No markets for this page")
|
||||||
|
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _h_page_admin_content():
|
||||||
|
# Content pre-rendered by before_request (async _markets_admin_panel_sx)
|
||||||
|
from quart import g
|
||||||
|
content = getattr(g, "page_admin_content", "")
|
||||||
|
return '(div :id "main-panel" ' + content + ')'
|
||||||
|
|
||||||
|
|
||||||
|
def _h_market_home_content():
|
||||||
|
from quart import g
|
||||||
|
post_data = getattr(g, "post_data", {})
|
||||||
|
post = post_data.get("post", {})
|
||||||
|
from sx.sx_components import _market_landing_content_sx
|
||||||
|
return _market_landing_content_sx(post)
|
||||||
|
|
||||||
|
|
||||||
|
def _h_market_admin_content():
|
||||||
|
return '"market admin"'
|
||||||
37
market/sxc/pages/market.sx
Normal file
37
market/sxc/pages/market.sx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
;; Market app defpage declarations.
|
||||||
|
;;
|
||||||
|
;; all-markets-index: / — global view across all pages
|
||||||
|
;; page-markets-index: / (on page_markets bp, mounted at /<slug>)
|
||||||
|
;; page-admin: / (on page_admin bp, mounted at /<slug>/admin)
|
||||||
|
;; market-home: / (on browse bp, mounted at /<page_slug>/<market_slug>)
|
||||||
|
;; market-admin: / (on admin bp, mounted at /<page_slug>/<market_slug>/admin)
|
||||||
|
|
||||||
|
(defpage all-markets-index
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :root
|
||||||
|
:content (all-markets-content))
|
||||||
|
|
||||||
|
(defpage page-markets-index
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :post
|
||||||
|
:content (page-markets-content))
|
||||||
|
|
||||||
|
(defpage page-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:post-admin :selected "markets")
|
||||||
|
:content (page-admin-content))
|
||||||
|
|
||||||
|
(defpage market-home
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout :market
|
||||||
|
:content (market-home-content))
|
||||||
|
|
||||||
|
(defpage market-admin
|
||||||
|
:path "/"
|
||||||
|
:auth :admin
|
||||||
|
:layout (:market-admin :selected "markets")
|
||||||
|
:content (market-admin-content))
|
||||||
@@ -70,16 +70,22 @@ def create_app() -> "Quart":
|
|||||||
app.jinja_loader,
|
app.jinja_loader,
|
||||||
])
|
])
|
||||||
|
|
||||||
# Load orders-specific s-expression components
|
# Load orders-specific s-expression components (loaded at import time)
|
||||||
from sx.sx_components import load_orders_components
|
import sx.sx_components # noqa: F811
|
||||||
load_orders_components()
|
|
||||||
|
# Setup defpage routes
|
||||||
|
from sxc.pages import setup_orders_pages
|
||||||
|
setup_orders_pages()
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
app.register_blueprint(register_actions())
|
app.register_blueprint(register_actions())
|
||||||
app.register_blueprint(register_data())
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# Orders list at /
|
# Orders list at / (defpage routes mounted below)
|
||||||
app.register_blueprint(register_orders(url_prefix="/"))
|
bp = register_orders(url_prefix="/")
|
||||||
|
from shared.sx.pages import mount_pages
|
||||||
|
mount_pages(bp, "orders")
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
# Checkout webhook + return
|
# Checkout webhook + return
|
||||||
app.register_blueprint(register_checkout())
|
app.register_blueprint(register_checkout())
|
||||||
|
|||||||
@@ -2,16 +2,13 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from quart import Blueprint, g, redirect, url_for, make_response
|
from quart import Blueprint, g, redirect, url_for, make_response
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from shared.models.order import Order
|
from shared.models.order import Order
|
||||||
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
|
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
|
||||||
from shared.config import config
|
from shared.config import config
|
||||||
|
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from services.check_sumup_status import check_sumup_status
|
from services.check_sumup_status import check_sumup_status
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
|
|
||||||
from .filters.qs import makeqs_factory, decode
|
from .filters.qs import makeqs_factory, decode
|
||||||
|
|
||||||
@@ -33,34 +30,6 @@ def register() -> Blueprint:
|
|||||||
def route():
|
def route():
|
||||||
g.makeqs_factory = makeqs_factory
|
g.makeqs_factory = makeqs_factory
|
||||||
|
|
||||||
@bp.get("/")
|
|
||||||
async def order_detail(order_id: int):
|
|
||||||
"""Show a single order + items."""
|
|
||||||
owner = _owner_filter()
|
|
||||||
if owner is None:
|
|
||||||
return await make_response("Order not found", 404)
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Order)
|
|
||||||
.options(selectinload(Order.items))
|
|
||||||
.where(Order.id == order_id, owner)
|
|
||||||
)
|
|
||||||
order = result.scalar_one_or_none()
|
|
||||||
if not order:
|
|
||||||
return await make_response("Order not found", 404)
|
|
||||||
|
|
||||||
from sx.sx_components import render_order_page, render_order_oob
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
calendar_entries = ctx.get("calendar_entries")
|
|
||||||
|
|
||||||
if not is_htmx_request():
|
|
||||||
html = await render_order_page(ctx, order, calendar_entries, url_for)
|
|
||||||
return await make_response(html)
|
|
||||||
else:
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
sx_src = await render_order_oob(ctx, order, calendar_entries, url_for)
|
|
||||||
return sx_response(sx_src)
|
|
||||||
|
|
||||||
@bp.get("/pay/")
|
@bp.get("/pay/")
|
||||||
async def order_pay(order_id: int):
|
async def order_pay(order_id: int):
|
||||||
"""Re-open the SumUp payment page for this order."""
|
"""Re-open the SumUp payment page for this order."""
|
||||||
@@ -73,7 +42,7 @@ def register() -> Blueprint:
|
|||||||
return await make_response("Order not found", 404)
|
return await make_response("Order not found", 404)
|
||||||
|
|
||||||
if order.status == "paid":
|
if order.status == "paid":
|
||||||
return redirect(url_for("orders.order.order_detail", order_id=order.id))
|
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
|
||||||
|
|
||||||
if order.sumup_hosted_url:
|
if order.sumup_hosted_url:
|
||||||
return redirect(order.sumup_hosted_url)
|
return redirect(order.sumup_hosted_url)
|
||||||
@@ -120,13 +89,13 @@ def register() -> Blueprint:
|
|||||||
return await make_response("Order not found", 404)
|
return await make_response("Order not found", 404)
|
||||||
|
|
||||||
if not order.sumup_checkout_id:
|
if not order.sumup_checkout_id:
|
||||||
return redirect(url_for("orders.order.order_detail", order_id=order.id))
|
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await check_sumup_status(g.s, order)
|
await check_sumup_status(g.s, order)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return redirect(url_for("orders.order.order_detail", order_id=order.id))
|
return redirect(url_for("orders.defpage_order_detail", order_id=order.id))
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, render_template, redirect, url_for, make_response
|
from quart import Blueprint, g, redirect, url_for, make_response, request
|
||||||
from sqlalchemy import select, func, or_, cast, String, exists
|
from sqlalchemy import select, func, or_, cast, String, exists
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
@@ -20,18 +20,6 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
|
|
||||||
ORDERS_PER_PAGE = 10
|
ORDERS_PER_PAGE = 10
|
||||||
|
|
||||||
oob = {
|
|
||||||
"extends": "_types/root/_index.html",
|
|
||||||
"child_id": "auth-header-child",
|
|
||||||
"header": "_types/auth/header/_header.html",
|
|
||||||
"nav": "_types/auth/_nav.html",
|
|
||||||
"main": "_types/auth/_main_panel.html",
|
|
||||||
}
|
|
||||||
|
|
||||||
@bp.context_processor
|
|
||||||
def inject_oob():
|
|
||||||
return {"oob": oob}
|
|
||||||
|
|
||||||
@bp.before_request
|
@bp.before_request
|
||||||
def route():
|
def route():
|
||||||
g.makeqs_factory = makeqs_factory
|
g.makeqs_factory = makeqs_factory
|
||||||
@@ -43,8 +31,115 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
if not ident["user_id"] and not ident["session_id"]:
|
if not ident["user_id"] and not ident["session_id"]:
|
||||||
return redirect(url_for("auth.login_form"))
|
return redirect(url_for("auth.login_form"))
|
||||||
|
|
||||||
@bp.get("/")
|
@bp.before_request
|
||||||
async def list_orders():
|
async def _prepare_page_data():
|
||||||
|
"""Load data for defpage routes into g.*."""
|
||||||
|
if request.method != "GET":
|
||||||
|
return
|
||||||
|
|
||||||
|
endpoint = request.endpoint or ""
|
||||||
|
|
||||||
|
# Orders list page
|
||||||
|
if endpoint.endswith("defpage_orders_list"):
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner_clause = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner_clause = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
q = decode()
|
||||||
|
page, search = q.page, q.search
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
where_clause = _search_clause(search) if search else None
|
||||||
|
|
||||||
|
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
||||||
|
if where_clause is not None:
|
||||||
|
count_stmt = count_stmt.where(where_clause)
|
||||||
|
|
||||||
|
total_count_result = await g.s.execute(count_stmt)
|
||||||
|
total_count = total_count_result.scalar_one() or 0
|
||||||
|
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
|
||||||
|
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
|
||||||
|
offset = (page - 1) * ORDERS_PER_PAGE
|
||||||
|
stmt = (
|
||||||
|
select(Order)
|
||||||
|
.where(owner_clause)
|
||||||
|
.order_by(Order.created_at.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(ORDERS_PER_PAGE)
|
||||||
|
)
|
||||||
|
if where_clause is not None:
|
||||||
|
stmt = stmt.where(where_clause)
|
||||||
|
|
||||||
|
result = await g.s.execute(stmt)
|
||||||
|
orders = result.scalars().all()
|
||||||
|
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
pfx = route_prefix()
|
||||||
|
qs_fn = makeqs_factory()
|
||||||
|
|
||||||
|
g.orders_page_data = {
|
||||||
|
"orders": orders,
|
||||||
|
"page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"search": search,
|
||||||
|
"search_count": total_count,
|
||||||
|
"url_for_fn": url_for,
|
||||||
|
"qs_fn": qs_fn,
|
||||||
|
"list_url": pfx + url_for("orders.defpage_orders_list"),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Order detail page
|
||||||
|
elif endpoint.endswith("defpage_order_detail"):
|
||||||
|
order_id = request.view_args.get("order_id")
|
||||||
|
if order_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
from quart import abort
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(Order)
|
||||||
|
.options(selectinload(Order.items))
|
||||||
|
.where(Order.id == order_id, owner)
|
||||||
|
)
|
||||||
|
order = result.scalar_one_or_none()
|
||||||
|
if not order:
|
||||||
|
from quart import abort
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
from shared.browser.app.csrf import generate_csrf_token
|
||||||
|
pfx = route_prefix()
|
||||||
|
|
||||||
|
g.order_detail_data = {
|
||||||
|
"order": order,
|
||||||
|
"calendar_entries": None,
|
||||||
|
"detail_url": pfx + url_for("orders.defpage_order_detail", order_id=order.id),
|
||||||
|
"list_url": pfx + url_for("orders.defpage_orders_list"),
|
||||||
|
"recheck_url": pfx + url_for("orders.order.order_recheck", order_id=order.id),
|
||||||
|
"pay_url": pfx + url_for("orders.order.order_pay", order_id=order.id),
|
||||||
|
"csrf_token": generate_csrf_token(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@bp.get("/rows")
|
||||||
|
async def orders_rows():
|
||||||
|
"""Pagination endpoint — returns order rows for page > 1."""
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
if ident["user_id"]:
|
if ident["user_id"]:
|
||||||
owner_clause = Order.user_id == ident["user_id"]
|
owner_clause = Order.user_id == ident["user_id"]
|
||||||
@@ -58,38 +153,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
if page < 1:
|
if page < 1:
|
||||||
page = 1
|
page = 1
|
||||||
|
|
||||||
where_clause = None
|
where_clause = _search_clause(search) if search else None
|
||||||
if search:
|
|
||||||
term = f"%{search.strip()}%"
|
|
||||||
conditions = [
|
|
||||||
Order.status.ilike(term),
|
|
||||||
Order.currency.ilike(term),
|
|
||||||
Order.sumup_checkout_id.ilike(term),
|
|
||||||
Order.sumup_status.ilike(term),
|
|
||||||
Order.description.ilike(term),
|
|
||||||
]
|
|
||||||
conditions.append(
|
|
||||||
exists(
|
|
||||||
select(1)
|
|
||||||
.select_from(OrderItem)
|
|
||||||
.where(
|
|
||||||
OrderItem.order_id == Order.id,
|
|
||||||
or_(
|
|
||||||
OrderItem.product_title.ilike(term),
|
|
||||||
OrderItem.product_slug.ilike(term),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
search_id = int(search)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
search_id = None
|
|
||||||
if search_id is not None:
|
|
||||||
conditions.append(Order.id == search_id)
|
|
||||||
else:
|
|
||||||
conditions.append(cast(Order.id, String).ilike(term))
|
|
||||||
where_clause = or_(*conditions)
|
|
||||||
|
|
||||||
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
||||||
if where_clause is not None:
|
if where_clause is not None:
|
||||||
@@ -116,38 +180,47 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
result = await g.s.execute(stmt)
|
result = await g.s.execute(stmt)
|
||||||
orders = result.scalars().all()
|
orders = result.scalars().all()
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
from sx.sx_components import _orders_rows_sx
|
||||||
from sx.sx_components import (
|
from shared.sx.helpers import sx_response
|
||||||
render_orders_page,
|
|
||||||
render_orders_rows,
|
|
||||||
render_orders_oob,
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
qs_fn = makeqs_factory()
|
qs_fn = makeqs_factory()
|
||||||
|
sx_src = _orders_rows_sx(orders, page, total_pages, url_for, qs_fn)
|
||||||
if not is_htmx_request():
|
resp = sx_response(sx_src)
|
||||||
html = await render_orders_page(
|
|
||||||
ctx, orders, page, total_pages, search, total_count,
|
|
||||||
url_for, qs_fn,
|
|
||||||
)
|
|
||||||
resp = await make_response(html)
|
|
||||||
elif page > 1:
|
|
||||||
# Sx wire format — client renders order rows
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
sx_src = await render_orders_rows(
|
|
||||||
ctx, orders, page, total_pages, url_for, qs_fn,
|
|
||||||
)
|
|
||||||
resp = sx_response(sx_src)
|
|
||||||
else:
|
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
sx_src = await render_orders_oob(
|
|
||||||
ctx, orders, page, total_pages, search, total_count,
|
|
||||||
url_for, qs_fn,
|
|
||||||
)
|
|
||||||
resp = sx_response(sx_src)
|
|
||||||
|
|
||||||
resp.headers["Hx-Push-Url"] = _current_url_without_page()
|
resp.headers["Hx-Push-Url"] = _current_url_without_page()
|
||||||
return _vary(resp)
|
return _vary(resp)
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|
||||||
|
|
||||||
|
def _search_clause(search: str):
|
||||||
|
"""Build an OR search clause across order fields."""
|
||||||
|
term = f"%{search.strip()}%"
|
||||||
|
conditions = [
|
||||||
|
Order.status.ilike(term),
|
||||||
|
Order.currency.ilike(term),
|
||||||
|
Order.sumup_checkout_id.ilike(term),
|
||||||
|
Order.sumup_status.ilike(term),
|
||||||
|
Order.description.ilike(term),
|
||||||
|
]
|
||||||
|
conditions.append(
|
||||||
|
exists(
|
||||||
|
select(1)
|
||||||
|
.select_from(OrderItem)
|
||||||
|
.where(
|
||||||
|
OrderItem.order_id == Order.id,
|
||||||
|
or_(
|
||||||
|
OrderItem.product_title.ilike(term),
|
||||||
|
OrderItem.product_slug.ilike(term),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
search_id = int(search)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
search_id = None
|
||||||
|
if search_id is not None:
|
||||||
|
conditions.append(Order.id == search_id)
|
||||||
|
else:
|
||||||
|
conditions.append(cast(Order.id, String).ilike(term))
|
||||||
|
return or_(*conditions)
|
||||||
|
|||||||
@@ -12,10 +12,8 @@ from typing import Any
|
|||||||
|
|
||||||
from shared.sx.jinja_bridge import load_service_components
|
from shared.sx.jinja_bridge import load_service_components
|
||||||
from shared.sx.helpers import (
|
from shared.sx.helpers import (
|
||||||
call_url, root_header_sx,
|
call_url,
|
||||||
full_page_sx, header_child_sx, oob_page_sx,
|
|
||||||
sx_call, SxExpr,
|
sx_call, SxExpr,
|
||||||
search_mobile_sx, search_desktop_sx,
|
|
||||||
)
|
)
|
||||||
from shared.infrastructure.urls import market_product_url, cart_url
|
from shared.infrastructure.urls import market_product_url, cart_url
|
||||||
|
|
||||||
@@ -103,7 +101,7 @@ def _orders_rows_sx(orders: list, page: int, total_pages: int,
|
|||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
for o in orders:
|
for o in orders:
|
||||||
d = _order_row_data(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id))
|
d = _order_row_data(o, pfx + url_for_fn("orders.defpage_order_detail", order_id=o.id))
|
||||||
parts.append(sx_call("order-row-desktop",
|
parts.append(sx_call("order-row-desktop",
|
||||||
oid=d["oid"], created=d["created"],
|
oid=d["oid"], created=d["created"],
|
||||||
desc=d["desc"], total=d["total"],
|
desc=d["desc"], total=d["total"],
|
||||||
@@ -115,7 +113,7 @@ def _orders_rows_sx(orders: list, page: int, total_pages: int,
|
|||||||
status=d["status"], url=d["url"]))
|
status=d["status"], url=d["url"]))
|
||||||
|
|
||||||
if page < total_pages:
|
if page < total_pages:
|
||||||
next_url = pfx + url_for_fn("orders.list_orders") + qs_fn(page=page + 1)
|
next_url = pfx + url_for_fn("orders.orders_rows") + qs_fn(page=page + 1)
|
||||||
parts.append(sx_call("infinite-scroll",
|
parts.append(sx_call("infinite-scroll",
|
||||||
url=next_url, page=page,
|
url=next_url, page=page,
|
||||||
total_pages=total_pages,
|
total_pages=total_pages,
|
||||||
@@ -143,63 +141,8 @@ def _orders_summary_sx(ctx: dict) -> str:
|
|||||||
# Public API: orders list
|
# Public API: orders list
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async def render_orders_page(ctx: dict, orders: list, page: int,
|
|
||||||
total_pages: int, search: str | None,
|
|
||||||
search_count: int, url_for_fn: Any,
|
|
||||||
qs_fn: Any) -> str:
|
|
||||||
"""Full page: orders list (sx wire format)."""
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
ctx["search"] = search
|
|
||||||
ctx["search_count"] = search_count
|
|
||||||
list_url = route_prefix() + url_for_fn("orders.list_orders")
|
|
||||||
|
|
||||||
rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
|
|
||||||
main = _orders_main_panel_sx(orders, rows)
|
|
||||||
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
inner = "(<> " + _auth_header_sx(ctx) + " " + _orders_header_sx(ctx, list_url) + ")"
|
|
||||||
hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")"
|
|
||||||
|
|
||||||
return full_page_sx(ctx, header_rows=hdr,
|
|
||||||
filter=_orders_summary_sx(ctx),
|
|
||||||
aside=search_desktop_sx(ctx),
|
|
||||||
content=main)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_orders_rows(ctx: dict, orders: list, page: int,
|
|
||||||
total_pages: int, url_for_fn: Any,
|
|
||||||
qs_fn: Any) -> str:
|
|
||||||
"""Pagination: just the table rows (sx wire format)."""
|
|
||||||
return _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def render_orders_oob(ctx: dict, orders: list, page: int,
|
|
||||||
total_pages: int, search: str | None,
|
|
||||||
search_count: int, url_for_fn: Any,
|
|
||||||
qs_fn: Any) -> str:
|
|
||||||
"""OOB response for HTMX navigation to orders list (sx)."""
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
ctx["search"] = search
|
|
||||||
ctx["search_count"] = search_count
|
|
||||||
list_url = route_prefix() + url_for_fn("orders.list_orders")
|
|
||||||
|
|
||||||
rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
|
|
||||||
main = _orders_main_panel_sx(orders, rows)
|
|
||||||
|
|
||||||
auth_hdr = _auth_header_sx(ctx, oob=True)
|
|
||||||
auth_child_oob = sx_call("oob-header-sx",
|
|
||||||
parent_id="auth-header-child",
|
|
||||||
row=SxExpr(_orders_header_sx(ctx, list_url)))
|
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
|
||||||
oobs = "(<> " + auth_hdr + " " + auth_child_oob + " " + root_hdr + ")"
|
|
||||||
|
|
||||||
return oob_page_sx(oobs=oobs,
|
|
||||||
filter=_orders_summary_sx(ctx),
|
|
||||||
aside=search_desktop_sx(ctx),
|
|
||||||
content=main)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -300,68 +243,8 @@ def _order_filter_sx(order: Any, list_url: str, recheck_url: str,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def render_order_page(ctx: dict, order: Any,
|
|
||||||
calendar_entries: list | None,
|
|
||||||
url_for_fn: Any) -> str:
|
|
||||||
"""Full page: single order detail (sx wire format)."""
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
|
||||||
|
|
||||||
pfx = route_prefix()
|
|
||||||
detail_url = pfx + url_for_fn("orders.order.order_detail", order_id=order.id)
|
|
||||||
list_url = pfx + url_for_fn("orders.list_orders")
|
|
||||||
recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id)
|
|
||||||
pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id)
|
|
||||||
|
|
||||||
main = _order_main_sx(order, calendar_entries)
|
|
||||||
filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token())
|
|
||||||
|
|
||||||
# Header stack: root -> auth -> orders -> order
|
|
||||||
hdr = root_header_sx(ctx)
|
|
||||||
order_row = sx_call(
|
|
||||||
"menu-row-sx",
|
|
||||||
id="order-row", level=3, colour="sky", link_href=detail_url,
|
|
||||||
link_label="Order", icon="fa fa-gbp",
|
|
||||||
)
|
|
||||||
detail_header = sx_call(
|
|
||||||
"order-detail-header-stack",
|
|
||||||
auth=SxExpr(_auth_header_sx(ctx)),
|
|
||||||
orders=SxExpr(_orders_header_sx(ctx, list_url)),
|
|
||||||
order=SxExpr(order_row),
|
|
||||||
)
|
|
||||||
hdr = "(<> " + hdr + " " + detail_header + ")"
|
|
||||||
|
|
||||||
return full_page_sx(ctx, header_rows=hdr, filter=filt, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
async def render_order_oob(ctx: dict, order: Any,
|
|
||||||
calendar_entries: list | None,
|
|
||||||
url_for_fn: Any) -> str:
|
|
||||||
"""OOB response for single order detail (sx)."""
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
|
||||||
|
|
||||||
pfx = route_prefix()
|
|
||||||
detail_url = pfx + url_for_fn("orders.order.order_detail", order_id=order.id)
|
|
||||||
list_url = pfx + url_for_fn("orders.list_orders")
|
|
||||||
recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id)
|
|
||||||
pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id)
|
|
||||||
|
|
||||||
main = _order_main_sx(order, calendar_entries)
|
|
||||||
filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token())
|
|
||||||
|
|
||||||
order_row_oob = sx_call(
|
|
||||||
"menu-row-sx",
|
|
||||||
id="order-row", level=3, colour="sky", link_href=detail_url,
|
|
||||||
link_label="Order", icon="fa fa-gbp", oob=True,
|
|
||||||
)
|
|
||||||
header_child_oob = sx_call("oob-header-sx",
|
|
||||||
parent_id="orders-header-child",
|
|
||||||
row=SxExpr(order_row_oob))
|
|
||||||
root_hdr = root_header_sx(ctx, oob=True)
|
|
||||||
oobs = "(<> " + header_child_oob + " " + root_hdr + ")"
|
|
||||||
|
|
||||||
return oob_page_sx(oobs=oobs, filter=filt, content=main)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
0
orders/sxc/__init__.py
Normal file
0
orders/sxc/__init__.py
Normal file
201
orders/sxc/pages/__init__.py
Normal file
201
orders/sxc/pages/__init__.py
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
"""Orders defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def setup_orders_pages() -> None:
|
||||||
|
"""Register orders-specific layouts, page helpers, and load page definitions."""
|
||||||
|
_register_orders_layouts()
|
||||||
|
_register_orders_helpers()
|
||||||
|
_load_orders_page_files()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_orders_page_files() -> None:
|
||||||
|
"""Load defpage definitions from orders/sxc/pages/*.sx."""
|
||||||
|
import os
|
||||||
|
from shared.sx.pages import load_page_dir
|
||||||
|
load_page_dir(os.path.dirname(__file__), "orders")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Layouts
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_orders_layouts() -> None:
|
||||||
|
from shared.sx.layouts import register_custom_layout
|
||||||
|
register_custom_layout("orders", _orders_full, _orders_oob, _orders_mobile)
|
||||||
|
register_custom_layout("order-detail", _order_detail_full, _order_detail_oob, _order_detail_mobile)
|
||||||
|
|
||||||
|
|
||||||
|
def _orders_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, header_child_sx
|
||||||
|
from sx.sx_components import _auth_header_sx, _orders_header_sx
|
||||||
|
|
||||||
|
list_url = kw.get("list_url", "/")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
inner = "(<> " + _auth_header_sx(ctx) + " " + _orders_header_sx(ctx, list_url) + ")"
|
||||||
|
return "(<> " + root_hdr + " " + header_child_sx(inner) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _orders_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _auth_header_sx, _orders_header_sx
|
||||||
|
|
||||||
|
list_url = kw.get("list_url", "/")
|
||||||
|
auth_hdr = _auth_header_sx(ctx, oob=True)
|
||||||
|
auth_child_oob = sx_call("oob-header-sx",
|
||||||
|
parent_id="auth-header-child",
|
||||||
|
row=SxExpr(_orders_header_sx(ctx, list_url)))
|
||||||
|
root_hdr = root_header_sx(ctx, oob=True)
|
||||||
|
return "(<> " + auth_hdr + " " + auth_child_oob + " " + root_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _orders_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
|
||||||
|
return mobile_menu_sx(mobile_root_nav_sx(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
def _order_detail_full(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
from sx.sx_components import _auth_header_sx, _orders_header_sx
|
||||||
|
|
||||||
|
list_url = kw.get("list_url", "/")
|
||||||
|
detail_url = kw.get("detail_url", "/")
|
||||||
|
root_hdr = root_header_sx(ctx)
|
||||||
|
order_row = sx_call(
|
||||||
|
"menu-row-sx",
|
||||||
|
id="order-row", level=3, colour="sky", link_href=detail_url,
|
||||||
|
link_label="Order", icon="fa fa-gbp",
|
||||||
|
)
|
||||||
|
detail_header = sx_call(
|
||||||
|
"order-detail-header-stack",
|
||||||
|
auth=SxExpr(_auth_header_sx(ctx)),
|
||||||
|
orders=SxExpr(_orders_header_sx(ctx, list_url)),
|
||||||
|
order=SxExpr(order_row),
|
||||||
|
)
|
||||||
|
return "(<> " + root_hdr + " " + detail_header + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _order_detail_oob(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import root_header_sx, sx_call, SxExpr
|
||||||
|
|
||||||
|
detail_url = kw.get("detail_url", "/")
|
||||||
|
order_row_oob = sx_call(
|
||||||
|
"menu-row-sx",
|
||||||
|
id="order-row", level=3, colour="sky", link_href=detail_url,
|
||||||
|
link_label="Order", icon="fa fa-gbp", oob=True,
|
||||||
|
)
|
||||||
|
header_child_oob = sx_call("oob-header-sx",
|
||||||
|
parent_id="orders-header-child",
|
||||||
|
row=SxExpr(order_row_oob))
|
||||||
|
root_hdr = root_header_sx(ctx, oob=True)
|
||||||
|
return "(<> " + header_child_oob + " " + root_hdr + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def _order_detail_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
from shared.sx.helpers import mobile_menu_sx, mobile_root_nav_sx
|
||||||
|
return mobile_menu_sx(mobile_root_nav_sx(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Page helpers — Python functions callable from defpage expressions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _register_orders_helpers() -> None:
|
||||||
|
from shared.sx.pages import register_page_helpers
|
||||||
|
|
||||||
|
register_page_helpers("orders", {
|
||||||
|
# Orders list
|
||||||
|
"orders-list-content": _h_orders_list_content,
|
||||||
|
"orders-list-filter": _h_orders_list_filter,
|
||||||
|
"orders-list-aside": _h_orders_list_aside,
|
||||||
|
"orders-list-url": _h_orders_list_url,
|
||||||
|
# Order detail
|
||||||
|
"order-detail-content": _h_order_detail_content,
|
||||||
|
"order-detail-filter": _h_order_detail_filter,
|
||||||
|
"order-detail-url": _h_order_detail_url,
|
||||||
|
"order-list-url-from-detail": _h_order_list_url_from_detail,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def _h_orders_list_content():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "orders_page_data", None)
|
||||||
|
if not d:
|
||||||
|
from shared.sx.helpers import sx_call
|
||||||
|
return sx_call("order-empty-state")
|
||||||
|
from sx.sx_components import _orders_rows_sx, _orders_main_panel_sx
|
||||||
|
rows = _orders_rows_sx(d["orders"], d["page"], d["total_pages"],
|
||||||
|
d["url_for_fn"], d["qs_fn"])
|
||||||
|
return _orders_main_panel_sx(d["orders"], rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _h_orders_list_filter():
|
||||||
|
from quart import g
|
||||||
|
from shared.sx.helpers import sx_call, SxExpr
|
||||||
|
from shared.sx.page import SEARCH_HEADERS_MOBILE
|
||||||
|
d = getattr(g, "orders_page_data", None)
|
||||||
|
search = d.get("search", "") if d else ""
|
||||||
|
search_count = d.get("search_count", "") if d else ""
|
||||||
|
search_mobile = sx_call("search-mobile",
|
||||||
|
current_local_href="/",
|
||||||
|
search=search or "",
|
||||||
|
search_count=search_count or "",
|
||||||
|
hx_select="#main-panel",
|
||||||
|
search_headers_mobile=SEARCH_HEADERS_MOBILE,
|
||||||
|
)
|
||||||
|
return sx_call("order-list-header", search_mobile=SxExpr(search_mobile))
|
||||||
|
|
||||||
|
|
||||||
|
def _h_orders_list_aside():
|
||||||
|
from quart import g
|
||||||
|
from shared.sx.helpers import sx_call
|
||||||
|
from shared.sx.page import SEARCH_HEADERS_DESKTOP
|
||||||
|
d = getattr(g, "orders_page_data", None)
|
||||||
|
search = d.get("search", "") if d else ""
|
||||||
|
search_count = d.get("search_count", "") if d else ""
|
||||||
|
return sx_call("search-desktop",
|
||||||
|
current_local_href="/",
|
||||||
|
search=search or "",
|
||||||
|
search_count=search_count or "",
|
||||||
|
hx_select="#main-panel",
|
||||||
|
search_headers_desktop=SEARCH_HEADERS_DESKTOP,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _h_orders_list_url():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "orders_page_data", None)
|
||||||
|
return d["list_url"] if d else "/"
|
||||||
|
|
||||||
|
|
||||||
|
def _h_order_detail_content():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "order_detail_data", None)
|
||||||
|
if not d:
|
||||||
|
return ""
|
||||||
|
from sx.sx_components import _order_main_sx
|
||||||
|
return _order_main_sx(d["order"], d["calendar_entries"])
|
||||||
|
|
||||||
|
|
||||||
|
def _h_order_detail_filter():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "order_detail_data", None)
|
||||||
|
if not d:
|
||||||
|
return ""
|
||||||
|
from sx.sx_components import _order_filter_sx
|
||||||
|
return _order_filter_sx(d["order"], d["list_url"], d["recheck_url"],
|
||||||
|
d["pay_url"], d["csrf_token"])
|
||||||
|
|
||||||
|
|
||||||
|
def _h_order_detail_url():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "order_detail_data", None)
|
||||||
|
return d["detail_url"] if d else "/"
|
||||||
|
|
||||||
|
|
||||||
|
def _h_order_list_url_from_detail():
|
||||||
|
from quart import g
|
||||||
|
d = getattr(g, "order_detail_data", None)
|
||||||
|
return d["list_url"] if d else "/"
|
||||||
27
orders/sxc/pages/orders.sx
Normal file
27
orders/sxc/pages/orders.sx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
;; Orders app — declarative page definitions
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Orders list
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defpage orders-list
|
||||||
|
:path "/"
|
||||||
|
:auth :public
|
||||||
|
:layout (:orders
|
||||||
|
:list-url (orders-list-url))
|
||||||
|
:filter (orders-list-filter)
|
||||||
|
:aside (orders-list-aside)
|
||||||
|
:content (orders-list-content))
|
||||||
|
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
;; Order detail
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defpage order-detail
|
||||||
|
:path "/<int:order_id>/"
|
||||||
|
:auth :public
|
||||||
|
:layout (:order-detail
|
||||||
|
:list-url (order-list-url-from-detail)
|
||||||
|
:detail-url (order-detail-url))
|
||||||
|
:filter (order-detail-filter)
|
||||||
|
:content (order-detail-content))
|
||||||
@@ -33,7 +33,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .types import Component, HandlerDef, Keyword, Lambda, Macro, NIL, RelationDef, Symbol
|
from .types import Component, HandlerDef, Keyword, Lambda, Macro, NIL, PageDef, RelationDef, Symbol
|
||||||
from .primitives import _PRIMITIVES
|
from .primitives import _PRIMITIVES
|
||||||
|
|
||||||
|
|
||||||
@@ -635,6 +635,85 @@ def _sf_defrelation(expr: list, env: dict) -> RelationDef:
|
|||||||
return defn
|
return defn
|
||||||
|
|
||||||
|
|
||||||
|
def _sf_defpage(expr: list, env: dict) -> PageDef:
|
||||||
|
"""``(defpage name :path "/..." :auth :public :content expr ...)``
|
||||||
|
|
||||||
|
Parses keyword args from the expression. All slot values are stored
|
||||||
|
as unevaluated AST — they are resolved at request time by execute_page().
|
||||||
|
"""
|
||||||
|
if len(expr) < 2:
|
||||||
|
raise EvalError("defpage requires a name")
|
||||||
|
name_sym = expr[1]
|
||||||
|
if not isinstance(name_sym, Symbol):
|
||||||
|
raise EvalError(f"defpage name must be symbol, got {type(name_sym).__name__}")
|
||||||
|
|
||||||
|
# Parse keyword args — values are NOT evaluated (stored as AST)
|
||||||
|
slots: dict[str, Any] = {}
|
||||||
|
i = 2
|
||||||
|
while i < len(expr):
|
||||||
|
key = expr[i]
|
||||||
|
if isinstance(key, Keyword) and i + 1 < len(expr):
|
||||||
|
slots[key.name] = expr[i + 1]
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Required fields
|
||||||
|
path = slots.get("path")
|
||||||
|
if path is None:
|
||||||
|
raise EvalError(f"defpage {name_sym.name} missing required :path")
|
||||||
|
if not isinstance(path, str):
|
||||||
|
raise EvalError(f"defpage {name_sym.name} :path must be a string")
|
||||||
|
|
||||||
|
auth_val = slots.get("auth", "public")
|
||||||
|
if isinstance(auth_val, Keyword):
|
||||||
|
auth: str | list = auth_val.name
|
||||||
|
elif isinstance(auth_val, list):
|
||||||
|
# (:rights "a" "b") → ["rights", "a", "b"]
|
||||||
|
auth = []
|
||||||
|
for item in auth_val:
|
||||||
|
if isinstance(item, Keyword):
|
||||||
|
auth.append(item.name)
|
||||||
|
elif isinstance(item, str):
|
||||||
|
auth.append(item)
|
||||||
|
else:
|
||||||
|
auth.append(_eval(item, env))
|
||||||
|
else:
|
||||||
|
auth = str(auth_val) if auth_val else "public"
|
||||||
|
|
||||||
|
# Layout — keep unevaluated
|
||||||
|
layout = slots.get("layout")
|
||||||
|
if isinstance(layout, Keyword):
|
||||||
|
layout = layout.name
|
||||||
|
elif isinstance(layout, list):
|
||||||
|
# Keep as unevaluated list for execute_page to resolve at request time
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Cache — evaluate if present (it's a static config dict)
|
||||||
|
cache_val = slots.get("cache")
|
||||||
|
cache = None
|
||||||
|
if cache_val is not None:
|
||||||
|
cache_result = _eval(cache_val, env)
|
||||||
|
if isinstance(cache_result, dict):
|
||||||
|
cache = cache_result
|
||||||
|
|
||||||
|
page = PageDef(
|
||||||
|
name=name_sym.name,
|
||||||
|
path=path,
|
||||||
|
auth=auth,
|
||||||
|
layout=layout,
|
||||||
|
cache=cache,
|
||||||
|
data_expr=slots.get("data"),
|
||||||
|
content_expr=slots.get("content"),
|
||||||
|
filter_expr=slots.get("filter"),
|
||||||
|
aside_expr=slots.get("aside"),
|
||||||
|
menu_expr=slots.get("menu"),
|
||||||
|
closure=dict(env),
|
||||||
|
)
|
||||||
|
env[f"page:{name_sym.name}"] = page
|
||||||
|
return page
|
||||||
|
|
||||||
|
|
||||||
_SPECIAL_FORMS: dict[str, Any] = {
|
_SPECIAL_FORMS: dict[str, Any] = {
|
||||||
"if": _sf_if,
|
"if": _sf_if,
|
||||||
"when": _sf_when,
|
"when": _sf_when,
|
||||||
@@ -657,6 +736,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
|
|||||||
"defmacro": _sf_defmacro,
|
"defmacro": _sf_defmacro,
|
||||||
"quasiquote": _sf_quasiquote,
|
"quasiquote": _sf_quasiquote,
|
||||||
"defhandler": _sf_defhandler,
|
"defhandler": _sf_defhandler,
|
||||||
|
"defpage": _sf_defpage,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -102,26 +102,136 @@ def root_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def mobile_nav_sx(ctx: dict) -> str:
|
def mobile_menu_sx(*sections: str) -> str:
|
||||||
"""Build mobile navigation panel from context fragments (nav_tree, auth_menu)."""
|
"""Assemble mobile menu from pre-built sections (deepest first)."""
|
||||||
|
parts = [s for s in sections if s]
|
||||||
|
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||||
|
|
||||||
|
|
||||||
|
def mobile_root_nav_sx(ctx: dict) -> str:
|
||||||
|
"""Root-level mobile nav via ~mobile-root-nav component."""
|
||||||
nav_tree = ctx.get("nav_tree") or ""
|
nav_tree = ctx.get("nav_tree") or ""
|
||||||
auth_menu = ctx.get("auth_menu") or ""
|
auth_menu = ctx.get("auth_menu") or ""
|
||||||
if not nav_tree and not auth_menu:
|
if not nav_tree and not auth_menu:
|
||||||
return ""
|
return ""
|
||||||
|
return sx_call("mobile-root-nav",
|
||||||
|
nav_tree=_as_sx(nav_tree),
|
||||||
|
auth_menu=_as_sx(auth_menu),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared nav-item builders — used by BOTH desktop headers and mobile menus
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _post_nav_items_sx(ctx: dict) -> str:
|
||||||
|
"""Build post-level nav items (container_nav + admin cog). Shared by
|
||||||
|
``post_header_sx`` (desktop) and ``post_mobile_nav_sx`` (mobile)."""
|
||||||
|
post = ctx.get("post") or {}
|
||||||
|
slug = post.get("slug", "")
|
||||||
|
if not slug:
|
||||||
|
return ""
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if nav_tree:
|
page_cart_count = ctx.get("page_cart_count", 0)
|
||||||
nav_tree_sx = _as_sx(nav_tree)
|
if page_cart_count and page_cart_count > 0:
|
||||||
|
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
|
||||||
|
parts.append(sx_call("page-cart-badge", href=cart_href,
|
||||||
|
count=str(page_cart_count)))
|
||||||
|
|
||||||
|
container_nav = str(ctx.get("container_nav") or "").strip()
|
||||||
|
# Skip empty fragment wrappers like "(<> )"
|
||||||
|
if container_nav and container_nav.replace("(<>", "").replace(")", "").strip():
|
||||||
parts.append(
|
parts.append(
|
||||||
f'(div :class "flex flex-col gap-2 p-3 text-sm" {nav_tree_sx})'
|
f'(div :id "entries-calendars-nav-wrapper"'
|
||||||
)
|
f' :class "flex flex-col sm:flex-row sm:items-center gap-2'
|
||||||
if auth_menu:
|
f' border-r border-stone-200 mr-2 sm:max-w-2xl"'
|
||||||
auth_sx = _as_sx(auth_menu)
|
f' {container_nav})'
|
||||||
parts.append(
|
|
||||||
f'(div :class "p-3 border-t border-stone-200" {auth_sx})'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Admin cog
|
||||||
|
admin_nav = ctx.get("post_admin_nav")
|
||||||
|
if not admin_nav:
|
||||||
|
rights = ctx.get("rights") or {}
|
||||||
|
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
||||||
|
if has_admin and slug:
|
||||||
|
from quart import request
|
||||||
|
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||||
|
is_admin_page = ctx.get("is_admin_section") or "/admin" in request.path
|
||||||
|
sel_cls = "!bg-stone-500 !text-white" if is_admin_page else ""
|
||||||
|
base_cls = ("justify-center cursor-pointer flex flex-row"
|
||||||
|
" items-center gap-2 rounded bg-stone-200 text-black p-3")
|
||||||
|
admin_nav = (
|
||||||
|
f'(div :class "relative nav-group"'
|
||||||
|
f' (a :href "{admin_href}"'
|
||||||
|
f' :class "{base_cls} {sel_cls}"'
|
||||||
|
f' (i :class "fa fa-cog" :aria-hidden "true")))'
|
||||||
|
)
|
||||||
|
if admin_nav:
|
||||||
|
parts.append(admin_nav)
|
||||||
return "(<> " + " ".join(parts) + ")" if parts else ""
|
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _post_admin_nav_items_sx(ctx: dict, slug: str,
|
||||||
|
selected: str = "") -> str:
|
||||||
|
"""Build post-admin nav items (calendars, markets, etc.). Shared by
|
||||||
|
``post_admin_header_sx`` (desktop) and mobile menu."""
|
||||||
|
select_colours = ctx.get("select_colours", "")
|
||||||
|
parts: list[str] = []
|
||||||
|
items = [
|
||||||
|
("events_url", f"/{slug}/admin/", "calendars"),
|
||||||
|
("market_url", f"/{slug}/admin/", "markets"),
|
||||||
|
("cart_url", f"/{slug}/admin/payments/", "payments"),
|
||||||
|
("blog_url", f"/{slug}/admin/entries/", "entries"),
|
||||||
|
("blog_url", f"/{slug}/admin/data/", "data"),
|
||||||
|
("blog_url", f"/{slug}/admin/preview/", "preview"),
|
||||||
|
("blog_url", f"/{slug}/admin/edit/", "edit"),
|
||||||
|
("blog_url", f"/{slug}/admin/settings/", "settings"),
|
||||||
|
]
|
||||||
|
for url_key, path, label in items:
|
||||||
|
url_fn = ctx.get(url_key)
|
||||||
|
if not callable(url_fn):
|
||||||
|
continue
|
||||||
|
href = url_fn(path)
|
||||||
|
is_sel = label == selected
|
||||||
|
parts.append(sx_call("nav-link", href=href, label=label,
|
||||||
|
select_colours=select_colours,
|
||||||
|
is_selected=is_sel or None))
|
||||||
|
return "(<> " + " ".join(parts) + ")" if parts else ""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Mobile menu section builders — wrap shared nav items for hamburger panel
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def post_mobile_nav_sx(ctx: dict) -> str:
|
||||||
|
"""Post-level mobile menu section."""
|
||||||
|
nav = _post_nav_items_sx(ctx)
|
||||||
|
if not nav:
|
||||||
|
return ""
|
||||||
|
post = ctx.get("post") or {}
|
||||||
|
slug = post.get("slug", "")
|
||||||
|
title = (post.get("title") or slug)[:40]
|
||||||
|
return sx_call("mobile-menu-section",
|
||||||
|
label=title,
|
||||||
|
href=call_url(ctx, "blog_url", f"/{slug}/"),
|
||||||
|
level=1,
|
||||||
|
items=SxExpr(nav),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def post_admin_mobile_nav_sx(ctx: dict, slug: str,
|
||||||
|
selected: str = "") -> str:
|
||||||
|
"""Post-admin mobile menu section."""
|
||||||
|
nav = _post_admin_nav_items_sx(ctx, slug, selected)
|
||||||
|
if not nav:
|
||||||
|
return ""
|
||||||
|
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
||||||
|
return sx_call("mobile-menu-section",
|
||||||
|
label="admin", href=admin_href, level=2,
|
||||||
|
items=SxExpr(nav),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def search_mobile_sx(ctx: dict) -> str:
|
def search_mobile_sx(ctx: dict) -> str:
|
||||||
"""Build mobile search input as sx call string."""
|
"""Build mobile search input as sx call string."""
|
||||||
return sx_call("search-mobile",
|
return sx_call("search-mobile",
|
||||||
@@ -154,43 +264,7 @@ def post_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
feature_image = post.get("feature_image")
|
feature_image = post.get("feature_image")
|
||||||
|
|
||||||
label_sx = sx_call("post-label", feature_image=feature_image, title=title)
|
label_sx = sx_call("post-label", feature_image=feature_image, title=title)
|
||||||
|
nav_sx = _post_nav_items_sx(ctx) or None
|
||||||
nav_parts: list[str] = []
|
|
||||||
page_cart_count = ctx.get("page_cart_count", 0)
|
|
||||||
if page_cart_count and page_cart_count > 0:
|
|
||||||
cart_href = call_url(ctx, "cart_url", f"/{slug}/")
|
|
||||||
nav_parts.append(sx_call("page-cart-badge", href=cart_href, count=str(page_cart_count)))
|
|
||||||
|
|
||||||
container_nav = ctx.get("container_nav")
|
|
||||||
if container_nav:
|
|
||||||
nav_parts.append(
|
|
||||||
f'(div :id "entries-calendars-nav-wrapper"'
|
|
||||||
f' :class "flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"'
|
|
||||||
f' {container_nav})'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Admin cog
|
|
||||||
admin_nav = ctx.get("post_admin_nav")
|
|
||||||
if not admin_nav:
|
|
||||||
rights = ctx.get("rights") or {}
|
|
||||||
has_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
|
|
||||||
if has_admin and slug:
|
|
||||||
from quart import request
|
|
||||||
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
|
|
||||||
is_admin_page = ctx.get("is_admin_section") or "/admin" in request.path
|
|
||||||
sel_cls = "!bg-stone-500 !text-white" if is_admin_page else ""
|
|
||||||
base_cls = ("justify-center cursor-pointer flex flex-row"
|
|
||||||
" items-center gap-2 rounded bg-stone-200 text-black p-3")
|
|
||||||
admin_nav = (
|
|
||||||
f'(div :class "relative nav-group"'
|
|
||||||
f' (a :href "{admin_href}"'
|
|
||||||
f' :class "{base_cls} {sel_cls}"'
|
|
||||||
f' (i :class "fa fa-cog" :aria-hidden "true")))'
|
|
||||||
)
|
|
||||||
if admin_nav:
|
|
||||||
nav_parts.append(admin_nav)
|
|
||||||
|
|
||||||
nav_sx = "(<> " + " ".join(nav_parts) + ")" if nav_parts else None
|
|
||||||
link_href = call_url(ctx, "blog_url", f"/{slug}/")
|
link_href = call_url(ctx, "blog_url", f"/{slug}/")
|
||||||
|
|
||||||
return sx_call("menu-row-sx",
|
return sx_call("menu-row-sx",
|
||||||
@@ -212,39 +286,7 @@ def post_admin_header_sx(ctx: dict, slug: str, *, oob: bool = False,
|
|||||||
label_parts.append(f'(span :class "text-white" "{escape(selected)}")')
|
label_parts.append(f'(span :class "text-white" "{escape(selected)}")')
|
||||||
label_sx = "(<> " + " ".join(label_parts) + ")"
|
label_sx = "(<> " + " ".join(label_parts) + ")"
|
||||||
|
|
||||||
# Nav items
|
nav_sx = _post_admin_nav_items_sx(ctx, slug, selected) or None
|
||||||
select_colours = ctx.get("select_colours", "")
|
|
||||||
base_cls = ("justify-center cursor-pointer flex flex-row items-center"
|
|
||||||
" gap-2 rounded bg-stone-200 text-black p-3")
|
|
||||||
selected_cls = ("justify-center cursor-pointer flex flex-row items-center"
|
|
||||||
" gap-2 rounded !bg-stone-500 !text-white p-3")
|
|
||||||
nav_parts: list[str] = []
|
|
||||||
items = [
|
|
||||||
("events_url", f"/{slug}/admin/", "calendars"),
|
|
||||||
("market_url", f"/{slug}/admin/", "markets"),
|
|
||||||
("cart_url", f"/{slug}/admin/payments/", "payments"),
|
|
||||||
("blog_url", f"/{slug}/admin/entries/", "entries"),
|
|
||||||
("blog_url", f"/{slug}/admin/data/", "data"),
|
|
||||||
("blog_url", f"/{slug}/admin/preview/", "preview"),
|
|
||||||
("blog_url", f"/{slug}/admin/edit/", "edit"),
|
|
||||||
("blog_url", f"/{slug}/admin/settings/", "settings"),
|
|
||||||
]
|
|
||||||
for url_key, path, label in items:
|
|
||||||
url_fn = ctx.get(url_key)
|
|
||||||
if not callable(url_fn):
|
|
||||||
continue
|
|
||||||
href = url_fn(path)
|
|
||||||
is_sel = label == selected
|
|
||||||
cls = selected_cls if is_sel else base_cls
|
|
||||||
aria = "true" if is_sel else None
|
|
||||||
nav_parts.append(
|
|
||||||
f'(div :class "relative nav-group"'
|
|
||||||
f' (a :href "{escape(href)}"'
|
|
||||||
+ (f' :aria-selected "true"' if aria else "")
|
|
||||||
+ f' :class "{cls} {escape(select_colours)}"'
|
|
||||||
+ f' "{escape(label)}"))'
|
|
||||||
)
|
|
||||||
nav_sx = "(<> " + " ".join(nav_parts) + ")" if nav_parts else None
|
|
||||||
|
|
||||||
if not admin_href:
|
if not admin_href:
|
||||||
blog_fn = ctx.get("blog_url")
|
blog_fn = ctx.get("blog_url")
|
||||||
@@ -301,7 +343,7 @@ def full_page_sx(ctx: dict, *, header_rows: str,
|
|||||||
"""
|
"""
|
||||||
# Auto-generate mobile nav from context when no menu provided
|
# Auto-generate mobile nav from context when no menu provided
|
||||||
if not menu:
|
if not menu:
|
||||||
menu = mobile_nav_sx(ctx)
|
menu = mobile_root_nav_sx(ctx)
|
||||||
body_sx = sx_call("app-body",
|
body_sx = sx_call("app-body",
|
||||||
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
|
header_rows=SxExpr(f"(<> {header_rows})") if header_rows else None,
|
||||||
filter=SxExpr(filter) if filter else None,
|
filter=SxExpr(filter) if filter else None,
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from typing import Any, Callable, Awaitable
|
|||||||
from .helpers import (
|
from .helpers import (
|
||||||
root_header_sx, post_header_sx, post_admin_header_sx,
|
root_header_sx, post_header_sx, post_admin_header_sx,
|
||||||
oob_header_sx, header_child_sx,
|
oob_header_sx, header_child_sx,
|
||||||
|
mobile_menu_sx, mobile_root_nav_sx,
|
||||||
|
post_mobile_nav_sx, post_admin_mobile_nav_sx,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,17 +28,19 @@ from .helpers import (
|
|||||||
class Layout:
|
class Layout:
|
||||||
"""A named layout that generates header rows for full and OOB rendering."""
|
"""A named layout that generates header rows for full and OOB rendering."""
|
||||||
|
|
||||||
__slots__ = ("name", "_full_fn", "_oob_fn")
|
__slots__ = ("name", "_full_fn", "_oob_fn", "_mobile_fn")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
full_fn: Callable[..., str | Awaitable[str]],
|
full_fn: Callable[..., str | Awaitable[str]],
|
||||||
oob_fn: Callable[..., str | Awaitable[str]],
|
oob_fn: Callable[..., str | Awaitable[str]],
|
||||||
|
mobile_fn: Callable[..., str | Awaitable[str]] | None = None,
|
||||||
):
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self._full_fn = full_fn
|
self._full_fn = full_fn
|
||||||
self._oob_fn = oob_fn
|
self._oob_fn = oob_fn
|
||||||
|
self._mobile_fn = mobile_fn
|
||||||
|
|
||||||
async def full_headers(self, ctx: dict, **kwargs: Any) -> str:
|
async def full_headers(self, ctx: dict, **kwargs: Any) -> str:
|
||||||
result = self._full_fn(ctx, **kwargs)
|
result = self._full_fn(ctx, **kwargs)
|
||||||
@@ -50,6 +54,14 @@ class Layout:
|
|||||||
result = await result
|
result = await result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def mobile_menu(self, ctx: dict, **kwargs: Any) -> str:
|
||||||
|
if self._mobile_fn is None:
|
||||||
|
return ""
|
||||||
|
result = self._mobile_fn(ctx, **kwargs)
|
||||||
|
if hasattr(result, "__await__"):
|
||||||
|
result = await result
|
||||||
|
return result
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Layout:{self.name}>"
|
return f"<Layout:{self.name}>"
|
||||||
|
|
||||||
@@ -113,9 +125,27 @@ def _post_admin_oob(ctx: dict, **kw: Any) -> str:
|
|||||||
return "(<> " + post_hdr + " " + admin_oob + ")"
|
return "(<> " + post_hdr + " " + admin_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
register_layout(Layout("root", _root_full, _root_oob))
|
def _root_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
register_layout(Layout("post", _post_full, _post_oob))
|
return mobile_root_nav_sx(ctx)
|
||||||
register_layout(Layout("post-admin", _post_admin_full, _post_admin_oob))
|
|
||||||
|
|
||||||
|
def _post_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
return mobile_menu_sx(post_mobile_nav_sx(ctx), mobile_root_nav_sx(ctx))
|
||||||
|
|
||||||
|
|
||||||
|
def _post_admin_mobile(ctx: dict, **kw: Any) -> str:
|
||||||
|
slug = ctx.get("post", {}).get("slug", "")
|
||||||
|
selected = kw.get("selected", "")
|
||||||
|
return mobile_menu_sx(
|
||||||
|
post_admin_mobile_nav_sx(ctx, slug, selected),
|
||||||
|
post_mobile_nav_sx(ctx),
|
||||||
|
mobile_root_nav_sx(ctx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
register_layout(Layout("root", _root_full, _root_oob, _root_mobile))
|
||||||
|
register_layout(Layout("post", _post_full, _post_oob, _post_mobile))
|
||||||
|
register_layout(Layout("post-admin", _post_admin_full, _post_admin_oob, _post_admin_mobile))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -127,13 +157,15 @@ _CUSTOM_LAYOUTS: dict[str, tuple] = {} # name → (full_fn, oob_fn)
|
|||||||
|
|
||||||
def register_custom_layout(name: str,
|
def register_custom_layout(name: str,
|
||||||
full_fn: Callable[..., str | Awaitable[str]],
|
full_fn: Callable[..., str | Awaitable[str]],
|
||||||
oob_fn: Callable[..., str | Awaitable[str]]) -> None:
|
oob_fn: Callable[..., str | Awaitable[str]],
|
||||||
|
mobile_fn: Callable[..., str | Awaitable[str]] | None = None) -> None:
|
||||||
"""Register a custom layout function.
|
"""Register a custom layout function.
|
||||||
|
|
||||||
Used by services with non-standard header patterns::
|
Used by services with non-standard header patterns::
|
||||||
|
|
||||||
register_custom_layout("sx-section",
|
register_custom_layout("sx-section",
|
||||||
full_fn=my_full_headers,
|
full_fn=my_full_headers,
|
||||||
oob_fn=my_oob_headers)
|
oob_fn=my_oob_headers,
|
||||||
|
mobile_fn=my_mobile_menu)
|
||||||
"""
|
"""
|
||||||
register_layout(Layout(name, full_fn, oob_fn))
|
register_layout(Layout(name, full_fn, oob_fn, mobile_fn))
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ async def execute_page(
|
|||||||
if page_def.menu_expr is not None:
|
if page_def.menu_expr is not None:
|
||||||
menu_sx = await _eval_slot(page_def.menu_expr, env, ctx, async_eval, async_eval_to_sx)
|
menu_sx = await _eval_slot(page_def.menu_expr, env, ctx, async_eval, async_eval_to_sx)
|
||||||
|
|
||||||
# Resolve layout → header rows
|
# Resolve layout → header rows + mobile menu fallback
|
||||||
tctx = await get_template_context()
|
tctx = await get_template_context()
|
||||||
header_rows = ""
|
header_rows = ""
|
||||||
oob_headers = ""
|
oob_headers = ""
|
||||||
@@ -261,6 +261,8 @@ async def execute_page(
|
|||||||
if layout is not None:
|
if layout is not None:
|
||||||
header_rows = await layout.full_headers(tctx, **layout_kwargs)
|
header_rows = await layout.full_headers(tctx, **layout_kwargs)
|
||||||
oob_headers = await layout.oob_headers(tctx, **layout_kwargs)
|
oob_headers = await layout.oob_headers(tctx, **layout_kwargs)
|
||||||
|
if not menu_sx:
|
||||||
|
menu_sx = await layout.mobile_menu(tctx, **layout_kwargs)
|
||||||
|
|
||||||
# Branch on request type
|
# Branch on request type
|
||||||
is_htmx = is_htmx_request()
|
is_htmx = is_htmx_request()
|
||||||
@@ -288,17 +290,22 @@ async def execute_page(
|
|||||||
# Blueprint mounting
|
# Blueprint mounting
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def mount_pages(bp: Any, service_name: str) -> None:
|
def mount_pages(bp: Any, service_name: str,
|
||||||
"""Mount all registered PageDef routes onto a Quart Blueprint.
|
names: set[str] | list[str] | None = None) -> None:
|
||||||
|
"""Mount registered PageDef routes onto a Quart Blueprint.
|
||||||
|
|
||||||
For each PageDef, adds a GET route with appropriate auth/cache
|
For each PageDef, adds a GET route with appropriate auth/cache
|
||||||
decorators. Coexists with existing Python routes on the same blueprint.
|
decorators. Coexists with existing Python routes on the same blueprint.
|
||||||
|
|
||||||
|
If *names* is given, only mount pages whose name is in the set.
|
||||||
"""
|
"""
|
||||||
from quart import make_response
|
from quart import make_response
|
||||||
|
|
||||||
pages = get_all_pages(service_name)
|
pages = get_all_pages(service_name)
|
||||||
|
|
||||||
for page_def in pages.values():
|
for page_def in pages.values():
|
||||||
|
if names is not None and page_def.name not in names:
|
||||||
|
continue
|
||||||
_mount_one_page(bp, service_name, page_def)
|
_mount_one_page(bp, service_name, page_def)
|
||||||
|
|
||||||
|
|
||||||
@@ -347,6 +354,9 @@ def _apply_auth(fn: Any, auth: str | list) -> Any:
|
|||||||
if auth == "admin":
|
if auth == "admin":
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
return require_admin(fn)
|
return require_admin(fn)
|
||||||
|
if auth == "post_author":
|
||||||
|
from shared.browser.app.authz import require_post_author
|
||||||
|
return require_post_author(fn)
|
||||||
if isinstance(auth, list) and auth and auth[0] == "rights":
|
if isinstance(auth, list) and auth and auth[0] == "rights":
|
||||||
from shared.browser.app.authz import require_rights
|
from shared.browser.app.authz import require_rights
|
||||||
return require_rights(*auth[1:])(fn)
|
return require_rights(*auth[1:])(fn)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user