Rebrand sexp → sx across web platform (173 files)

Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 11:06:57 +00:00
parent 17cebe07e7
commit e8bc228c7f
174 changed files with 3126 additions and 2952 deletions

View File

@@ -0,0 +1,177 @@
# Sexp Fragment Protocol: Component Defs Between Services
## Context
Fragment endpoints return raw sexp source (e.g., `(~blog-nav-wrapper :items ...)`). The consuming service embeds this in its page sexp, which the client evaluates. But blog-specific components like `~blog-nav-wrapper` are only in blog's `_COMPONENT_ENV` — not in market's. So market's `client_components_tag()` never sends them to the client, causing "Unknown component" errors.
The fix: transfer component definitions alongside fragments. Services tell the provider what they already have; the provider sends only what's missing. The consuming service registers received defs into its `_COMPONENT_ENV` so they're included in `client_components_tag()` output for the client.
## Approach: Structured Sexp Request/Response
Replace the current GET + `X-Fragment-Request` header protocol with POST + sexp body. This aligns with the vision in `docs/sexpr-internal-protocol-first.md`.
### Request format (POST body)
```scheme
(fragment-request
:type "nav-tree"
:params (:app-name "market" :path "/")
:components (~blog-nav-wrapper ~blog-nav-item-link ~header-row-sx ...))
```
`:components` lists component names already in the consumer's `_COMPONENT_ENV`. Provider skips these.
### Response format
```scheme
(fragment-response
:defs ((defcomp ~blog-nav-wrapper (&key ...) ...) (defcomp ~blog-nav-item-link ...))
:content (~blog-nav-wrapper :items ...))
```
`:defs` contains only components the consumer doesn't have. `:content` is the fragment sexp (same as current response body).
## Changes
### 1. `shared/infrastructure/fragments.py` — Client side
**`fetch_fragment()`**: Switch from GET to POST with sexp body.
- Build request body using `sexp_call`:
```python
from shared.sexp.helpers import sexp_call, SexpExpr
from shared.sexp.jinja_bridge import _COMPONENT_ENV
comp_names = [k for k in _COMPONENT_ENV if k.startswith("~")]
body = sexp_call("fragment-request",
type=fragment_type,
params=params or {},
components=SexpExpr("(" + " ".join(comp_names) + ")"))
```
- POST to same URL, body as `text/sexp`, keep `X-Fragment-Request` header for backward compat
- Parse response: extract `:defs` and `:content` from the sexp response
- Register defs into `_COMPONENT_ENV` via `register_components()`
- Return `:content` wrapped as `SexpExpr`
**New helper `_parse_fragment_response(text)`**:
- `parse()` the response sexp
- Extract keyword args (reuse the keyword-extraction pattern from `evaluator.py`)
- Return `(defs_source, content_source)` tuple
### 2. `shared/sexp/helpers.py` — Response builder
**New `fragment_response(content, request_text)`**:
```python
def fragment_response(content: str, request_text: str) -> str:
"""Build a structured fragment response with missing component defs."""
from .parser import parse, serialize
from .types import Keyword, Component
from .jinja_bridge import _COMPONENT_ENV
# Parse request to get :components list
req = parse(request_text)
loaded = set()
# extract :components keyword value
...
# Diff against _COMPONENT_ENV, serialize missing defs
defs_parts = []
for key, val in _COMPONENT_ENV.items():
if not isinstance(val, Component):
continue
if key in loaded or f"~{val.name}" in loaded:
continue
defs_parts.append(_serialize_defcomp(val))
defs_sexp = "(" + " ".join(defs_parts) + ")" if defs_parts else "nil"
return sexp_call("fragment-response",
defs=SexpExpr(defs_sexp),
content=SexpExpr(content))
```
### 3. Fragment endpoints — All services
**Generic change in each `bp/fragments/routes.py`**: Update the route handler to accept POST, read sexp body, use `fragment_response()` for the response.
The `get_fragment` handler becomes:
```python
@bp.route("/<fragment_type>", methods=["GET", "POST"])
async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type)
if handler is None:
return Response("", status=200, content_type="text/sexp")
content = await handler()
# Structured sexp protocol (POST with sexp body)
request_body = await request.get_data(as_text=True)
if request_body and request.content_type == "text/sexp":
from shared.sexp.helpers import fragment_response
body = fragment_response(content, request_body)
return Response(body, status=200, content_type="text/sexp")
# Legacy GET fallback
return Response(content, status=200, content_type="text/sexp")
```
Since all fragment endpoints follow the identical `_handlers` + `get_fragment` pattern, we can extract this into a shared helper in `fragments.py` or a new `shared/infrastructure/fragment_endpoint.py`.
### 4. Extract shared fragment endpoint helper
To avoid touching every service's fragment routes, create a shared blueprint factory:
**`shared/infrastructure/fragment_endpoint.py`**:
```python
def create_fragment_blueprint(handlers: dict) -> Blueprint:
"""Create a fragment endpoint blueprint with sexp protocol support."""
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
@bp.before_request
async def _require_fragment_header():
if not request.headers.get(FRAGMENT_HEADER):
return Response("", status=403)
@bp.route("/<fragment_type>", methods=["GET", "POST"])
async def get_fragment(fragment_type: str):
handler = handlers.get(fragment_type)
if handler is None:
return Response("", status=200, content_type="text/sexp")
content = await handler()
# Sexp protocol: POST with structured request/response
if request.method == "POST" and request.content_type == "text/sexp":
request_body = await request.get_data(as_text=True)
from shared.sexp.helpers import fragment_response
body = fragment_response(content, request_body)
return Response(body, status=200, content_type="text/sexp")
return Response(content, status=200, content_type="text/sexp")
return bp
```
Then each service's `register()` just returns `create_fragment_blueprint(_handlers)`. This is a small refactor since they all duplicate the same boilerplate today.
## Files to modify
| File | Change |
|------|--------|
| `shared/infrastructure/fragments.py` | POST sexp body, parse response, register defs |
| `shared/sexp/helpers.py` | `fragment_response()` builder, `_serialize_defcomp()` |
| `shared/infrastructure/fragment_endpoint.py` | **New** — shared blueprint factory |
| `blog/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `market/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `events/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `cart/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `account/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `orders/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `federation/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
| `relations/bp/fragments/routes.py` | Use `create_fragment_blueprint` |
## Verification
1. Start blog + market services: `./dev.sh blog market`
2. Load market page — should fetch nav-tree from blog with sexp protocol
3. Check market logs: no "Unknown component" errors
4. Inspect page source: `client_components_tag()` output includes `~blog-nav-wrapper` etc.
5. Cross-domain sx-get navigation (blog → market) works without reload
6. Run sexp tests: `python3 -m pytest shared/sexp/tests/ -x -q`
7. Second page load: `:components` list in request includes blog nav components, response `:defs` is empty

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from pathlib import Path from pathlib import Path
from quart import g, request from quart import g, request

View File

@@ -18,7 +18,7 @@ from shared.models import UserNewsletter
from shared.models.ghost_membership_entities import GhostNewsletter from shared.models.ghost_membership_entities import GhostNewsletter
from shared.infrastructure.urls import login_url 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.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
oob = { oob = {
"oob_extends": "oob_elements.html", "oob_extends": "oob_elements.html",
@@ -47,8 +47,8 @@ def register(url_prefix="/"):
@account_bp.get("/") @account_bp.get("/")
async def account(): async def account():
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_account_page, render_account_oob from sx.sx_components import render_account_page, render_account_oob
if not g.get("user"): if not g.get("user"):
return redirect(login_url("/")) return redirect(login_url("/"))
@@ -58,8 +58,8 @@ def register(url_prefix="/"):
html = await render_account_page(ctx) html = await render_account_page(ctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_account_oob(ctx) sx_src = await render_account_oob(ctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@account_bp.get("/newsletters/") @account_bp.get("/newsletters/")
async def newsletters(): async def newsletters():
@@ -89,16 +89,16 @@ def register(url_prefix="/"):
"subscribed": un.subscribed if un else False, "subscribed": un.subscribed if un else False,
}) })
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_newsletters_page, render_newsletters_oob from sx.sx_components import render_newsletters_page, render_newsletters_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_newsletters_page(ctx, newsletter_list) html = await render_newsletters_page(ctx, newsletter_list)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_newsletters_oob(ctx, newsletter_list) sx_src = await render_newsletters_oob(ctx, newsletter_list)
return sexp_response(sexp_src) return sx_response(sx_src)
@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):
@@ -125,8 +125,8 @@ def register(url_prefix="/"):
await g.s.flush() await g.s.flush()
from sexp.sexp_components import render_newsletter_toggle from sx.sx_components import render_newsletter_toggle
return sexp_response(render_newsletter_toggle(un)) return sx_response(render_newsletter_toggle(un))
# Catch-all for fragment-provided pages — must be last # Catch-all for fragment-provided pages — must be last
@account_bp.get("/<slug>/") @account_bp.get("/<slug>/")
@@ -144,15 +144,15 @@ def register(url_prefix="/"):
if not fragment_html: if not fragment_html:
abort(404) abort(404)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_fragment_page, render_fragment_oob from sx.sx_components import render_fragment_page, render_fragment_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_fragment_page(ctx, fragment_html) html = await render_fragment_page(ctx, fragment_html)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_fragment_oob(ctx, fragment_html) sx_src = await render_fragment_oob(ctx, fragment_html)
return sexp_response(sexp_src) return sx_response(sx_src)
return account_bp return account_bp

View File

@@ -275,8 +275,8 @@ def register(url_prefix="/auth"):
redirect_url = pop_login_redirect_target() redirect_url = pop_login_redirect_target()
return redirect(redirect_url) return redirect(redirect_url)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_login_page(ctx) return await render_login_page(ctx)
@@ -291,8 +291,8 @@ def register(url_prefix="/auth"):
is_valid, email = validate_email(email_input) is_valid, email = validate_email(email_input)
if not is_valid: if not is_valid:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error="Please enter a valid email address.", email=email_input) ctx = await get_template_context(error="Please enter a valid email address.", email=email_input)
return await render_login_page(ctx), 400 return await render_login_page(ctx), 400
@@ -301,8 +301,8 @@ def register(url_prefix="/auth"):
try: try:
allowed, _ = await _check_rate_limit(f"magic_email:{email}", 5, 900) allowed, _ = await _check_rate_limit(f"magic_email:{email}", 5, 900)
if not allowed: if not allowed:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_check_email_page from sx.sx_components import render_check_email_page
ctx = await get_template_context(email=email, email_error=None) ctx = await get_template_context(email=email, email_error=None)
return await render_check_email_page(ctx), 200 return await render_check_email_page(ctx), 200
except Exception: except Exception:
@@ -324,8 +324,8 @@ def register(url_prefix="/auth"):
"Please try again in a moment." "Please try again in a moment."
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_check_email_page from sx.sx_components import render_check_email_page
ctx = await get_template_context(email=email, email_error=email_error) ctx = await get_template_context(email=email, email_error=email_error)
return await render_check_email_page(ctx) return await render_check_email_page(ctx)
@@ -340,15 +340,15 @@ def register(url_prefix="/auth"):
user, error = await validate_magic_link(s, token) user, error = await validate_magic_link(s, token)
if error: if error:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error=error) ctx = await get_template_context(error=error)
return await render_login_page(ctx), 400 return await render_login_page(ctx), 400
user_id = user.id user_id = user.id
except Exception: except Exception:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error="Could not sign you in right now. Please try again.") ctx = await get_template_context(error="Could not sign you in right now. Please try again.")
return await render_login_page(ctx), 502 return await render_login_page(ctx), 502
@@ -679,8 +679,8 @@ def register(url_prefix="/auth"):
@auth_bp.get("/device/") @auth_bp.get("/device/")
async def device_form(): async def device_form():
"""Browser form where user enters the code displayed in terminal.""" """Browser form where user enters the code displayed in terminal."""
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_page from sx.sx_components import render_device_page
code = request.args.get("code", "") code = request.args.get("code", "")
ctx = await get_template_context(code=code) ctx = await get_template_context(code=code)
return await render_device_page(ctx) return await render_device_page(ctx)
@@ -693,8 +693,8 @@ def register(url_prefix="/auth"):
user_code = (form.get("code") or "").strip().replace("-", "").upper() user_code = (form.get("code") or "").strip().replace("-", "").upper()
if not user_code or len(user_code) != 8: if not user_code or len(user_code) != 8:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_page from sx.sx_components import render_device_page
ctx = await get_template_context(error="Please enter a valid 8-character code.", code=form.get("code", "")) ctx = await get_template_context(error="Please enter a valid 8-character code.", code=form.get("code", ""))
return await render_device_page(ctx), 400 return await render_device_page(ctx), 400
@@ -703,8 +703,8 @@ def register(url_prefix="/auth"):
r = await get_auth_redis() r = await get_auth_redis()
device_code = await r.get(f"devflow_uc:{user_code}") device_code = await r.get(f"devflow_uc:{user_code}")
if not device_code: if not device_code:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_page from sx.sx_components import render_device_page
ctx = await get_template_context(error="Code not found or expired. Please try again.", code=form.get("code", "")) ctx = await get_template_context(error="Code not found or expired. Please try again.", code=form.get("code", ""))
return await render_device_page(ctx), 400 return await render_device_page(ctx), 400
@@ -720,13 +720,13 @@ def register(url_prefix="/auth"):
# Logged in — approve immediately # Logged in — approve immediately
ok = await _approve_device(device_code, g.user) ok = await _approve_device(device_code, g.user)
if not ok: if not ok:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_page from sx.sx_components import render_device_page
ctx = await get_template_context(error="Code expired or already used.") ctx = await get_template_context(error="Code expired or already used.")
return await render_device_page(ctx), 400 return await render_device_page(ctx), 400
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_approved_page from sx.sx_components import render_device_approved_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_device_approved_page(ctx) return await render_device_approved_page(ctx)
@@ -734,8 +734,8 @@ def register(url_prefix="/auth"):
@auth_bp.get("/device/complete/") @auth_bp.get("/device/complete/")
async def device_complete(): async def device_complete():
"""Post-login redirect — completes approval after magic link auth.""" """Post-login redirect — completes approval after magic link auth."""
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_device_page, render_device_approved_page from sx.sx_components import render_device_page, render_device_approved_page
device_code = request.args.get("code", "") device_code = request.args.get("code", "")

View File

@@ -1,6 +1,6 @@
"""Account app fragment endpoints. """Account app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
Fragments: Fragments:
@@ -18,15 +18,15 @@ def register():
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
# --------------------------------------------------------------- # ---------------------------------------------------------------
# Fragment handlers — return sexp source text # Fragment handlers — return sx source text
# --------------------------------------------------------------- # ---------------------------------------------------------------
async def _auth_menu(): async def _auth_menu():
from shared.infrastructure.urls import account_url from shared.infrastructure.urls import account_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
user_email = request.args.get("email", "") user_email = request.args.get("email", "")
return sexp_call("auth-menu", return sx_call("auth-menu",
user_email=user_email or None, user_email=user_email or None,
account_url=account_url("")) account_url=account_url(""))
@@ -47,8 +47,8 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
src = await handler() src = await handler()
return Response(src, status=200, content_type="text/sexp") return Response(src, status=200, content_type="text/sx")
return bp return bp

View File

@@ -9,13 +9,13 @@ from __future__ import annotations
import os import os
from typing import Any from typing import Any
from shared.sexp.jinja_bridge import load_service_components from shared.sx.jinja_bridge import load_service_components
from shared.sexp.helpers import ( from shared.sx.helpers import (
call_url, sexp_call, SexpExpr, call_url, sx_call, SxExpr,
root_header_sexp, full_page_sexp, header_child_sexp, oob_page_sexp, root_header_sx, full_page_sx, header_child_sx, oob_page_sx,
) )
# Load account-specific .sexpr components at import time # Load account-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__))) load_service_components(os.path.dirname(os.path.dirname(__file__)))
@@ -23,10 +23,10 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
# Header helpers # Header helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _auth_nav_sexp(ctx: dict) -> str: def _auth_nav_sx(ctx: dict) -> str:
"""Auth section desktop nav items.""" """Auth section desktop nav items."""
parts = [ parts = [
sexp_call("nav-link", sx_call("nav-link",
href=call_url(ctx, "account_url", "/newsletters/"), href=call_url(ctx, "account_url", "/newsletters/"),
label="newsletters", label="newsletters",
select_colours=ctx.get("select_colours", ""), select_colours=ctx.get("select_colours", ""),
@@ -38,22 +38,22 @@ def _auth_nav_sexp(ctx: dict) -> str:
return "(<> " + " ".join(parts) + ")" return "(<> " + " ".join(parts) + ")"
def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the account section header row.""" """Build the account section header row."""
return sexp_call( return sx_call(
"menu-row-sx", "menu-row-sx",
id="auth-row", level=1, colour="sky", id="auth-row", level=1, colour="sky",
link_href=call_url(ctx, "account_url", "/"), link_href=call_url(ctx, "account_url", "/"),
link_label="account", icon="fa-solid fa-user", link_label="account", icon="fa-solid fa-user",
nav=SexpExpr(_auth_nav_sexp(ctx)), nav=SxExpr(_auth_nav_sx(ctx)),
child_id="auth-header-child", oob=oob, child_id="auth-header-child", oob=oob,
) )
def _auth_nav_mobile_sexp(ctx: dict) -> str: def _auth_nav_mobile_sx(ctx: dict) -> str:
"""Mobile nav menu for auth section.""" """Mobile nav menu for auth section."""
parts = [ parts = [
sexp_call("nav-link", sx_call("nav-link",
href=call_url(ctx, "account_url", "/newsletters/"), href=call_url(ctx, "account_url", "/newsletters/"),
label="newsletters", label="newsletters",
select_colours=ctx.get("select_colours", ""), select_colours=ctx.get("select_colours", ""),
@@ -69,7 +69,7 @@ def _auth_nav_mobile_sexp(ctx: dict) -> str:
# Account dashboard (GET /) # Account dashboard (GET /)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _account_main_panel_sexp(ctx: dict) -> str: def _account_main_panel_sx(ctx: dict) -> str:
"""Account info panel with user details and logout.""" """Account info panel with user details and logout."""
from quart import g from quart import g
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
@@ -77,33 +77,33 @@ def _account_main_panel_sexp(ctx: dict) -> str:
user = getattr(g, "user", None) user = getattr(g, "user", None)
error = ctx.get("error", "") error = ctx.get("error", "")
error_sexp = sexp_call("account-error-banner", error=error) if error else "" error_sx = sx_call("account-error-banner", error=error) if error else ""
user_email_sexp = "" user_email_sx = ""
user_name_sexp = "" user_name_sx = ""
if user: if user:
user_email_sexp = sexp_call("account-user-email", email=user.email) user_email_sx = sx_call("account-user-email", email=user.email)
if user.name: if user.name:
user_name_sexp = sexp_call("account-user-name", name=user.name) user_name_sx = sx_call("account-user-name", name=user.name)
logout_sexp = sexp_call("account-logout-form", csrf_token=generate_csrf_token()) logout_sx = sx_call("account-logout-form", csrf_token=generate_csrf_token())
labels_sexp = "" labels_sx = ""
if user and hasattr(user, "labels") and user.labels: if user and hasattr(user, "labels") and user.labels:
label_items = " ".join( label_items = " ".join(
sexp_call("account-label-item", name=label.name) sx_call("account-label-item", name=label.name)
for label in user.labels for label in user.labels
) )
labels_sexp = sexp_call("account-labels-section", labels_sx = sx_call("account-labels-section",
items=SexpExpr("(<> " + label_items + ")")) items=SxExpr("(<> " + label_items + ")"))
return sexp_call( return sx_call(
"account-main-panel", "account-main-panel",
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
email=SexpExpr(user_email_sexp) if user_email_sexp else None, email=SxExpr(user_email_sx) if user_email_sx else None,
name=SexpExpr(user_name_sexp) if user_name_sexp else None, name=SxExpr(user_name_sx) if user_name_sx else None,
logout=SexpExpr(logout_sexp), logout=SxExpr(logout_sx),
labels=SexpExpr(labels_sexp) if labels_sexp else None, labels=SxExpr(labels_sx) if labels_sx else None,
) )
@@ -111,7 +111,7 @@ def _account_main_panel_sexp(ctx: dict) -> str:
# Newsletters (GET /newsletters/) # Newsletters (GET /newsletters/)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> str: def _newsletter_toggle_sx(un: Any, account_url_fn: Any, csrf_token: str) -> str:
"""Render a single newsletter toggle switch.""" """Render a single newsletter toggle switch."""
nid = un.newsletter_id nid = un.newsletter_id
toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/") toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/")
@@ -123,7 +123,7 @@ def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> st
bg = "bg-stone-300" bg = "bg-stone-300"
translate = "translate-x-1" translate = "translate-x-1"
checked = "false" checked = "false"
return sexp_call( return sx_call(
"account-newsletter-toggle", "account-newsletter-toggle",
id=f"nl-{nid}", url=toggle_url, id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
@@ -134,9 +134,9 @@ def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> st
) )
def _newsletter_toggle_off_sexp(nid: int, toggle_url: str, csrf_token: str) -> str: def _newsletter_toggle_off_sx(nid: int, toggle_url: str, csrf_token: str) -> str:
"""Render an unsubscribed newsletter toggle (no subscription record yet).""" """Render an unsubscribed newsletter toggle (no subscription record yet)."""
return sexp_call( return sx_call(
"account-newsletter-toggle-off", "account-newsletter-toggle-off",
id=f"nl-{nid}", url=toggle_url, id=f"nl-{nid}", url=toggle_url,
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
@@ -144,7 +144,7 @@ def _newsletter_toggle_off_sexp(nid: int, toggle_url: str, csrf_token: str) -> s
) )
def _newsletters_panel_sexp(ctx: dict, newsletter_list: list) -> str: def _newsletters_panel_sx(ctx: dict, newsletter_list: list) -> str:
"""Newsletters management panel.""" """Newsletters management panel."""
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
@@ -157,30 +157,30 @@ def _newsletters_panel_sexp(ctx: dict, newsletter_list: list) -> str:
nl = item["newsletter"] nl = item["newsletter"]
un = item.get("un") un = item.get("un")
desc_sexp = sexp_call( desc_sx = sx_call(
"account-newsletter-desc", description=nl.description "account-newsletter-desc", description=nl.description
) if nl.description else "" ) if nl.description else ""
if un: if un:
toggle = _newsletter_toggle_sexp(un, account_url_fn, csrf) toggle = _newsletter_toggle_sx(un, account_url_fn, csrf)
else: else:
toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/") toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/")
toggle = _newsletter_toggle_off_sexp(nl.id, toggle_url, csrf) toggle = _newsletter_toggle_off_sx(nl.id, toggle_url, csrf)
items.append(sexp_call( items.append(sx_call(
"account-newsletter-item", "account-newsletter-item",
name=nl.name, name=nl.name,
desc=SexpExpr(desc_sexp) if desc_sexp else None, desc=SxExpr(desc_sx) if desc_sx else None,
toggle=SexpExpr(toggle), toggle=SxExpr(toggle),
)) ))
list_sexp = sexp_call( list_sx = sx_call(
"account-newsletter-list", "account-newsletter-list",
items=SexpExpr("(<> " + " ".join(items) + ")"), items=SxExpr("(<> " + " ".join(items) + ")"),
) )
else: else:
list_sexp = sexp_call("account-newsletter-empty") list_sx = sx_call("account-newsletter-empty")
return sexp_call("account-newsletters-panel", list=SexpExpr(list_sexp)) return sx_call("account-newsletters-panel", list=SxExpr(list_sx))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -196,11 +196,11 @@ def _login_page_content(ctx: dict) -> str:
email = ctx.get("email", "") email = ctx.get("email", "")
action = url_for("auth.start_login") action = url_for("auth.start_login")
error_sexp = sexp_call("account-login-error", error=error) if error else "" error_sx = sx_call("account-login-error", error=error) if error else ""
return sexp_call( return sx_call(
"account-login-form", "account-login-form",
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
action=action, action=action,
csrf_token=generate_csrf_token(), email=email, csrf_token=generate_csrf_token(), email=email,
) )
@@ -215,11 +215,11 @@ def _device_page_content(ctx: dict) -> str:
code = ctx.get("code", "") code = ctx.get("code", "")
action = url_for("auth.device_submit") action = url_for("auth.device_submit")
error_sexp = sexp_call("account-device-error", error=error) if error else "" error_sx = sx_call("account-device-error", error=error) if error else ""
return sexp_call( return sx_call(
"account-device-form", "account-device-form",
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
action=action, action=action,
csrf_token=generate_csrf_token(), code=code, csrf_token=generate_csrf_token(), code=code,
) )
@@ -227,7 +227,7 @@ def _device_page_content(ctx: dict) -> str:
def _device_approved_content() -> str: def _device_approved_content() -> str:
"""Device approved success content.""" """Device approved success content."""
return sexp_call("account-device-approved") return sx_call("account-device-approved")
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -236,26 +236,26 @@ def _device_approved_content() -> str:
async def render_account_page(ctx: dict) -> str: async def render_account_page(ctx: dict) -> str:
"""Full page: account dashboard.""" """Full page: account dashboard."""
main = _account_main_panel_sexp(ctx) main = _account_main_panel_sx(ctx)
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
hdr_child = header_child_sexp(_auth_header_sexp(ctx)) hdr_child = header_child_sx(_auth_header_sx(ctx))
header_rows = "(<> " + hdr + " " + hdr_child + ")" header_rows = "(<> " + hdr + " " + hdr_child + ")"
return full_page_sexp(ctx, header_rows=header_rows, return full_page_sx(ctx, header_rows=header_rows,
content=main, content=main,
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
async def render_account_oob(ctx: dict) -> str: async def render_account_oob(ctx: dict) -> str:
"""OOB response for account dashboard.""" """OOB response for account dashboard."""
main = _account_main_panel_sexp(ctx) main = _account_main_panel_sx(ctx)
oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
return oob_page_sexp(oobs=oobs, return oob_page_sx(oobs=oobs,
content=main, content=main,
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -264,26 +264,26 @@ async def render_account_oob(ctx: dict) -> str:
async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str: async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str:
"""Full page: newsletters.""" """Full page: newsletters."""
main = _newsletters_panel_sexp(ctx, newsletter_list) main = _newsletters_panel_sx(ctx, newsletter_list)
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
hdr_child = header_child_sexp(_auth_header_sexp(ctx)) hdr_child = header_child_sx(_auth_header_sx(ctx))
header_rows = "(<> " + hdr + " " + hdr_child + ")" header_rows = "(<> " + hdr + " " + hdr_child + ")"
return full_page_sexp(ctx, header_rows=header_rows, return full_page_sx(ctx, header_rows=header_rows,
content=main, content=main,
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str: async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str:
"""OOB response for newsletters.""" """OOB response for newsletters."""
main = _newsletters_panel_sexp(ctx, newsletter_list) main = _newsletters_panel_sx(ctx, newsletter_list)
oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
return oob_page_sexp(oobs=oobs, return oob_page_sx(oobs=oobs,
content=main, content=main,
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -292,22 +292,22 @@ async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str:
async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str: async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str:
"""Full page: fragment-provided content.""" """Full page: fragment-provided content."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
hdr_child = header_child_sexp(_auth_header_sexp(ctx)) hdr_child = header_child_sx(_auth_header_sx(ctx))
header_rows = "(<> " + hdr + " " + hdr_child + ")" header_rows = "(<> " + hdr + " " + hdr_child + ")"
return full_page_sexp(ctx, header_rows=header_rows, return full_page_sx(ctx, header_rows=header_rows,
content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")',
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str: async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str:
"""OOB response for fragment pages.""" """OOB response for fragment pages."""
oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" oobs = "(<> " + _auth_header_sx(ctx, oob=True) + " " + root_header_sx(ctx, oob=True) + ")"
return oob_page_sexp(oobs=oobs, return oob_page_sx(oobs=oobs,
content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', content=f'(~rich-text :html "{_sx_escape(page_fragment_html)}")',
menu=_auth_nav_mobile_sexp(ctx)) menu=_auth_nav_mobile_sx(ctx))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -316,24 +316,24 @@ async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str:
async def render_login_page(ctx: dict) -> str: async def render_login_page(ctx: dict) -> str:
"""Full page: login form.""" """Full page: login form."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr, return full_page_sx(ctx, header_rows=hdr,
content=_login_page_content(ctx), content=_login_page_content(ctx),
meta_html='<title>Login \u2014 Rose Ash</title>') meta_html='<title>Login \u2014 Rose Ash</title>')
async def render_device_page(ctx: dict) -> str: async def render_device_page(ctx: dict) -> str:
"""Full page: device authorization form.""" """Full page: device authorization form."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr, return full_page_sx(ctx, header_rows=hdr,
content=_device_page_content(ctx), content=_device_page_content(ctx),
meta_html='<title>Authorize Device \u2014 Rose Ash</title>') meta_html='<title>Authorize Device \u2014 Rose Ash</title>')
async def render_device_approved_page(ctx: dict) -> str: async def render_device_approved_page(ctx: dict) -> str:
"""Full page: device approved.""" """Full page: device approved."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr, return full_page_sx(ctx, header_rows=hdr,
content=_device_approved_content(), content=_device_approved_content(),
meta_html='<title>Device Authorized \u2014 Rose Ash</title>') meta_html='<title>Device Authorized \u2014 Rose Ash</title>')
@@ -346,14 +346,14 @@ def _check_email_content(email: str, email_error: str | None = None) -> str:
"""Check email confirmation content.""" """Check email confirmation content."""
from markupsafe import escape from markupsafe import escape
error_sexp = sexp_call( error_sx = sx_call(
"account-check-email-error", error=str(escape(email_error)) "account-check-email-error", error=str(escape(email_error))
) if email_error else "" ) if email_error else ""
return sexp_call( return sx_call(
"account-check-email", "account-check-email",
email=str(escape(email)), email=str(escape(email)),
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
) )
@@ -361,8 +361,8 @@ async def render_check_email_page(ctx: dict) -> str:
"""Full page: check email after magic link sent.""" """Full page: check email after magic link sent."""
email = ctx.get("email", "") email = ctx.get("email", "")
email_error = ctx.get("email_error") email_error = ctx.get("email_error")
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr, return full_page_sx(ctx, header_rows=hdr,
content=_check_email_content(email, email_error), content=_check_email_content(email, email_error),
meta_html='<title>Check your email \u2014 Rose Ash</title>') meta_html='<title>Check your email \u2014 Rose Ash</title>')
@@ -380,13 +380,13 @@ def render_newsletter_toggle(un) -> str:
if account_url_fn is None: if account_url_fn is None:
from shared.infrastructure.urls import account_url from shared.infrastructure.urls import account_url
account_url_fn = account_url account_url_fn = account_url
return _newsletter_toggle_sexp(un, account_url_fn, generate_csrf_token()) return _newsletter_toggle_sx(un, account_url_fn, generate_csrf_token())
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Internal helpers # Internal helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _sexp_escape(s: str) -> str: def _sx_escape(s: str) -> str:
"""Escape a string for embedding in sexp string literals.""" """Escape a string for embedding in sx string literals."""
return s.replace("\\", "\\\\").replace('"', '\\"') return s.replace("\\", "\\\\").replace('"', '\\"')

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from pathlib import Path from pathlib import Path
from quart import g, request from quart import g, request

View File

@@ -14,7 +14,7 @@ from quart import (
from shared.browser.app.redis_cacher import clear_all_cache from shared.browser.app.redis_cacher import clear_all_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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from shared.config import config from shared.config import config
from datetime import datetime from datetime import datetime
@@ -30,30 +30,30 @@ def register(url_prefix):
@bp.get("/") @bp.get("/")
@require_admin @require_admin
async def home(): async def home():
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_settings_page, render_settings_oob from sx.sx_components import render_settings_page, render_settings_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_settings_page(tctx) html = await render_settings_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_settings_oob(tctx) sx_src = await render_settings_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/cache/") @bp.get("/cache/")
@require_admin @require_admin
async def cache(): async def cache():
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_cache_page, render_cache_oob from sx.sx_components import render_cache_page, render_cache_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_cache_page(tctx) html = await render_cache_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_cache_oob(tctx) sx_src = await render_cache_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/cache_clear/") @bp.post("/cache_clear/")
@require_admin @require_admin
@@ -61,9 +61,9 @@ def register(url_prefix):
await clear_all_cache() await clear_all_cache()
if is_htmx_request(): if is_htmx_request():
now = datetime.now() now = datetime.now()
from shared.sexp.jinja_bridge import render as render_comp from shared.sx.jinja_bridge import render as render_comp
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 sexp_response(html) return sx_response(html)
return redirect(url_for("settings.cache")) return redirect(url_for("settings.cache"))
return bp return bp

View File

@@ -15,7 +15,7 @@ from sqlalchemy import select, delete
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.browser.app.utils.htmx import is_htmx_request
from shared.browser.app.redis_cacher import invalidate_tag_cache from shared.browser.app.redis_cacher import invalidate_tag_cache
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from models.tag_group import TagGroup, TagGroupTag from models.tag_group import TagGroup, TagGroupTag
from models.ghost_content import Tag from models.ghost_content import Tag
@@ -58,15 +58,15 @@ def register():
ctx = {"groups": groups, "unassigned_tags": unassigned} ctx = {"groups": groups, "unassigned_tags": unassigned}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_tag_groups_page, render_tag_groups_oob from sx.sx_components import render_tag_groups_page, render_tag_groups_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(ctx) tctx.update(ctx)
if not is_htmx_request(): if not is_htmx_request():
return await make_response(await render_tag_groups_page(tctx)) return await make_response(await render_tag_groups_page(tctx))
else: else:
return sexp_response(await render_tag_groups_oob(tctx)) return sx_response(await render_tag_groups_oob(tctx))
@bp.post("/") @bp.post("/")
@require_admin @require_admin
@@ -123,15 +123,15 @@ def register():
"assigned_tag_ids": assigned_tag_ids, "assigned_tag_ids": assigned_tag_ids,
} }
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_tag_group_edit_page, render_tag_group_edit_oob from sx.sx_components import render_tag_group_edit_page, render_tag_group_edit_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(ctx) tctx.update(ctx)
if not is_htmx_request(): if not is_htmx_request():
return await make_response(await render_tag_group_edit_page(tctx)) return await make_response(await render_tag_group_edit_page(tctx))
else: else:
return sexp_response(await render_tag_group_edit_oob(tctx)) return sx_response(await render_tag_group_edit_oob(tctx))
@bp.post("/<int:id>/") @bp.post("/<int:id>/")
@require_admin @require_admin

View File

@@ -22,7 +22,7 @@ from .services.pages_data import pages_data
from shared.browser.app.redis_cacher import cache_page, invalidate_tag_cache from shared.browser.app.redis_cacher import cache_page, invalidate_tag_cache
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.browser.app.authz import require_admin from shared.browser.app.authz import require_admin
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from shared.utils import host_url from shared.utils import host_url
def register(url_prefix, title): def register(url_prefix, title):
@@ -143,8 +143,8 @@ def register(url_prefix, title):
ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count ctx["page_cart_count"] = page_summary.count + page_summary.calendar_count + page_summary.ticket_count
ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total) ctx["page_cart_total"] = float(page_summary.total + page_summary.calendar_total + page_summary.ticket_total)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_home_page, render_home_oob from sx.sx_components import render_home_page, render_home_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(ctx) tctx.update(ctx)
@@ -152,8 +152,8 @@ def register(url_prefix, title):
html = await render_home_page(tctx) html = await render_home_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_home_oob(tctx) sx_src = await render_home_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@blogs_bp.get("/index") @blogs_bp.get("/index")
@blogs_bp.get("/index/") @blogs_bp.get("/index/")
@@ -181,8 +181,8 @@ def register(url_prefix, title):
"tag_groups": [], "tag_groups": [],
"posts": data.get("pages", []), "posts": data.get("pages", []),
} }
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_blog_page, render_blog_oob, render_blog_page_cards from sx.sx_components import render_blog_page, render_blog_oob, render_blog_page_cards
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(context) tctx.update(context)
@@ -190,11 +190,11 @@ def register(url_prefix, title):
html = await render_blog_page(tctx) html = await render_blog_page(tctx)
return await make_response(html) return await make_response(html)
elif q.page > 1: elif q.page > 1:
sexp_src = await render_blog_page_cards(tctx) sx_src = await render_blog_page_cards(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
else: else:
sexp_src = await render_blog_oob(tctx) sx_src = await render_blog_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
# Default: posts listing # Default: posts listing
# Drafts filter requires login; ignore if not logged in # Drafts filter requires login; ignore if not logged in
@@ -224,8 +224,8 @@ def register(url_prefix, title):
"drafts": q.drafts if show_drafts else None, "drafts": q.drafts if show_drafts else None,
} }
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_blog_page, render_blog_oob, render_blog_cards from sx.sx_components import render_blog_page, render_blog_oob, render_blog_cards
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(context) tctx.update(context)
@@ -233,18 +233,18 @@ def register(url_prefix, title):
html = await render_blog_page(tctx) html = await render_blog_page(tctx)
return await make_response(html) return await make_response(html)
elif q.page > 1: elif q.page > 1:
# Sexp wire format — client renders blog cards # Sx wire format — client renders blog cards
sexp_src = await render_blog_cards(tctx) sx_src = await render_blog_cards(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
else: else:
sexp_src = await render_blog_oob(tctx) sx_src = await render_blog_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@blogs_bp.get("/new/") @blogs_bp.get("/new/")
@require_admin @require_admin
async def new_post(): async def new_post():
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_new_post_oob, render_editor_panel from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel() tctx["editor_html"] = render_editor_panel()
@@ -252,8 +252,8 @@ def register(url_prefix, title):
html = await render_new_post_page(tctx) html = await render_new_post_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_new_post_oob(tctx) sx_src = await render_new_post_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@blogs_bp.post("/new/") @blogs_bp.post("/new/")
@require_admin @require_admin
@@ -274,8 +274,8 @@ def register(url_prefix, title):
try: try:
lexical_doc = json.loads(lexical_raw) lexical_doc = json.loads(lexical_raw)
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_editor_panel from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.") tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.")
html = await render_new_post_page(tctx) html = await render_new_post_page(tctx)
@@ -283,8 +283,8 @@ def register(url_prefix, title):
ok, reason = validate_lexical(lexical_doc) ok, reason = validate_lexical(lexical_doc)
if not ok: if not ok:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_editor_panel from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error=reason) tctx["editor_html"] = render_editor_panel(save_error=reason)
html = await render_new_post_page(tctx) html = await render_new_post_page(tctx)
@@ -324,8 +324,8 @@ def register(url_prefix, title):
@blogs_bp.get("/new-page/") @blogs_bp.get("/new-page/")
@require_admin @require_admin
async def new_page(): async def new_page():
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_new_post_oob, render_editor_panel from sx.sx_components import render_new_post_page, render_new_post_oob, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(is_page=True) tctx["editor_html"] = render_editor_panel(is_page=True)
@@ -334,8 +334,8 @@ def register(url_prefix, title):
html = await render_new_post_page(tctx) html = await render_new_post_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_new_post_oob(tctx) sx_src = await render_new_post_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@blogs_bp.post("/new-page/") @blogs_bp.post("/new-page/")
@require_admin @require_admin
@@ -356,8 +356,8 @@ def register(url_prefix, title):
try: try:
lexical_doc = json.loads(lexical_raw) lexical_doc = json.loads(lexical_raw)
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_editor_panel from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True) tctx["editor_html"] = render_editor_panel(save_error="Invalid JSON in editor content.", is_page=True)
tctx["is_page"] = True tctx["is_page"] = True
@@ -366,8 +366,8 @@ def register(url_prefix, title):
ok, reason = validate_lexical(lexical_doc) ok, reason = validate_lexical(lexical_doc)
if not ok: if not ok:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_new_post_page, render_editor_panel from sx.sx_components import render_new_post_page, render_editor_panel
tctx = await get_template_context() tctx = await get_template_context()
tctx["editor_html"] = render_editor_panel(save_error=reason, is_page=True) tctx["editor_html"] = render_editor_panel(save_error=reason, is_page=True)
tctx["is_page"] = True tctx["is_page"] = True

View File

@@ -1,6 +1,6 @@
"""Blog app fragment endpoints. """Blog app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
""" """
@@ -26,13 +26,13 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
result = await handler() result = await handler()
return Response(result, status=200, content_type="text/sexp") return Response(result, status=200, content_type="text/sx")
# --- nav-tree fragment — returns sexp source --- # --- nav-tree fragment — returns sx source ---
async def _nav_tree_handler(): async def _nav_tree_handler():
from shared.sexp.helpers import sexp_call, SexpExpr from shared.sx.helpers import sx_call, SxExpr
from shared.infrastructure.urls import ( from shared.infrastructure.urls import (
blog_url, cart_url, market_url, events_url, blog_url, cart_url, market_url, events_url,
federation_url, account_url, artdag_url, federation_url, account_url, artdag_url,
@@ -54,36 +54,36 @@ def register():
nav_cls = "whitespace-nowrap flex items-center gap-2 rounded p-2 text-sm" nav_cls = "whitespace-nowrap flex items-center gap-2 rounded p-2 text-sm"
item_sexps = [] item_sxs = []
for item in menu_items: for item in menu_items:
href = app_slugs.get(item.slug, blog_url(f"/{item.slug}/")) href = app_slugs.get(item.slug, blog_url(f"/{item.slug}/"))
selected = "true" if (item.slug == first_seg selected = "true" if (item.slug == first_seg
or item.slug == app_name) else "false" or item.slug == app_name) else "false"
img = sexp_call("blog-nav-item-image", img = sx_call("blog-nav-item-image",
src=getattr(item, "feature_image", None), src=getattr(item, "feature_image", None),
label=getattr(item, "label", item.slug)) label=getattr(item, "label", item.slug))
item_sexps.append(sexp_call( item_sxs.append(sx_call(
"blog-nav-item-link", "blog-nav-item-link",
href=href, hx_get=href, selected=selected, nav_cls=nav_cls, href=href, hx_get=href, selected=selected, nav_cls=nav_cls,
img=SexpExpr(img), label=getattr(item, "label", item.slug), img=SxExpr(img), label=getattr(item, "label", item.slug),
)) ))
# artdag link # artdag link
href = artdag_url("/") href = artdag_url("/")
selected = "true" if ("artdag" == first_seg selected = "true" if ("artdag" == first_seg
or "artdag" == app_name) else "false" or "artdag" == app_name) else "false"
img = sexp_call("blog-nav-item-image", src=None, label="art-dag") img = sx_call("blog-nav-item-image", src=None, label="art-dag")
item_sexps.append(sexp_call( item_sxs.append(sx_call(
"blog-nav-item-link", "blog-nav-item-link",
href=href, hx_get=href, selected=selected, nav_cls=nav_cls, href=href, hx_get=href, selected=selected, nav_cls=nav_cls,
img=SexpExpr(img), label="art-dag", img=SxExpr(img), label="art-dag",
)) ))
if not item_sexps: if not item_sxs:
return sexp_call("blog-nav-empty", return sx_call("blog-nav-empty",
wrapper_id="menu-items-nav-wrapper") wrapper_id="menu-items-nav-wrapper")
items_frag = "(<> " + " ".join(item_sexps) + ")" items_frag = "(<> " + " ".join(item_sxs) + ")"
arrow_cls = "scrolling-menu-arrow-menu-items-container" arrow_cls = "scrolling-menu-arrow-menu-items-container"
container_id = "menu-items-container" container_id = "menu-items-container"
@@ -101,21 +101,21 @@ def register():
right_hs = ("on click set #" + container_id right_hs = ("on click set #" + container_id
+ ".scrollLeft to #" + container_id + ".scrollLeft + 200") + ".scrollLeft to #" + container_id + ".scrollLeft + 200")
return sexp_call("blog-nav-wrapper", return sx_call("blog-nav-wrapper",
arrow_cls=arrow_cls, arrow_cls=arrow_cls,
container_id=container_id, container_id=container_id,
left_hs=left_hs, left_hs=left_hs,
scroll_hs=scroll_hs, scroll_hs=scroll_hs,
right_hs=right_hs, right_hs=right_hs,
items=SexpExpr(items_frag)) items=SxExpr(items_frag))
_handlers["nav-tree"] = _nav_tree_handler _handlers["nav-tree"] = _nav_tree_handler
# --- link-card fragment — returns sexp source --- # --- link-card fragment — returns sx source ---
def _blog_link_card_sexp(post, link: str) -> str: def _blog_link_card_sx(post, link: str) -> str:
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
published = post.published_at.strftime("%d %b %Y") if post.published_at else None published = post.published_at.strftime("%d %b %Y") if post.published_at else None
return sexp_call("link-card", return sx_call("link-card",
link=link, link=link,
title=post.title, title=post.title,
image=post.feature_image, image=post.feature_image,
@@ -139,7 +139,7 @@ def register():
parts.append(f"<!-- fragment:{s} -->") parts.append(f"<!-- fragment:{s} -->")
post = await services.blog.get_post_by_slug(g.s, s) post = await services.blog.get_post_by_slug(g.s, s)
if post: if post:
parts.append(_blog_link_card_sexp(post, blog_url(f"/{post.slug}"))) parts.append(_blog_link_card_sx(post, blog_url(f"/{post.slug}")))
return "\n".join(parts) return "\n".join(parts)
# Single mode # Single mode
@@ -148,7 +148,7 @@ def register():
post = await services.blog.get_post_by_slug(g.s, slug) post = await services.blog.get_post_by_slug(g.s, slug)
if not post: if not post:
return "" return ""
return _blog_link_card_sexp(post, blog_url(f"/{post.slug}")) return _blog_link_card_sx(post, blog_url(f"/{post.slug}"))
_handlers["link-card"] = _link_card_handler _handlers["link-card"] = _link_card_handler

View File

@@ -13,14 +13,14 @@ from .services.menu_items import (
MenuItemError, MenuItemError,
) )
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items') bp = Blueprint("menu_items", __name__, url_prefix='/settings/menu_items')
def get_menu_items_nav_oob_sync(menu_items): def get_menu_items_nav_oob_sync(menu_items):
"""Helper to generate OOB update for root nav menu items""" """Helper to generate OOB update for root nav menu items"""
from sexp.sexp_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.get("/")
@@ -30,8 +30,8 @@ def register():
menu_items = await get_all_menu_items(g.s) menu_items = await get_all_menu_items(g.s)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_menu_items_page, render_menu_items_oob from sx.sx_components import render_menu_items_page, render_menu_items_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx["menu_items"] = menu_items tctx["menu_items"] = menu_items
@@ -39,8 +39,8 @@ def register():
html = await render_menu_items_page(tctx) html = await render_menu_items_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_menu_items_oob(tctx) sx_src = await render_menu_items_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/new/") @bp.get("/new/")
@require_admin @require_admin
@@ -73,10 +73,10 @@ def register():
# Get updated list and nav OOB # Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s) menu_items = await get_all_menu_items(g.s)
from sexp.sexp_components import render_menu_items_list from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items) html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
except MenuItemError as e: except MenuItemError as e:
return jsonify({"message": str(e), "errors": {}}), 400 return jsonify({"message": str(e), "errors": {}}), 400
@@ -116,10 +116,10 @@ def register():
# Get updated list and nav OOB # Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s) menu_items = await get_all_menu_items(g.s)
from sexp.sexp_components import render_menu_items_list from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items) html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
except MenuItemError as e: except MenuItemError as e:
return jsonify({"message": str(e), "errors": {}}), 400 return jsonify({"message": str(e), "errors": {}}), 400
@@ -137,10 +137,10 @@ def register():
# Get updated list and nav OOB # Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s) menu_items = await get_all_menu_items(g.s)
from sexp.sexp_components import render_menu_items_list from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items) html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
@bp.get("/pages/search/") @bp.get("/pages/search/")
@require_admin @require_admin
@@ -184,9 +184,9 @@ def register():
# Get updated list and nav OOB # Get updated list and nav OOB
menu_items = await get_all_menu_items(g.s) menu_items = await get_all_menu_items(g.s)
from sexp.sexp_components import render_menu_items_list from sx.sx_components import render_menu_items_list
html = render_menu_items_list(menu_items) html = render_menu_items_list(menu_items)
nav_oob = get_menu_items_nav_oob_sync(menu_items) nav_oob = get_menu_items_nav_oob_sync(menu_items)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
return bp return bp

View File

@@ -12,7 +12,7 @@ from quart import (
) )
from shared.browser.app.authz import require_admin, require_post_author from shared.browser.app.authz import require_admin, require_post_author
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from shared.utils import host_url from shared.utils import host_url
def register(): def register():
@@ -52,8 +52,8 @@ def register():
"sumup_checkout_prefix": sumup_checkout_prefix, "sumup_checkout_prefix": sumup_checkout_prefix,
} }
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_admin_page, render_post_admin_oob from sx.sx_components import render_post_admin_page, render_post_admin_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(ctx) tctx.update(ctx)
@@ -61,8 +61,8 @@ def register():
html = await render_post_admin_page(tctx) html = await render_post_admin_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_admin_oob(tctx) sx_src = await render_post_admin_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.put("/features/") @bp.put("/features/")
@require_admin @require_admin
@@ -99,14 +99,14 @@ def register():
features = result.get("features", {}) features = result.get("features", {})
from sexp.sexp_components import render_features_panel from sx.sx_components import render_features_panel
html = render_features_panel( html = render_features_panel(
features, post, features, post,
sumup_configured=result.get("sumup_configured", False), sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "", sumup_merchant_code=result.get("sumup_merchant_code") or "",
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "", sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
) )
return sexp_response(html) return sx_response(html)
@bp.put("/admin/sumup/") @bp.put("/admin/sumup/")
@require_admin @require_admin
@@ -138,20 +138,20 @@ def register():
result = await call_action("blog", "update-page-config", payload=payload) result = await call_action("blog", "update-page-config", payload=payload)
features = result.get("features", {}) features = result.get("features", {})
from sexp.sexp_components import render_features_panel from sx.sx_components import render_features_panel
html = render_features_panel( html = render_features_panel(
features, post, features, post,
sumup_configured=result.get("sumup_configured", False), sumup_configured=result.get("sumup_configured", False),
sumup_merchant_code=result.get("sumup_merchant_code") or "", sumup_merchant_code=result.get("sumup_merchant_code") or "",
sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "", sumup_checkout_prefix=result.get("sumup_checkout_prefix") or "",
) )
return sexp_response(html) return sx_response(html)
@bp.get("/data/") @bp.get("/data/")
@require_admin @require_admin
async def data(slug: str): async def data(slug: str):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_data_page, render_post_data_oob from sx.sx_components import render_post_data_page, render_post_data_oob
data_html = await render_template("_types/post_data/_main_panel.html") data_html = await render_template("_types/post_data/_main_panel.html")
tctx = await get_template_context() tctx = await get_template_context()
@@ -160,8 +160,8 @@ def register():
html = await render_post_data_page(tctx) html = await render_post_data_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_data_oob(tctx) sx_src = await render_post_data_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/entries/calendar/<int:calendar_id>/") @bp.get("/entries/calendar/<int:calendar_id>/")
@require_admin @require_admin
@@ -269,8 +269,8 @@ def register():
# Load entries and post for each calendar # Load entries and post for each calendar
for calendar in all_calendars: for calendar in all_calendars:
await g.s.refresh(calendar, ["entries", "post"]) await g.s.refresh(calendar, ["entries", "post"])
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_entries_page, render_post_entries_oob from sx.sx_components import render_post_entries_page, render_post_entries_oob
entries_html = await render_template( entries_html = await render_template(
"_types/post_entries/_main_panel.html", "_types/post_entries/_main_panel.html",
@@ -283,8 +283,8 @@ def register():
html = await render_post_entries_page(tctx) html = await render_post_entries_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_entries_oob(tctx) sx_src = await render_post_entries_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/entries/<int:entry_id>/toggle/") @bp.post("/entries/<int:entry_id>/toggle/")
@require_admin @require_admin
@@ -330,13 +330,13 @@ def register():
).scalars().all() ).scalars().all()
# Return the associated entries admin list + OOB update for nav entries # Return the associated entries admin list + OOB update for nav entries
from sexp.sexp_components import render_associated_entries, render_nav_entries_oob from sx.sx_components import render_associated_entries, render_nav_entries_oob
post = g.post_data["post"] post = g.post_data["post"]
admin_list = render_associated_entries(all_calendars, associated_entry_ids, post["slug"]) admin_list = render_associated_entries(all_calendars, associated_entry_ids, post["slug"])
nav_entries_html = render_nav_entries_oob(associated_entries, calendars, post) nav_entries_html = render_nav_entries_oob(associated_entries, calendars, post)
return sexp_response(admin_list + nav_entries_html) return sx_response(admin_list + nav_entries_html)
@bp.get("/settings/") @bp.get("/settings/")
@require_post_author @require_post_author
@@ -348,8 +348,8 @@ def register():
ghost_post = await get_post_for_edit(ghost_id, is_page=is_page) ghost_post = await get_post_for_edit(ghost_id, is_page=is_page)
save_success = request.args.get("saved") == "1" save_success = request.args.get("saved") == "1"
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_settings_page, render_post_settings_oob from sx.sx_components import render_post_settings_page, render_post_settings_oob
settings_html = await render_template( settings_html = await render_template(
"_types/post_settings/_main_panel.html", "_types/post_settings/_main_panel.html",
@@ -362,8 +362,8 @@ def register():
html = await render_post_settings_page(tctx) html = await render_post_settings_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_settings_oob(tctx) sx_src = await render_post_settings_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/settings/") @bp.post("/settings/")
@require_post_author @require_post_author
@@ -452,8 +452,8 @@ def register():
from types import SimpleNamespace from types import SimpleNamespace
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters] newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_edit_page, render_post_edit_oob from sx.sx_components import render_post_edit_page, render_post_edit_oob
edit_html = await render_template( edit_html = await render_template(
"_types/post_edit/_main_panel.html", "_types/post_edit/_main_panel.html",
@@ -468,8 +468,8 @@ def register():
html = await render_post_edit_page(tctx) html = await render_post_edit_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_edit_oob(tctx) sx_src = await render_post_edit_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/edit/") @bp.post("/edit/")
@require_post_author @require_post_author
@@ -598,8 +598,8 @@ def register():
page_markets = await _fetch_page_markets(post_id) page_markets = await _fetch_page_markets(post_id)
from sexp.sexp_components import render_markets_panel from sx.sx_components import render_markets_panel
return sexp_response(render_markets_panel(page_markets, post)) return sx_response(render_markets_panel(page_markets, post))
@bp.post("/markets/new/") @bp.post("/markets/new/")
@require_admin @require_admin
@@ -624,8 +624,8 @@ def register():
# Return updated markets list # Return updated markets list
page_markets = await _fetch_page_markets(post_id) page_markets = await _fetch_page_markets(post_id)
from sexp.sexp_components import render_markets_panel from sx.sx_components import render_markets_panel
return sexp_response(render_markets_panel(page_markets, post)) return sx_response(render_markets_panel(page_markets, post))
@bp.delete("/markets/<market_slug>/") @bp.delete("/markets/<market_slug>/")
@require_admin @require_admin
@@ -644,7 +644,7 @@ def register():
# Return updated markets list # Return updated markets list
page_markets = await _fetch_page_markets(post_id) page_markets = await _fetch_page_markets(post_id)
from sexp.sexp_components import render_markets_panel from sx.sx_components import render_markets_panel
return sexp_response(render_markets_panel(page_markets, post)) return sx_response(render_markets_panel(page_markets, post))
return bp return bp

View File

@@ -21,7 +21,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache
from .admin.routes import register as register_admin from .admin.routes import register as register_admin
from shared.config import config from shared.config import config
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
bp = Blueprint("post", __name__, url_prefix='/<slug>') bp = Blueprint("post", __name__, url_prefix='/<slug>')
@@ -104,28 +104,28 @@ def register():
@bp.get("/") @bp.get("/")
@cache_page(tag="post.post_detail") @cache_page(tag="post.post_detail")
async def post_detail(slug: str): async def post_detail(slug: str):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_post_page, render_post_oob from sx.sx_components import render_post_page, render_post_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_post_page(tctx) html = await render_post_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_post_oob(tctx) sx_src = await render_post_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/like/toggle/") @bp.post("/like/toggle/")
@clear_cache(tag="post.post_detail", tag_scope="user") @clear_cache(tag="post.post_detail", tag_scope="user")
async def like_toggle(slug: str): async def like_toggle(slug: str):
from shared.utils import host_url from shared.utils import host_url
from sexp.sexp_components import render_like_toggle_button from sx.sx_components import render_like_toggle_button
like_url = host_url(url_for('blog.post.like_toggle', slug=slug)) like_url = host_url(url_for('blog.post.like_toggle', slug=slug))
# Get post_id from g.post_data # Get post_id from g.post_data
if not g.user: if not g.user:
return sexp_response(render_like_toggle_button(slug, False, like_url), status=403) return sx_response(render_like_toggle_button(slug, False, like_url), status=403)
post_id = g.post_data["post"]["id"] post_id = g.post_data["post"]["id"]
user_id = g.user.id user_id = g.user.id
@@ -135,7 +135,7 @@ def register():
}) })
liked = result["liked"] liked = result["liked"]
return sexp_response(render_like_toggle_button(slug, liked, like_url)) return sx_response(render_like_toggle_button(slug, liked, like_url))
@bp.get("/w/<widget_domain>/") @bp.get("/w/<widget_domain>/")
async def widget_paginate(slug: str, widget_domain: str): async def widget_paginate(slug: str, widget_domain: str):

View File

@@ -6,7 +6,7 @@ from sqlalchemy.orm import selectinload
from shared.browser.app.authz import require_login from shared.browser.app.authz import require_login
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from models import Snippet from models import Snippet
@@ -39,8 +39,8 @@ def register():
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.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_snippets_page, render_snippets_oob from sx.sx_components import render_snippets_page, render_snippets_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx["snippets"] = snippets tctx["snippets"] = snippets
@@ -49,8 +49,8 @@ def register():
html = await render_snippets_page(tctx) html = await render_snippets_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_snippets_oob(tctx) sx_src = await render_snippets_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.delete("/<int:snippet_id>/") @bp.delete("/<int:snippet_id>/")
@require_login @require_login
@@ -68,8 +68,8 @@ def register():
await g.s.flush() await g.s.flush()
snippets = await _visible_snippets(g.s) snippets = await _visible_snippets(g.s)
from sexp.sexp_components import render_snippets_list from sx.sx_components import render_snippets_list
return sexp_response(render_snippets_list(snippets, is_admin)) return sx_response(render_snippets_list(snippets, is_admin))
@bp.patch("/<int:snippet_id>/visibility/") @bp.patch("/<int:snippet_id>/visibility/")
@require_login @require_login
@@ -92,7 +92,7 @@ def register():
await g.s.flush() await g.s.flush()
snippets = await _visible_snippets(g.s) snippets = await _visible_snippets(g.s)
from sexp.sexp_components import render_snippets_list from sx.sx_components import render_snippets_list
return sexp_response(render_snippets_list(snippets, True)) return sx_response(render_snippets_list(snippets, True))
return bp return bp

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path

View File

@@ -150,8 +150,8 @@ def register(url_prefix: str) -> Blueprint:
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)
except ValueError as e: except ValueError as e:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_checkout_error_page from sx.sx_components import render_checkout_error_page
tctx = await get_template_context() tctx = await get_template_context()
html = await render_checkout_error_page(tctx, error=str(e)) html = await render_checkout_error_page(tctx, error=str(e))
return await make_response(html, 400) return await make_response(html, 400)
@@ -207,8 +207,8 @@ def register(url_prefix: str) -> Blueprint:
hosted_url = result.get("sumup_hosted_url") hosted_url = result.get("sumup_hosted_url")
if not hosted_url: if not hosted_url:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_checkout_error_page from sx.sx_components import render_checkout_error_page
tctx = await get_template_context() tctx = await get_template_context()
html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.") html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.")
return await make_response(html, 500) return await make_response(html, 500)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from quart import Blueprint, render_template, make_response from quart import Blueprint, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from .services import get_cart_grouped_by_page from .services import get_cart_grouped_by_page
@@ -15,8 +15,8 @@ def register(url_prefix: str) -> Blueprint:
@bp.get("/") @bp.get("/")
async def overview(): async def overview():
from quart import g from quart import g
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_overview_page, render_overview_oob from sx.sx_components import render_overview_page, render_overview_oob
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()
@@ -25,7 +25,7 @@ def register(url_prefix: str) -> Blueprint:
html = await render_overview_page(ctx, page_groups) html = await render_overview_page(ctx, page_groups)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_overview_oob(ctx, page_groups) sx_src = await render_overview_oob(ctx, page_groups)
return sexp_response(sexp_src) return sx_response(sx_src)
return bp return bp

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from quart import Blueprint, g, redirect, make_response, url_for from quart import Blueprint, g, redirect, make_response, url_for
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response 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,
@@ -41,8 +41,8 @@ def register(url_prefix: str) -> Blueprint:
ticket_total=ticket_total, ticket_total=ticket_total,
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_page_cart_page, render_page_cart_oob from sx.sx_components import render_page_cart_page, render_page_cart_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
@@ -52,11 +52,11 @@ def register(url_prefix: str) -> Blueprint:
) )
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_page_cart_oob( sx_src = await render_page_cart_oob(
ctx, post, cart, cal_entries, page_tickets, ctx, post, cart, cal_entries, page_tickets,
ticket_groups, total, calendar_total, ticket_total, ticket_groups, total, calendar_total, ticket_total,
) )
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/checkout/") @bp.post("/checkout/")
async def page_checkout(): async def page_checkout():
@@ -111,8 +111,8 @@ def register(url_prefix: str) -> Blueprint:
hosted_url = result.get("sumup_hosted_url") hosted_url = result.get("sumup_hosted_url")
if not hosted_url: if not hosted_url:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_checkout_error_page from sx.sx_components import render_checkout_error_page
tctx = await get_template_context() tctx = await get_template_context()
html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.") html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.")
return await make_response(html, 500) return await make_response(html, 500)

View File

@@ -1,6 +1,6 @@
"""Cart app fragment endpoints. """Cart app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
Fragments: Fragments:
@@ -19,13 +19,13 @@ def register():
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments") bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
# --------------------------------------------------------------- # ---------------------------------------------------------------
# Fragment handlers — return sexp source text # Fragment handlers — return sx source text
# --------------------------------------------------------------- # ---------------------------------------------------------------
async def _cart_mini(): async def _cart_mini():
from shared.services.registry import services from shared.services.registry import services
from shared.infrastructure.urls import blog_url, cart_url from shared.infrastructure.urls import blog_url, cart_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
user_id = request.args.get("user_id", type=int) user_id = request.args.get("user_id", type=int)
session_id = request.args.get("session_id") session_id = request.args.get("session_id")
@@ -35,7 +35,7 @@ def register():
) )
count = summary.count + summary.calendar_count + summary.ticket_count count = summary.count + summary.calendar_count + summary.ticket_count
oob = request.args.get("oob", "") oob = request.args.get("oob", "")
return sexp_call("cart-mini", return sx_call("cart-mini",
cart_count=count, cart_count=count,
blog_url=blog_url(""), blog_url=blog_url(""),
cart_url=cart_url(""), cart_url=cart_url(""),
@@ -43,9 +43,9 @@ def register():
async def _account_nav_item(): async def _account_nav_item():
from shared.infrastructure.urls import cart_url from shared.infrastructure.urls import cart_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
return sexp_call("account-nav-item", return sx_call("account-nav-item",
href=cart_url("/orders/"), href=cart_url("/orders/"),
label="orders") label="orders")
@@ -67,8 +67,8 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
src = await handler() src = await handler()
return Response(src, status=200, content_type="text/sexp") return Response(src, status=200, content_type="text/sx")
return bp return bp

View File

@@ -13,7 +13,7 @@ from shared.infrastructure.http_utils import vary as _vary, current_url_without_
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from bp.cart.services import check_sumup_status from bp.cart.services import check_sumup_status
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from .filters.qs import makeqs_factory, decode from .filters.qs import makeqs_factory, decode
@@ -56,8 +56,8 @@ def register() -> Blueprint:
order = result.scalar_one_or_none() order = result.scalar_one_or_none()
if not order: if not order:
return await make_response("Order not found", 404) return await make_response("Order not found", 404)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_order_page, render_order_oob from sx.sx_components import render_order_page, render_order_oob
ctx = await get_template_context() ctx = await get_template_context()
calendar_entries = ctx.get("calendar_entries") calendar_entries = ctx.get("calendar_entries")
@@ -66,8 +66,8 @@ def register() -> Blueprint:
html = await render_order_page(ctx, order, calendar_entries, url_for) html = await render_order_page(ctx, order, calendar_entries, url_for)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_order_oob(ctx, order, calendar_entries, url_for) sx_src = await render_order_oob(ctx, order, calendar_entries, url_for)
return sexp_response(sexp_src) 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):
@@ -121,8 +121,8 @@ def register() -> Blueprint:
await g.s.flush() await g.s.flush()
if not hosted_url: if not hosted_url:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_checkout_error_page from sx.sx_components import render_checkout_error_page
tctx = await get_template_context() tctx = await get_template_context()
html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp when trying to reopen payment.", order=order) html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp when trying to reopen payment.", order=order)
return await make_response(html, 500) return await make_response(html, 500)

View File

@@ -13,7 +13,7 @@ from shared.infrastructure.http_utils import vary as _vary, current_url_without_
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from bp.cart.services import check_sumup_status from bp.cart.services import check_sumup_status
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from bp import register_order from bp import register_order
from .filters.qs import makeqs_factory, decode from .filters.qs import makeqs_factory, decode
@@ -137,8 +137,8 @@ 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.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import ( from sx.sx_components import (
render_orders_page, render_orders_page,
render_orders_rows, render_orders_rows,
render_orders_oob, render_orders_oob,
@@ -154,16 +154,16 @@ def register(url_prefix: str) -> Blueprint:
) )
resp = await make_response(html) resp = await make_response(html)
elif page > 1: elif page > 1:
sexp_src = await render_orders_rows( sx_src = await render_orders_rows(
ctx, orders, page, total_pages, url_for, qs_fn, ctx, orders, page, total_pages, url_for, qs_fn,
) )
resp = sexp_response(sexp_src) resp = sx_response(sx_src)
else: else:
sexp_src = await render_orders_oob( sx_src = await render_orders_oob(
ctx, orders, page, total_pages, search, total_count, ctx, orders, page, total_pages, search, total_count,
url_for, qs_fn, url_for, qs_fn,
) )
resp = sexp_response(sexp_src) 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)

View File

@@ -8,7 +8,7 @@ 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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -17,8 +17,8 @@ def register():
@bp.get("/") @bp.get("/")
@require_admin @require_admin
async def admin(**kwargs): async def admin(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_cart_admin_page, render_cart_admin_oob from sx.sx_components import render_cart_admin_page, render_cart_admin_oob
ctx = await get_template_context() ctx = await get_template_context()
page_post = getattr(g, "page_post", None) page_post = getattr(g, "page_post", None)
@@ -26,14 +26,14 @@ def register():
html = await render_cart_admin_page(ctx, page_post) html = await render_cart_admin_page(ctx, page_post)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_cart_admin_oob(ctx, page_post) sx_src = await render_cart_admin_oob(ctx, page_post)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/payments/") @bp.get("/payments/")
@require_admin @require_admin
async def payments(**kwargs): async def payments(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_cart_payments_page, render_cart_payments_oob from sx.sx_components import render_cart_payments_page, render_cart_payments_oob
ctx = await get_template_context() ctx = await get_template_context()
page_post = getattr(g, "page_post", None) page_post = getattr(g, "page_post", None)
@@ -41,8 +41,8 @@ def register():
html = await render_cart_payments_page(ctx, page_post) html = await render_cart_payments_page(ctx, page_post)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_cart_payments_oob(ctx, page_post) sx_src = await render_cart_payments_oob(ctx, page_post)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.put("/payments/") @bp.put("/payments/")
@require_admin @require_admin
@@ -77,10 +77,10 @@ def register():
) )
g.page_config = SimpleNamespace(**raw_pc) if raw_pc else None g.page_config = SimpleNamespace(**raw_pc) if raw_pc else None
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_cart_payments_panel from sx.sx_components import render_cart_payments_panel
ctx = await get_template_context() ctx = await get_template_context()
html = render_cart_payments_panel(ctx) html = render_cart_payments_panel(ctx)
return sexp_response(html) return sx_response(html)
return bp return bp

View File

@@ -10,17 +10,17 @@ import os
from typing import Any from typing import Any
from markupsafe import escape from markupsafe import escape
from shared.sexp.jinja_bridge import load_service_components from shared.sx.jinja_bridge import load_service_components
from shared.sexp.helpers import ( from shared.sx.helpers import (
call_url, root_header_sexp, post_admin_header_sexp, call_url, root_header_sx, post_admin_header_sx,
post_header_sexp as _shared_post_header_sexp, post_header_sx as _shared_post_header_sx,
search_desktop_sexp, search_mobile_sexp, search_desktop_sx, search_mobile_sx,
full_page_sexp, oob_page_sexp, header_child_sexp, full_page_sx, oob_page_sx, header_child_sx,
sexp_call, SexpExpr, sx_call, SxExpr,
) )
from shared.infrastructure.urls import market_product_url, cart_url from shared.infrastructure.urls import market_product_url, cart_url
# Load cart-specific .sexpr components at import time # Load cart-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__))) load_service_components(os.path.dirname(os.path.dirname(__file__)))
@@ -29,7 +29,7 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _ensure_post_ctx(ctx: dict, page_post: Any) -> dict: def _ensure_post_ctx(ctx: dict, page_post: Any) -> dict:
"""Ensure ctx has a 'post' dict from page_post DTO (for shared post_header_sexp).""" """Ensure ctx has a 'post' dict from page_post DTO (for shared post_header_sx)."""
if ctx.get("post") or not page_post: if ctx.get("post") or not page_post:
return ctx return ctx
ctx = {**ctx, "post": { ctx = {**ctx, "post": {
@@ -63,16 +63,16 @@ async def _ensure_container_nav(ctx: dict) -> dict:
return {**ctx, "container_nav": events_nav + market_nav} return {**ctx, "container_nav": events_nav + market_nav}
async def _post_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False) -> str: async def _post_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
"""Build post-level header row from page_post DTO, using shared helper.""" """Build post-level header row from page_post DTO, using shared helper."""
ctx = _ensure_post_ctx(ctx, page_post) ctx = _ensure_post_ctx(ctx, page_post)
ctx = await _ensure_container_nav(ctx) ctx = await _ensure_container_nav(ctx)
return _shared_post_header_sexp(ctx, oob=oob) return _shared_post_header_sx(ctx, oob=oob)
def _cart_header_sexp(ctx: dict, *, oob: bool = False) -> str: def _cart_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the cart section header row.""" """Build the cart section header row."""
return sexp_call( return sx_call(
"menu-row-sx", "menu-row-sx",
id="cart-row", level=1, colour="sky", id="cart-row", level=1, colour="sky",
link_href=call_url(ctx, "cart_url", "/"), link_href=call_url(ctx, "cart_url", "/"),
@@ -81,28 +81,28 @@ def _cart_header_sexp(ctx: dict, *, oob: bool = False) -> str:
) )
def _page_cart_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False) -> str: def _page_cart_header_sx(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
"""Build the per-page cart header row.""" """Build the per-page cart header row."""
slug = page_post.slug if page_post else "" slug = page_post.slug if page_post else ""
title = ((page_post.title if page_post else None) or "")[:160] title = ((page_post.title if page_post else None) or "")[:160]
label_parts = [] label_parts = []
if page_post and page_post.feature_image: if page_post and page_post.feature_image:
label_parts.append(sexp_call("cart-page-label-img", src=page_post.feature_image)) label_parts.append(sx_call("cart-page-label-img", src=page_post.feature_image))
label_parts.append(f'(span "{escape(title)}")') label_parts.append(f'(span "{escape(title)}")')
label_sexp = "(<> " + " ".join(label_parts) + ")" label_sx = "(<> " + " ".join(label_parts) + ")"
nav_sexp = sexp_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/")) nav_sx = sx_call("cart-all-carts-link", href=call_url(ctx, "cart_url", "/"))
return sexp_call( return sx_call(
"menu-row-sx", "menu-row-sx",
id="page-cart-row", level=2, colour="sky", id="page-cart-row", level=2, colour="sky",
link_href=call_url(ctx, "cart_url", f"/{slug}/"), link_href=call_url(ctx, "cart_url", f"/{slug}/"),
link_label_content=SexpExpr(label_sexp), link_label_content=SxExpr(label_sx),
nav=SexpExpr(nav_sexp), oob=oob, nav=SxExpr(nav_sx), oob=oob,
) )
def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str:
"""Build the account section header row (for orders).""" """Build the account section header row (for orders)."""
return sexp_call( return sx_call(
"menu-row-sx", "menu-row-sx",
id="auth-row", level=1, colour="sky", id="auth-row", level=1, colour="sky",
link_href=call_url(ctx, "account_url", "/"), link_href=call_url(ctx, "account_url", "/"),
@@ -111,9 +111,9 @@ def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str:
) )
def _orders_header_sexp(ctx: dict, list_url: str) -> str: def _orders_header_sx(ctx: dict, list_url: str) -> str:
"""Build the orders section header row.""" """Build the orders section header row."""
return sexp_call( return sx_call(
"menu-row-sx", "menu-row-sx",
id="orders-row", level=2, colour="sky", id="orders-row", level=2, colour="sky",
link_href=list_url, link_label="Orders", icon="fa fa-gbp", link_href=list_url, link_label="Orders", icon="fa fa-gbp",
@@ -125,13 +125,13 @@ def _orders_header_sexp(ctx: dict, list_url: str) -> str:
# Cart overview # Cart overview
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _badge_sexp(icon: str, count: int, label: str) -> str: def _badge_sx(icon: str, count: int, label: str) -> str:
"""Render a count badge.""" """Render a count badge."""
s = "s" if count != 1 else "" s = "s" if count != 1 else ""
return sexp_call("cart-badge", icon=icon, text=f"{count} {label}{s}") return sx_call("cart-badge", icon=icon, text=f"{count} {label}{s}")
def _page_group_card_sexp(grp: Any, ctx: dict) -> str: def _page_group_card_sx(grp: Any, ctx: dict) -> str:
"""Render a single page group card for cart overview.""" """Render a single page group card for cart overview."""
post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None) post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None)
cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", []) cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", [])
@@ -149,13 +149,13 @@ def _page_group_card_sexp(grp: Any, ctx: dict) -> str:
# Count badges # Count badges
badge_parts = [] badge_parts = []
if product_count > 0: if product_count > 0:
badge_parts.append(_badge_sexp("fa fa-box-open", product_count, "item")) badge_parts.append(_badge_sx("fa fa-box-open", product_count, "item"))
if calendar_count > 0: if calendar_count > 0:
badge_parts.append(_badge_sexp("fa fa-calendar", calendar_count, "booking")) badge_parts.append(_badge_sx("fa fa-calendar", calendar_count, "booking"))
if ticket_count > 0: if ticket_count > 0:
badge_parts.append(_badge_sexp("fa fa-ticket", ticket_count, "ticket")) badge_parts.append(_badge_sx("fa fa-ticket", ticket_count, "ticket"))
badges_sexp = "(<> " + " ".join(badge_parts) + ")" if badge_parts else '""' badges_sx = "(<> " + " ".join(badge_parts) + ")" if badge_parts else '""'
badges_wrap = sexp_call("cart-badges-wrap", badges=SexpExpr(badges_sexp)) badges_wrap = sx_call("cart-badges-wrap", badges=SxExpr(badges_sx))
if post: if post:
slug = post.slug if hasattr(post, "slug") else post.get("slug", "") slug = post.slug if hasattr(post, "slug") else post.get("slug", "")
@@ -164,58 +164,58 @@ def _page_group_card_sexp(grp: Any, ctx: dict) -> str:
cart_href = call_url(ctx, "cart_url", f"/{slug}/") cart_href = call_url(ctx, "cart_url", f"/{slug}/")
if feature_image: if feature_image:
img = sexp_call("cart-group-card-img", src=feature_image, alt=title) img = sx_call("cart-group-card-img", src=feature_image, alt=title)
else: else:
img = sexp_call("cart-group-card-placeholder") img = sx_call("cart-group-card-placeholder")
mp_sub = "" mp_sub = ""
if market_place: if market_place:
mp_name = market_place.name if hasattr(market_place, "name") else market_place.get("name", "") mp_name = market_place.name if hasattr(market_place, "name") else market_place.get("name", "")
mp_sub = sexp_call("cart-mp-subtitle", title=title) mp_sub = sx_call("cart-mp-subtitle", title=title)
else: else:
mp_name = "" mp_name = ""
display_title = mp_name or title display_title = mp_name or title
return sexp_call( return sx_call(
"cart-group-card", "cart-group-card",
href=cart_href, img=SexpExpr(img), display_title=display_title, href=cart_href, img=SxExpr(img), display_title=display_title,
subtitle=SexpExpr(mp_sub) if mp_sub else None, subtitle=SxExpr(mp_sub) if mp_sub else None,
badges=SexpExpr(badges_wrap), badges=SxExpr(badges_wrap),
total=f"\u00a3{total:.2f}", total=f"\u00a3{total:.2f}",
) )
else: else:
# Orphan items # Orphan items
return sexp_call( return sx_call(
"cart-orphan-card", "cart-orphan-card",
badges=SexpExpr(badges_wrap), badges=SxExpr(badges_wrap),
total=f"\u00a3{total:.2f}", total=f"\u00a3{total:.2f}",
) )
def _empty_cart_sexp() -> str: def _empty_cart_sx() -> str:
"""Empty cart state.""" """Empty cart state."""
return sexp_call("cart-empty") return sx_call("cart-empty")
def _overview_main_panel_sexp(page_groups: list, ctx: dict) -> str: def _overview_main_panel_sx(page_groups: list, ctx: dict) -> str:
"""Cart overview main panel.""" """Cart overview main panel."""
if not page_groups: if not page_groups:
return _empty_cart_sexp() return _empty_cart_sx()
cards = [_page_group_card_sexp(grp, ctx) for grp in page_groups] cards = [_page_group_card_sx(grp, ctx) for grp in page_groups]
has_items = any(c for c in cards) has_items = any(c for c in cards)
if not has_items: if not has_items:
return _empty_cart_sexp() return _empty_cart_sx()
cards_sexp = "(<> " + " ".join(c for c in cards if c) + ")" cards_sx = "(<> " + " ".join(c for c in cards if c) + ")"
return sexp_call("cart-overview-panel", cards=SexpExpr(cards_sexp)) return sx_call("cart-overview-panel", cards=SxExpr(cards_sx))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Page cart # Page cart
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _cart_item_sexp(item: Any, ctx: dict) -> str: def _cart_item_sx(item: Any, ctx: dict) -> str:
"""Render a single product cart item.""" """Render a single product cart item."""
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
@@ -230,41 +230,41 @@ def _cart_item_sexp(item: Any, ctx: dict) -> str:
prod_url = market_product_url(slug) prod_url = market_product_url(slug)
if p.image: if p.image:
img = sexp_call("cart-item-img", src=p.image, alt=p.title) img = sx_call("cart-item-img", src=p.image, alt=p.title)
else: else:
img = sexp_call("cart-item-no-img") img = sx_call("cart-item-no-img")
price_parts = [] price_parts = []
if unit_price: if unit_price:
price_parts.append(sexp_call("cart-item-price", text=f"{symbol}{unit_price:.2f}")) price_parts.append(sx_call("cart-item-price", text=f"{symbol}{unit_price:.2f}"))
if p.special_price and p.special_price != p.regular_price: if p.special_price and p.special_price != p.regular_price:
price_parts.append(sexp_call("cart-item-price-was", text=f"{symbol}{p.regular_price:.2f}")) price_parts.append(sx_call("cart-item-price-was", text=f"{symbol}{p.regular_price:.2f}"))
else: else:
price_parts.append(sexp_call("cart-item-no-price")) price_parts.append(sx_call("cart-item-no-price"))
price_sexp = "(<> " + " ".join(price_parts) + ")" if len(price_parts) > 1 else price_parts[0] price_sx = "(<> " + " ".join(price_parts) + ")" if len(price_parts) > 1 else price_parts[0]
deleted_sexp = sexp_call("cart-item-deleted") if getattr(item, "is_deleted", False) else None deleted_sx = sx_call("cart-item-deleted") if getattr(item, "is_deleted", False) else None
brand_sexp = sexp_call("cart-item-brand", brand=p.brand) if getattr(p, "brand", None) else None brand_sx = sx_call("cart-item-brand", brand=p.brand) if getattr(p, "brand", None) else None
line_total_sexp = None line_total_sx = None
if unit_price: if unit_price:
lt = unit_price * item.quantity lt = unit_price * item.quantity
line_total_sexp = sexp_call("cart-item-line-total", text=f"Line total: {symbol}{lt:.2f}") line_total_sx = sx_call("cart-item-line-total", text=f"Line total: {symbol}{lt:.2f}")
return sexp_call( return sx_call(
"cart-item", "cart-item",
id=f"cart-item-{slug}", img=SexpExpr(img), prod_url=prod_url, title=p.title, id=f"cart-item-{slug}", img=SxExpr(img), prod_url=prod_url, title=p.title,
brand=SexpExpr(brand_sexp) if brand_sexp else None, brand=SxExpr(brand_sx) if brand_sx else None,
deleted=SexpExpr(deleted_sexp) if deleted_sexp else None, deleted=SxExpr(deleted_sx) if deleted_sx else None,
price=SexpExpr(price_sexp), price=SxExpr(price_sx),
qty_url=qty_url, csrf=csrf, minus=str(item.quantity - 1), qty_url=qty_url, csrf=csrf, minus=str(item.quantity - 1),
qty=str(item.quantity), plus=str(item.quantity + 1), qty=str(item.quantity), plus=str(item.quantity + 1),
line_total=SexpExpr(line_total_sexp) if line_total_sexp else None, line_total=SxExpr(line_total_sx) if line_total_sx else None,
) )
def _calendar_entries_sexp(entries: list) -> str: def _calendar_entries_sx(entries: list) -> str:
"""Render calendar booking entries in cart.""" """Render calendar booking entries in cart."""
if not entries: if not entries:
return "" return ""
@@ -275,15 +275,15 @@ def _calendar_entries_sexp(entries: list) -> str:
end = getattr(e, "end_at", None) end = getattr(e, "end_at", None)
cost = getattr(e, "cost", 0) or 0 cost = getattr(e, "cost", 0) or 0
end_str = f" \u2013 {end}" if end else "" end_str = f" \u2013 {end}" if end else ""
parts.append(sexp_call( parts.append(sx_call(
"cart-cal-entry", "cart-cal-entry",
name=name, date_str=f"{start}{end_str}", cost=f"\u00a3{cost:.2f}", name=name, date_str=f"{start}{end_str}", cost=f"\u00a3{cost:.2f}",
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
return sexp_call("cart-cal-section", items=SexpExpr(items_sexp)) return sx_call("cart-cal-section", items=SxExpr(items_sx))
def _ticket_groups_sexp(ticket_groups: list, ctx: dict) -> str: def _ticket_groups_sx(ticket_groups: list, ctx: dict) -> str:
"""Render ticket groups in cart.""" """Render ticket groups in cart."""
if not ticket_groups: if not ticket_groups:
return "" return ""
@@ -309,26 +309,26 @@ def _ticket_groups_sexp(ticket_groups: list, ctx: dict) -> str:
if end_at: if end_at:
date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}" date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}"
tt_name_sexp = sexp_call("cart-ticket-type-name", name=tt_name) if tt_name else None tt_name_sx = sx_call("cart-ticket-type-name", name=tt_name) if tt_name else None
tt_hidden_sexp = sexp_call("cart-ticket-type-hidden", value=str(tt_id)) if tt_id else None tt_hidden_sx = sx_call("cart-ticket-type-hidden", value=str(tt_id)) if tt_id else None
parts.append(sexp_call( parts.append(sx_call(
"cart-ticket-article", "cart-ticket-article",
name=name, name=name,
type_name=SexpExpr(tt_name_sexp) if tt_name_sexp else None, type_name=SxExpr(tt_name_sx) if tt_name_sx else None,
date_str=date_str, date_str=date_str,
price=f"\u00a3{price or 0:.2f}", qty_url=qty_url, csrf=csrf, price=f"\u00a3{price or 0:.2f}", qty_url=qty_url, csrf=csrf,
entry_id=str(entry_id), entry_id=str(entry_id),
type_hidden=SexpExpr(tt_hidden_sexp) if tt_hidden_sexp else None, type_hidden=SxExpr(tt_hidden_sx) if tt_hidden_sx else None,
minus=str(max(quantity - 1, 0)), qty=str(quantity), minus=str(max(quantity - 1, 0)), qty=str(quantity),
plus=str(quantity + 1), line_total=f"Line total: \u00a3{line_total:.2f}", plus=str(quantity + 1), line_total=f"Line total: \u00a3{line_total:.2f}",
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
return sexp_call("cart-tickets-section", items=SexpExpr(items_sexp)) return sx_call("cart-tickets-section", items=SxExpr(items_sx))
def _cart_summary_sexp(ctx: dict, cart: list, cal_entries: list, tickets: list, def _cart_summary_sx(ctx: dict, cart: list, cal_entries: list, tickets: list,
total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str: total_fn: Any, cal_total_fn: Any, ticket_total_fn: Any) -> str:
"""Render the order summary sidebar.""" """Render the order summary sidebar."""
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
@@ -360,41 +360,41 @@ def _cart_summary_sexp(ctx: dict, cart: list, cal_entries: list, tickets: list,
action = url_for("cart_global.checkout") action = url_for("cart_global.checkout")
from shared.utils import route_prefix from shared.utils import route_prefix
action = route_prefix() + action action = route_prefix() + action
checkout_sexp = sexp_call( checkout_sx = sx_call(
"cart-checkout-form", "cart-checkout-form",
action=action, csrf=csrf, label=f" Checkout as {user.email}", action=action, csrf=csrf, label=f" Checkout as {user.email}",
) )
else: else:
href = login_url(request.url) href = login_url(request.url)
checkout_sexp = sexp_call("cart-checkout-signin", href=href) checkout_sx = sx_call("cart-checkout-signin", href=href)
return sexp_call( return sx_call(
"cart-summary-panel", "cart-summary-panel",
item_count=str(item_count), subtotal=f"{symbol}{grand:.2f}", item_count=str(item_count), subtotal=f"{symbol}{grand:.2f}",
checkout=SexpExpr(checkout_sexp), checkout=SxExpr(checkout_sx),
) )
def _page_cart_main_panel_sexp(ctx: dict, cart: list, cal_entries: list, def _page_cart_main_panel_sx(ctx: dict, cart: list, cal_entries: list,
tickets: list, ticket_groups: list, tickets: list, ticket_groups: list,
total_fn: Any, cal_total_fn: Any, total_fn: Any, cal_total_fn: Any,
ticket_total_fn: Any) -> str: ticket_total_fn: Any) -> str:
"""Page cart main panel.""" """Page cart main panel."""
if not cart and not cal_entries and not tickets: if not cart and not cal_entries and not tickets:
return sexp_call("cart-page-empty") return sx_call("cart-page-empty")
item_parts = [_cart_item_sexp(item, ctx) for item in cart] item_parts = [_cart_item_sx(item, ctx) for item in cart]
items_sexp = "(<> " + " ".join(item_parts) + ")" if item_parts else '""' items_sx = "(<> " + " ".join(item_parts) + ")" if item_parts else '""'
cal_sexp = _calendar_entries_sexp(cal_entries) cal_sx = _calendar_entries_sx(cal_entries)
tickets_sexp = _ticket_groups_sexp(ticket_groups, ctx) tickets_sx = _ticket_groups_sx(ticket_groups, ctx)
summary_sexp = _cart_summary_sexp(ctx, cart, cal_entries, tickets, total_fn, cal_total_fn, ticket_total_fn) summary_sx = _cart_summary_sx(ctx, cart, cal_entries, tickets, total_fn, cal_total_fn, ticket_total_fn)
return sexp_call( return sx_call(
"cart-page-panel", "cart-page-panel",
items=SexpExpr(items_sexp), items=SxExpr(items_sx),
cal=SexpExpr(cal_sexp) if cal_sexp else None, cal=SxExpr(cal_sx) if cal_sx else None,
tickets=SexpExpr(tickets_sexp) if tickets_sexp else None, tickets=SxExpr(tickets_sx) if tickets_sx else None,
summary=SexpExpr(summary_sexp), summary=SxExpr(summary_sx),
) )
@@ -402,7 +402,7 @@ def _page_cart_main_panel_sexp(ctx: dict, cart: list, cal_entries: list,
# Orders list (same pattern as orders service) # Orders list (same pattern as orders service)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _order_row_sexp(order: Any, detail_url: str) -> str: def _order_row_sx(order: Any, detail_url: str) -> str:
"""Render a single order as desktop table row + mobile card.""" """Render a single order as desktop table row + mobile card."""
status = order.status or "pending" status = order.status or "pending"
sl = status.lower() sl = status.lower()
@@ -415,14 +415,14 @@ def _order_row_sexp(order: Any, detail_url: str) -> str:
created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014"
total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}" total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}"
desktop = sexp_call( desktop = sx_call(
"cart-order-row-desktop", "cart-order-row-desktop",
order_id=f"#{order.id}", created=created, desc=order.description or "", order_id=f"#{order.id}", created=created, desc=order.description or "",
total=total, pill=pill_cls, status=status, detail_url=detail_url, total=total, pill=pill_cls, status=status, detail_url=detail_url,
) )
mobile_pill = f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] {pill}" mobile_pill = f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] {pill}"
mobile = sexp_call( mobile = sx_call(
"cart-order-row-mobile", "cart-order-row-mobile",
order_id=f"#{order.id}", pill=mobile_pill, status=status, order_id=f"#{order.id}", pill=mobile_pill, status=status,
created=created, total=total, detail_url=detail_url, created=created, total=total, detail_url=detail_url,
@@ -431,47 +431,47 @@ def _order_row_sexp(order: Any, detail_url: str) -> str:
return "(<> " + desktop + " " + mobile + ")" return "(<> " + desktop + " " + mobile + ")"
def _orders_rows_sexp(orders: list, page: int, total_pages: int, def _orders_rows_sx(orders: list, page: int, total_pages: int,
url_for_fn: Any, qs_fn: Any) -> str: url_for_fn: Any, qs_fn: Any) -> str:
"""Render order rows + infinite scroll sentinel.""" """Render order rows + infinite scroll sentinel."""
from shared.utils import route_prefix from shared.utils import route_prefix
pfx = route_prefix() pfx = route_prefix()
parts = [ parts = [
_order_row_sexp(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id)) _order_row_sx(o, pfx + url_for_fn("orders.order.order_detail", order_id=o.id))
for o in orders for o in orders
] ]
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.list_orders") + qs_fn(page=page + 1)
parts.append(sexp_call( parts.append(sx_call(
"infinite-scroll", "infinite-scroll",
url=next_url, page=page, total_pages=total_pages, url=next_url, page=page, total_pages=total_pages,
id_prefix="orders", colspan=5, id_prefix="orders", colspan=5,
)) ))
else: else:
parts.append(sexp_call("cart-orders-end")) parts.append(sx_call("cart-orders-end"))
return "(<> " + " ".join(parts) + ")" return "(<> " + " ".join(parts) + ")"
def _orders_main_panel_sexp(orders: list, rows_sexp: str) -> str: def _orders_main_panel_sx(orders: list, rows_sx: str) -> str:
"""Main panel for orders list.""" """Main panel for orders list."""
if not orders: if not orders:
return sexp_call("cart-orders-empty") return sx_call("cart-orders-empty")
return sexp_call("cart-orders-table", rows=SexpExpr(rows_sexp)) return sx_call("cart-orders-table", rows=SxExpr(rows_sx))
def _orders_summary_sexp(ctx: dict) -> str: def _orders_summary_sx(ctx: dict) -> str:
"""Filter section for orders list.""" """Filter section for orders list."""
return sexp_call("cart-orders-filter", search_mobile=SexpExpr(search_mobile_sexp(ctx))) return sx_call("cart-orders-filter", search_mobile=SxExpr(search_mobile_sx(ctx)))
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Single order detail # Single order detail
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _order_items_sexp(order: Any) -> str: def _order_items_sx(order: Any) -> str:
"""Render order items list.""" """Render order items list."""
if not order or not order.items: if not order or not order.items:
return "" return ""
@@ -479,27 +479,27 @@ def _order_items_sexp(order: Any) -> str:
for item in order.items: for item in order.items:
prod_url = market_product_url(item.product_slug) prod_url = market_product_url(item.product_slug)
if item.product_image: if item.product_image:
img = sexp_call( img = sx_call(
"cart-order-item-img", "cart-order-item-img",
src=item.product_image, alt=item.product_title or "Product image", src=item.product_image, alt=item.product_title or "Product image",
) )
else: else:
img = sexp_call("cart-order-item-no-img") img = sx_call("cart-order-item-no-img")
parts.append(sexp_call( parts.append(sx_call(
"cart-order-item", "cart-order-item",
prod_url=prod_url, img=SexpExpr(img), prod_url=prod_url, img=SxExpr(img),
title=item.product_title or "Unknown product", title=item.product_title or "Unknown product",
product_id=f"Product ID: {item.product_id}", product_id=f"Product ID: {item.product_id}",
qty=f"Qty: {item.quantity}", qty=f"Qty: {item.quantity}",
price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}", price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}",
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
return sexp_call("cart-order-items-panel", items=SexpExpr(items_sexp)) return sx_call("cart-order-items-panel", items=SxExpr(items_sx))
def _order_summary_sexp(order: Any) -> str: def _order_summary_sx(order: Any) -> str:
"""Order summary card.""" """Order summary card."""
return sexp_call( return sx_call(
"order-summary-card", "order-summary-card",
order_id=order.id, order_id=order.id,
created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None, created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None,
@@ -508,7 +508,7 @@ def _order_summary_sexp(order: Any) -> str:
) )
def _order_calendar_items_sexp(calendar_entries: list | None) -> str: def _order_calendar_items_sx(calendar_entries: list | None) -> str:
"""Render calendar bookings for an order.""" """Render calendar bookings for an order."""
if not calendar_entries: if not calendar_entries:
return "" return ""
@@ -525,43 +525,43 @@ def _order_calendar_items_sexp(calendar_entries: list | None) -> str:
ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else "" ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else ""
if e.end_at: if e.end_at:
ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}" ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}"
parts.append(sexp_call( parts.append(sx_call(
"cart-order-cal-entry", "cart-order-cal-entry",
name=e.name, pill=pill_cls, status=st.capitalize(), name=e.name, pill=pill_cls, status=st.capitalize(),
date_str=ds, cost=f"\u00a3{e.cost or 0:.2f}", date_str=ds, cost=f"\u00a3{e.cost or 0:.2f}",
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
return sexp_call("cart-order-cal-section", items=SexpExpr(items_sexp)) return sx_call("cart-order-cal-section", items=SxExpr(items_sx))
def _order_main_sexp(order: Any, calendar_entries: list | None) -> str: def _order_main_sx(order: Any, calendar_entries: list | None) -> str:
"""Main panel for single order detail.""" """Main panel for single order detail."""
summary = _order_summary_sexp(order) summary = _order_summary_sx(order)
items = _order_items_sexp(order) items = _order_items_sx(order)
cal = _order_calendar_items_sexp(calendar_entries) cal = _order_calendar_items_sx(calendar_entries)
return sexp_call( return sx_call(
"cart-order-main", "cart-order-main",
summary=SexpExpr(summary), summary=SxExpr(summary),
items=SexpExpr(items) if items else None, items=SxExpr(items) if items else None,
cal=SexpExpr(cal) if cal else None, cal=SxExpr(cal) if cal else None,
) )
def _order_filter_sexp(order: Any, list_url: str, recheck_url: str, def _order_filter_sx(order: Any, list_url: str, recheck_url: str,
pay_url: str, csrf_token: str) -> str: pay_url: str, csrf_token: str) -> str:
"""Filter section for single order detail.""" """Filter section for single order detail."""
created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014"
status = order.status or "pending" status = order.status or "pending"
pay_sexp = None pay_sx = None
if status != "paid": if status != "paid":
pay_sexp = sexp_call("cart-order-pay-btn", url=pay_url) pay_sx = sx_call("cart-order-pay-btn", url=pay_url)
return sexp_call( return sx_call(
"cart-order-filter", "cart-order-filter",
info=f"Placed {created} \u00b7 Status: {status}", info=f"Placed {created} \u00b7 Status: {status}",
list_url=list_url, recheck_url=recheck_url, csrf=csrf_token, list_url=list_url, recheck_url=recheck_url, csrf=csrf_token,
pay=SexpExpr(pay_sexp) if pay_sexp else None, pay=SxExpr(pay_sx) if pay_sx else None,
) )
@@ -571,16 +571,16 @@ def _order_filter_sexp(order: Any, list_url: str, recheck_url: str,
async def render_overview_page(ctx: dict, page_groups: list) -> str: async def render_overview_page(ctx: dict, page_groups: list) -> str:
"""Full page: cart overview.""" """Full page: cart overview."""
main = _overview_main_panel_sexp(page_groups, ctx) main = _overview_main_panel_sx(page_groups, ctx)
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr, content=main) return full_page_sx(ctx, header_rows=hdr, content=main)
async def render_overview_oob(ctx: dict, page_groups: list) -> str: async def render_overview_oob(ctx: dict, page_groups: list) -> str:
"""OOB response for cart overview.""" """OOB response for cart overview."""
main = _overview_main_panel_sexp(page_groups, ctx) main = _overview_main_panel_sx(page_groups, ctx)
oobs = root_header_sexp(ctx, oob=True) oobs = root_header_sx(ctx, oob=True)
return oob_page_sexp(oobs=oobs, content=main) return oob_page_sx(oobs=oobs, content=main)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -592,17 +592,17 @@ async def render_page_cart_page(ctx: dict, page_post: Any,
ticket_groups: list, total_fn: Any, ticket_groups: list, total_fn: Any,
cal_total_fn: Any, ticket_total_fn: Any) -> str: cal_total_fn: Any, ticket_total_fn: Any) -> str:
"""Full page: page-specific cart.""" """Full page: page-specific cart."""
main = _page_cart_main_panel_sexp(ctx, cart, cal_entries, tickets, ticket_groups, main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups,
total_fn, cal_total_fn, ticket_total_fn) total_fn, cal_total_fn, ticket_total_fn)
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
child = _cart_header_sexp(ctx) child = _cart_header_sx(ctx)
page_hdr = _page_cart_header_sexp(ctx, page_post) page_hdr = _page_cart_header_sx(ctx, page_post)
nested = sexp_call( nested = sx_call(
"cart-header-child-nested", "cart-header-child-nested",
outer=SexpExpr(child), inner=SexpExpr(page_hdr), outer=SxExpr(child), inner=SxExpr(page_hdr),
) )
header_rows = "(<> " + hdr + " " + nested + ")" header_rows = "(<> " + hdr + " " + nested + ")"
return full_page_sexp(ctx, header_rows=header_rows, content=main) return full_page_sx(ctx, header_rows=header_rows, content=main)
async def render_page_cart_oob(ctx: dict, page_post: Any, async def render_page_cart_oob(ctx: dict, page_post: Any,
@@ -610,14 +610,14 @@ async def render_page_cart_oob(ctx: dict, page_post: Any,
ticket_groups: list, total_fn: Any, ticket_groups: list, total_fn: Any,
cal_total_fn: Any, ticket_total_fn: Any) -> str: cal_total_fn: Any, ticket_total_fn: Any) -> str:
"""OOB response for page cart.""" """OOB response for page cart."""
main = _page_cart_main_panel_sexp(ctx, cart, cal_entries, tickets, ticket_groups, main = _page_cart_main_panel_sx(ctx, cart, cal_entries, tickets, ticket_groups,
total_fn, cal_total_fn, ticket_total_fn) total_fn, cal_total_fn, ticket_total_fn)
child_oob = sexp_call("cart-header-child-oob", child_oob = sx_call("cart-header-child-oob",
inner=SexpExpr(_page_cart_header_sexp(ctx, page_post))) inner=SxExpr(_page_cart_header_sx(ctx, page_post)))
cart_hdr_oob = _cart_header_sexp(ctx, oob=True) cart_hdr_oob = _cart_header_sx(ctx, oob=True)
root_hdr_oob = root_header_sexp(ctx, oob=True) root_hdr_oob = root_header_sx(ctx, oob=True)
oobs = "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")" oobs = "(<> " + child_oob + " " + cart_hdr_oob + " " + root_hdr_oob + ")"
return oob_page_sexp(oobs=oobs, content=main) return oob_page_sx(oobs=oobs, content=main)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -635,21 +635,21 @@ async def render_orders_page(ctx: dict, orders: list, page: int,
ctx["search_count"] = search_count ctx["search_count"] = search_count
list_url = route_prefix() + url_for_fn("orders.list_orders") list_url = route_prefix() + url_for_fn("orders.list_orders")
rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
main = _orders_main_panel_sexp(orders, rows) main = _orders_main_panel_sx(orders, rows)
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
auth = _auth_header_sexp(ctx) auth = _auth_header_sx(ctx)
orders_hdr = _orders_header_sexp(ctx, list_url) orders_hdr = _orders_header_sx(ctx, list_url)
auth_child = sexp_call( auth_child = sx_call(
"cart-auth-header-child", "cart-auth-header-child",
auth=SexpExpr(auth), orders=SexpExpr(orders_hdr), auth=SxExpr(auth), orders=SxExpr(orders_hdr),
) )
header_rows = "(<> " + hdr + " " + auth_child + ")" header_rows = "(<> " + hdr + " " + auth_child + ")"
return full_page_sexp(ctx, header_rows=header_rows, return full_page_sx(ctx, header_rows=header_rows,
filter=_orders_summary_sexp(ctx), filter=_orders_summary_sx(ctx),
aside=search_desktop_sexp(ctx), aside=search_desktop_sx(ctx),
content=main) content=main)
@@ -657,7 +657,7 @@ async def render_orders_rows(ctx: dict, orders: list, page: int,
total_pages: int, url_for_fn: Any, total_pages: int, url_for_fn: Any,
qs_fn: Any) -> str: qs_fn: Any) -> str:
"""Pagination: just the table rows.""" """Pagination: just the table rows."""
return _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) return _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
async def render_orders_oob(ctx: dict, orders: list, page: int, async def render_orders_oob(ctx: dict, orders: list, page: int,
@@ -671,20 +671,20 @@ async def render_orders_oob(ctx: dict, orders: list, page: int,
ctx["search_count"] = search_count ctx["search_count"] = search_count
list_url = route_prefix() + url_for_fn("orders.list_orders") list_url = route_prefix() + url_for_fn("orders.list_orders")
rows = _orders_rows_sexp(orders, page, total_pages, url_for_fn, qs_fn) rows = _orders_rows_sx(orders, page, total_pages, url_for_fn, qs_fn)
main = _orders_main_panel_sexp(orders, rows) main = _orders_main_panel_sx(orders, rows)
auth_oob = _auth_header_sexp(ctx, oob=True) auth_oob = _auth_header_sx(ctx, oob=True)
auth_child_oob = sexp_call( auth_child_oob = sx_call(
"cart-auth-header-child-oob", "cart-auth-header-child-oob",
inner=SexpExpr(_orders_header_sexp(ctx, list_url)), inner=SxExpr(_orders_header_sx(ctx, list_url)),
) )
root_oob = root_header_sexp(ctx, oob=True) root_oob = root_header_sx(ctx, oob=True)
oobs = "(<> " + auth_oob + " " + auth_child_oob + " " + root_oob + ")" oobs = "(<> " + auth_oob + " " + auth_child_oob + " " + root_oob + ")"
return oob_page_sexp(oobs=oobs, return oob_page_sx(oobs=oobs,
filter=_orders_summary_sexp(ctx), filter=_orders_summary_sx(ctx),
aside=search_desktop_sexp(ctx), aside=search_desktop_sx(ctx),
content=main) content=main)
@@ -705,24 +705,24 @@ async def render_order_page(ctx: dict, order: Any,
recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) 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) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id)
main = _order_main_sexp(order, calendar_entries) main = _order_main_sx(order, calendar_entries)
filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token())
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
order_row = sexp_call( order_row = sx_call(
"menu-row-sx", "menu-row-sx",
id="order-row", level=3, colour="sky", id="order-row", level=3, colour="sky",
link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp",
) )
order_child = sexp_call( order_child = sx_call(
"cart-order-header-child", "cart-order-header-child",
auth=SexpExpr(_auth_header_sexp(ctx)), auth=SxExpr(_auth_header_sx(ctx)),
orders=SexpExpr(_orders_header_sexp(ctx, list_url)), orders=SxExpr(_orders_header_sx(ctx, list_url)),
order=SexpExpr(order_row), order=SxExpr(order_row),
) )
header_rows = "(<> " + hdr + " " + order_child + ")" header_rows = "(<> " + hdr + " " + order_child + ")"
return full_page_sexp(ctx, header_rows=header_rows, filter=filt, content=main) return full_page_sx(ctx, header_rows=header_rows, filter=filt, content=main)
async def render_order_oob(ctx: dict, order: Any, async def render_order_oob(ctx: dict, order: Any,
@@ -738,66 +738,66 @@ async def render_order_oob(ctx: dict, order: Any,
recheck_url = pfx + url_for_fn("orders.order.order_recheck", order_id=order.id) 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) pay_url = pfx + url_for_fn("orders.order.order_pay", order_id=order.id)
main = _order_main_sexp(order, calendar_entries) main = _order_main_sx(order, calendar_entries)
filt = _order_filter_sexp(order, list_url, recheck_url, pay_url, generate_csrf_token()) filt = _order_filter_sx(order, list_url, recheck_url, pay_url, generate_csrf_token())
order_row_oob = sexp_call( order_row_oob = sx_call(
"menu-row-sx", "menu-row-sx",
id="order-row", level=3, colour="sky", id="order-row", level=3, colour="sky",
link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp", link_href=detail_url, link_label=f"Order {order.id}", icon="fa fa-gbp",
oob=True, oob=True,
) )
orders_child_oob = sexp_call("cart-orders-header-child-oob", orders_child_oob = sx_call("cart-orders-header-child-oob",
inner=SexpExpr(order_row_oob)) inner=SxExpr(order_row_oob))
root_oob = root_header_sexp(ctx, oob=True) root_oob = root_header_sx(ctx, oob=True)
oobs = "(<> " + orders_child_oob + " " + root_oob + ")" oobs = "(<> " + orders_child_oob + " " + root_oob + ")"
return oob_page_sexp(oobs=oobs, filter=filt, content=main) return oob_page_sx(oobs=oobs, filter=filt, content=main)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public API: Checkout error # Public API: Checkout error
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _checkout_error_filter_sexp() -> str: def _checkout_error_filter_sx() -> str:
return sexp_call("cart-checkout-error-filter") return sx_call("cart-checkout-error-filter")
def _checkout_error_content_sexp(error: str | None, order: Any | None) -> str: def _checkout_error_content_sx(error: str | None, order: Any | None) -> str:
err_msg = error or "Unexpected error while creating the hosted checkout session." err_msg = error or "Unexpected error while creating the hosted checkout session."
order_sexp = None order_sx = None
if order: if order:
order_sexp = sexp_call("cart-checkout-error-order-id", order_id=f"#{order.id}") order_sx = sx_call("cart-checkout-error-order-id", order_id=f"#{order.id}")
back_url = cart_url("/") back_url = cart_url("/")
return sexp_call( return sx_call(
"cart-checkout-error-content", "cart-checkout-error-content",
error_msg=err_msg, error_msg=err_msg,
order=SexpExpr(order_sexp) if order_sexp else None, order=SxExpr(order_sx) if order_sx else None,
back_url=back_url, back_url=back_url,
) )
async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str:
"""Full page: checkout error.""" """Full page: checkout error."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
filt = _checkout_error_filter_sexp() filt = _checkout_error_filter_sx()
content = _checkout_error_content_sexp(error, order) content = _checkout_error_content_sx(error, order)
return full_page_sexp(ctx, header_rows=hdr, filter=filt, content=content) return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Page admin (/<page_slug>/admin/) # Page admin (/<page_slug>/admin/)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _cart_page_admin_header_sexp(ctx: dict, page_post: Any, *, oob: bool = False, def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
selected: str = "") -> str: selected: str = "") -> str:
"""Build the page-level admin header row -- delegates to shared helper.""" """Build the page-level admin header row -- delegates to shared helper."""
slug = page_post.slug if page_post else "" slug = page_post.slug if page_post else ""
ctx = _ensure_post_ctx(ctx, page_post) ctx = _ensure_post_ctx(ctx, page_post)
return post_admin_header_sexp(ctx, slug, oob=oob, selected=selected) return post_admin_header_sx(ctx, slug, oob=oob, selected=selected)
def _cart_admin_main_panel_sexp(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.payments")
@@ -809,7 +809,7 @@ def _cart_admin_main_panel_sexp(ctx: dict) -> str:
) )
def _cart_payments_main_panel_sexp(ctx: dict) -> str: def _cart_payments_main_panel_sx(ctx: dict) -> str:
"""Render SumUp payment config form.""" """Render SumUp payment config form."""
from quart import url_for from quart import url_for
csrf_token = ctx.get("csrf_token") csrf_token = ctx.get("csrf_token")
@@ -823,7 +823,7 @@ def _cart_payments_main_panel_sexp(ctx: dict) -> str:
placeholder = "--------" if sumup_configured else "sup_sk_..." placeholder = "--------" if sumup_configured else "sup_sk_..."
input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500" input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
return sexp_call("cart-payments-panel", return sx_call("cart-payments-panel",
update_url=update_url, csrf=csrf, update_url=update_url, csrf=csrf,
merchant_code=merchant_code, placeholder=placeholder, merchant_code=merchant_code, placeholder=placeholder,
input_cls=input_cls, sumup_configured=sumup_configured, input_cls=input_cls, sumup_configured=sumup_configured,
@@ -836,19 +836,19 @@ def _cart_payments_main_panel_sexp(ctx: dict) -> str:
async def render_cart_admin_page(ctx: dict, page_post: Any) -> str: async def render_cart_admin_page(ctx: dict, page_post: Any) -> str:
"""Full page: cart page admin overview.""" """Full page: cart page admin overview."""
content = _cart_admin_main_panel_sexp(ctx) content = _cart_admin_main_panel_sx(ctx)
root_hdr = root_header_sexp(ctx) root_hdr = root_header_sx(ctx)
post_hdr = await _post_header_sexp(ctx, page_post) post_hdr = await _post_header_sx(ctx, page_post)
admin_hdr = _cart_page_admin_header_sexp(ctx, page_post) admin_hdr = _cart_page_admin_header_sx(ctx, page_post)
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")" header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
return full_page_sexp(ctx, header_rows=header_rows, content=content) return full_page_sx(ctx, header_rows=header_rows, content=content)
async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str: async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str:
"""OOB response: cart page admin overview.""" """OOB response: cart page admin overview."""
content = _cart_admin_main_panel_sexp(ctx) content = _cart_admin_main_panel_sx(ctx)
oobs = _cart_page_admin_header_sexp(ctx, page_post, oob=True) oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True)
return oob_page_sexp(oobs=oobs, content=content) return oob_page_sx(oobs=oobs, content=content)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -857,21 +857,21 @@ async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str:
async def render_cart_payments_page(ctx: dict, page_post: Any) -> str: async def render_cart_payments_page(ctx: dict, page_post: Any) -> str:
"""Full page: payments config.""" """Full page: payments config."""
content = _cart_payments_main_panel_sexp(ctx) content = _cart_payments_main_panel_sx(ctx)
root_hdr = root_header_sexp(ctx) root_hdr = root_header_sx(ctx)
post_hdr = await _post_header_sexp(ctx, page_post) post_hdr = await _post_header_sx(ctx, page_post)
admin_hdr = _cart_page_admin_header_sexp(ctx, page_post, selected="payments") admin_hdr = _cart_page_admin_header_sx(ctx, page_post, selected="payments")
header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")" header_rows = "(<> " + root_hdr + " " + post_hdr + " " + admin_hdr + ")"
return full_page_sexp(ctx, header_rows=header_rows, content=content) return full_page_sx(ctx, header_rows=header_rows, content=content)
async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str: async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str:
"""OOB response: payments config.""" """OOB response: payments config."""
content = _cart_payments_main_panel_sexp(ctx) content = _cart_payments_main_panel_sx(ctx)
oobs = _cart_page_admin_header_sexp(ctx, page_post, oob=True, selected="payments") oobs = _cart_page_admin_header_sx(ctx, page_post, oob=True, selected="payments")
return oob_page_sexp(oobs=oobs, content=content) 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."""
return _cart_payments_main_panel_sexp(ctx) return _cart_payments_main_panel_sx(ctx)

View File

@@ -45,7 +45,7 @@ services:
- ./blog/alembic.ini:/app/blog/alembic.ini:ro - ./blog/alembic.ini:/app/blog/alembic.ini:ro
- ./blog/alembic:/app/blog/alembic:ro - ./blog/alembic:/app/blog/alembic:ro
- ./blog/app.py:/app/app.py - ./blog/app.py:/app/app.py
- ./blog/sexp:/app/sexp - ./blog/sx:/app/sx
- ./blog/bp:/app/bp - ./blog/bp:/app/bp
- ./blog/services:/app/services - ./blog/services:/app/services
- ./blog/templates:/app/templates - ./blog/templates:/app/templates
@@ -83,7 +83,7 @@ services:
- ./market/alembic.ini:/app/market/alembic.ini:ro - ./market/alembic.ini:/app/market/alembic.ini:ro
- ./market/alembic:/app/market/alembic:ro - ./market/alembic:/app/market/alembic:ro
- ./market/app.py:/app/app.py - ./market/app.py:/app/app.py
- ./market/sexp:/app/sexp - ./market/sx:/app/sx
- ./market/bp:/app/bp - ./market/bp:/app/bp
- ./market/services:/app/services - ./market/services:/app/services
- ./market/templates:/app/templates - ./market/templates:/app/templates
@@ -120,7 +120,7 @@ services:
- ./cart/alembic.ini:/app/cart/alembic.ini:ro - ./cart/alembic.ini:/app/cart/alembic.ini:ro
- ./cart/alembic:/app/cart/alembic:ro - ./cart/alembic:/app/cart/alembic:ro
- ./cart/app.py:/app/app.py - ./cart/app.py:/app/app.py
- ./cart/sexp:/app/sexp - ./cart/sx:/app/sx
- ./cart/bp:/app/bp - ./cart/bp:/app/bp
- ./cart/services:/app/services - ./cart/services:/app/services
- ./cart/templates:/app/templates - ./cart/templates:/app/templates
@@ -157,7 +157,7 @@ services:
- ./events/alembic.ini:/app/events/alembic.ini:ro - ./events/alembic.ini:/app/events/alembic.ini:ro
- ./events/alembic:/app/events/alembic:ro - ./events/alembic:/app/events/alembic:ro
- ./events/app.py:/app/app.py - ./events/app.py:/app/app.py
- ./events/sexp:/app/sexp - ./events/sx:/app/sx
- ./events/bp:/app/bp - ./events/bp:/app/bp
- ./events/services:/app/services - ./events/services:/app/services
- ./events/templates:/app/templates - ./events/templates:/app/templates
@@ -194,7 +194,7 @@ services:
- ./federation/alembic.ini:/app/federation/alembic.ini:ro - ./federation/alembic.ini:/app/federation/alembic.ini:ro
- ./federation/alembic:/app/federation/alembic:ro - ./federation/alembic:/app/federation/alembic:ro
- ./federation/app.py:/app/app.py - ./federation/app.py:/app/app.py
- ./federation/sexp:/app/sexp - ./federation/sx:/app/sx
- ./federation/bp:/app/bp - ./federation/bp:/app/bp
- ./federation/services:/app/services - ./federation/services:/app/services
- ./federation/templates:/app/templates - ./federation/templates:/app/templates
@@ -231,7 +231,7 @@ services:
- ./account/alembic.ini:/app/account/alembic.ini:ro - ./account/alembic.ini:/app/account/alembic.ini:ro
- ./account/alembic:/app/account/alembic:ro - ./account/alembic:/app/account/alembic:ro
- ./account/app.py:/app/app.py - ./account/app.py:/app/app.py
- ./account/sexp:/app/sexp - ./account/sx:/app/sx
- ./account/bp:/app/bp - ./account/bp:/app/bp
- ./account/services:/app/services - ./account/services:/app/services
- ./account/templates:/app/templates - ./account/templates:/app/templates
@@ -330,7 +330,7 @@ services:
- ./orders/alembic.ini:/app/orders/alembic.ini:ro - ./orders/alembic.ini:/app/orders/alembic.ini:ro
- ./orders/alembic:/app/orders/alembic:ro - ./orders/alembic:/app/orders/alembic:ro
- ./orders/app.py:/app/app.py - ./orders/app.py:/app/app.py
- ./orders/sexp:/app/sexp - ./orders/sx:/app/sx
- ./orders/bp:/app/bp - ./orders/bp:/app/bp
- ./orders/services:/app/services - ./orders/services:/app/services
- ./orders/templates:/app/templates - ./orders/templates:/app/templates
@@ -361,7 +361,7 @@ services:
- /root/rose-ash/_config/app-config.yaml:/app/config/app-config.yaml:ro - /root/rose-ash/_config/app-config.yaml:/app/config/app-config.yaml:ro
- ./shared:/app/shared - ./shared:/app/shared
- ./test/app.py:/app/app.py - ./test/app.py:/app/app.py
- ./test/sexp:/app/sexp - ./test/sx:/app/sx
- ./test/bp:/app/bp - ./test/bp:/app/bp
- ./test/services:/app/services - ./test/services:/app/services
- ./test/runner.py:/app/runner.py - ./test/runner.py:/app/runner.py

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from pathlib import Path from pathlib import Path
from quart import g, abort, request from quart import g, abort, request

View File

@@ -14,7 +14,7 @@ from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response from quart import Blueprint, g, request, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
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
@@ -66,13 +66,13 @@ def register() -> Blueprint:
entries, has_more, pending_tickets, page_info = await _load_entries(page) entries, has_more, pending_tickets, page_info = await _load_entries(page)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_all_events_page, render_all_events_oob from sx.sx_components import render_all_events_page, render_all_events_oob
ctx = await get_template_context() ctx = await get_template_context()
if is_htmx_request(): if is_htmx_request():
sexp_src = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view) sx_src = await render_all_events_oob(ctx, entries, has_more, pending_tickets, page_info, page, view)
return sexp_response(sexp_src) return sx_response(sx_src)
else: else:
html = await render_all_events_page(ctx, entries, has_more, pending_tickets, page_info, page, view) html = await render_all_events_page(ctx, entries, has_more, pending_tickets, page_info, page, view)
return await make_response(html, 200) return await make_response(html, 200)
@@ -84,9 +84,9 @@ def register() -> Blueprint:
entries, has_more, pending_tickets, page_info = await _load_entries(page) entries, has_more, pending_tickets, page_info = await _load_entries(page)
from sexp.sexp_components import render_all_events_cards from sx.sx_components import render_all_events_cards
sexp_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view) sx_src = await render_all_events_cards(entries, has_more, pending_tickets, page_info, page, view)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/all-tickets/adjust") @bp.post("/all-tickets/adjust")
async def adjust_ticket(): async def adjust_ticket():
@@ -125,9 +125,9 @@ def register() -> Blueprint:
if ident["session_id"] is not None: if ident["session_id"] is not None:
frag_params["session_id"] = ident["session_id"] frag_params["session_id"] = ident["session_id"]
from sexp.sexp_components import render_ticket_widget from sx.sx_components import render_ticket_widget
widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust") widget_html = render_ticket_widget(entry, qty, "/all-tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sexp_response(widget_html + (mini_html or "")) return sx_response(widget_html + (mini_html or ""))
return bp return bp

View File

@@ -7,7 +7,7 @@ from quart import (
from shared.browser.app.authz import require_admin from shared.browser.app.authz import require_admin
from shared.browser.app.redis_cacher import clear_cache from shared.browser.app.redis_cacher import clear_cache
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
@@ -20,24 +20,24 @@ def register():
async def admin(calendar_slug: str, **kwargs): async def admin(calendar_slug: str, **kwargs):
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_calendar_admin_page, render_calendar_admin_oob from sx.sx_components import render_calendar_admin_page, render_calendar_admin_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_calendar_admin_page(tctx) html = await render_calendar_admin_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_calendar_admin_oob(tctx) sx_src = await render_calendar_admin_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/description/") @bp.get("/description/")
@require_admin @require_admin
async def calendar_description_edit(calendar_slug: str, **kwargs): async def calendar_description_edit(calendar_slug: str, **kwargs):
from sexp.sexp_components import render_calendar_description_edit from sx.sx_components import render_calendar_description_edit
html = render_calendar_description_edit(g.calendar) html = render_calendar_description_edit(g.calendar)
return sexp_response(html) return sx_response(html)
@bp.post("/description/") @bp.post("/description/")
@@ -51,16 +51,16 @@ def register():
g.calendar.description = description g.calendar.description = description
await g.s.flush() await g.s.flush()
from sexp.sexp_components import render_calendar_description from sx.sx_components import render_calendar_description
html = render_calendar_description(g.calendar, oob=True) html = render_calendar_description(g.calendar, oob=True)
return sexp_response(html) return sx_response(html)
@bp.get("/description/view/") @bp.get("/description/view/")
@require_admin @require_admin
async def calendar_description_view(calendar_slug: str, **kwargs): async def calendar_description_view(calendar_slug: str, **kwargs):
from sexp.sexp_components import render_calendar_description from sx.sx_components import render_calendar_description
html = render_calendar_description(g.calendar) html = render_calendar_description(g.calendar)
return sexp_response(html) return sx_response(html)
return bp return bp

View File

@@ -24,7 +24,7 @@ from .services.calendar_view import (
update_calendar_description, update_calendar_description,
) )
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from ..slots.routes import register as register_slots from ..slots.routes import register as register_slots
@@ -157,8 +157,8 @@ def register():
user_entries = visible.user_entries user_entries = visible.user_entries
confirmed_entries = visible.confirmed_entries confirmed_entries = visible.confirmed_entries
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_calendar_page, render_calendar_oob from sx.sx_components import render_calendar_page, render_calendar_oob
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(dict( tctx.update(dict(
@@ -175,8 +175,8 @@ def register():
html = await render_calendar_page(tctx) html = await render_calendar_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_calendar_oob(tctx) sx_src = await render_calendar_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.put("/") @bp.put("/")
@@ -198,11 +198,11 @@ def register():
description = (form.get("description") or "").strip() description = (form.get("description") or "").strip()
await update_calendar_description(g.calendar, description) await update_calendar_description(g.calendar, description)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import _calendar_admin_main_panel_html from sx.sx_components import _calendar_admin_main_panel_html
ctx = await get_template_context() ctx = await get_template_context()
html = _calendar_admin_main_panel_html(ctx) html = _calendar_admin_main_panel_html(ctx)
return sexp_response(html) return sx_response(html)
@bp.delete("/") @bp.delete("/")
@@ -217,14 +217,14 @@ def register():
# If we have post context (blog-embedded mode), update nav # If we have post context (blog-embedded mode), update nav
post_data = getattr(g, "post_data", None) post_data = getattr(g, "post_data", None)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_calendars_list_panel from sx.sx_components import render_calendars_list_panel
ctx = await get_template_context() ctx = await get_template_context()
html = render_calendars_list_panel(ctx) html = render_calendars_list_panel(ctx)
if post_data: if post_data:
from shared.services.entry_associations import get_associated_entries from shared.services.entry_associations import get_associated_entries
from sexp.sexp_components import render_post_nav_entries_oob from sx.sx_components import render_post_nav_entries_oob
post_id = (post_data.get("post") or {}).get("id") post_id = (post_data.get("post") or {}).get("id")
cals = ( cals = (
@@ -239,7 +239,7 @@ def register():
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
html = html + nav_oob html = html + nav_oob
return sexp_response(html) return sx_response(html)
return bp return bp

View File

@@ -16,7 +16,7 @@ from .services.entries import (
) )
from shared.browser.app.authz import require_admin from shared.browser.app.authz import require_admin
from shared.browser.app.redis_cacher import clear_cache from shared.browser.app.redis_cacher import clear_cache
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from bp.calendar_entry.routes import register as register_calendar_entry from bp.calendar_entry.routes import register as register_calendar_entry
@@ -217,7 +217,7 @@ def register():
if ident["session_id"] is not None: if ident["session_id"] is not None:
frag_params["session_id"] = ident["session_id"] frag_params["session_id"] = ident["session_id"]
# Re-query day entries for the sexp component # Re-query day entries for the sx component
from datetime import date as date_cls, timedelta from datetime import date as date_cls, timedelta
from bp.calendar.services import get_visible_entries_for_period from bp.calendar.services import get_visible_entries_for_period
from quart import session as qsession from quart import session as qsession
@@ -258,10 +258,10 @@ def register():
"styles": styles, "styles": styles,
} }
from sexp.sexp_components import render_day_main_panel from sx.sx_components import render_day_main_panel
html = render_day_main_panel(ctx) html = render_day_main_panel(ctx)
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sexp_response(html + (mini_html or "")) return sx_response(html + (mini_html or ""))
@bp.get("/add/") @bp.get("/add/")
async def add_form(day: int, month: int, year: int, **kwargs): async def add_form(day: int, month: int, year: int, **kwargs):

View File

@@ -28,7 +28,7 @@ import math
import logging import logging
from shared.infrastructure.fragments import fetch_fragment from shared.infrastructure.fragments import fetch_fragment
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from ..ticket_types.routes import register as register_ticket_types from ..ticket_types.routes import register as register_ticket_types
@@ -111,7 +111,7 @@ def register():
) )
# Render OOB nav # Render OOB nav
from sexp.sexp_components import render_day_entries_nav_oob from sx.sx_components import render_day_entries_nav_oob
return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date) return render_day_entries_nav_oob(visible.confirmed_entries, calendar, day_date)
async def get_post_nav_oob(entry_id: int): async def get_post_nav_oob(entry_id: int):
@@ -148,7 +148,7 @@ def register():
).scalars().all() ).scalars().all()
# Render OOB nav for this post # Render OOB nav for this post
from sexp.sexp_components import render_post_nav_entries_oob from sx.sx_components import render_post_nav_entries_oob
nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post) nav_oob = render_post_nav_entries_oob(associated_entries, calendars, post)
nav_oobs.append(nav_oob) nav_oobs.append(nav_oob)
@@ -242,16 +242,16 @@ def register():
@require_admin @require_admin
async def get(entry_id: int, **rest): async def get(entry_id: int, **rest):
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_entry_page, render_entry_oob from sx.sx_components import render_entry_page, render_entry_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_entry_page(tctx) html = await render_entry_page(tctx)
return await make_response(html, 200) return await make_response(html, 200)
else: else:
sexp_src = await render_entry_oob(tctx) sx_src = await render_entry_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/edit/") @bp.get("/edit/")
@require_admin @require_admin
@@ -419,12 +419,12 @@ def register():
# Get nav OOB update # Get nav OOB update
nav_oob = await get_day_nav_oob(year, month, day) nav_oob = await get_day_nav_oob(year, month, day)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_entry_page from sx.sx_components import render_entry_page
tctx = await get_template_context() tctx = await get_template_context()
html = await render_entry_page(tctx) html = await render_entry_page(tctx)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
@bp.post("/confirm/") @bp.post("/confirm/")
@@ -448,9 +448,9 @@ def register():
# Re-read entry to get updated state # Re-read entry to get updated state
await g.s.refresh(g.entry) await g.s.refresh(g.entry)
from sexp.sexp_components import render_entry_optioned from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year) html = render_entry_optioned(g.entry, g.calendar, day, month, year)
return sexp_response(html + day_nav_oob + post_nav_oob) return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/decline/") @bp.post("/decline/")
@require_admin @require_admin
@@ -473,9 +473,9 @@ def register():
# Re-read entry to get updated state # Re-read entry to get updated state
await g.s.refresh(g.entry) await g.s.refresh(g.entry)
from sexp.sexp_components import render_entry_optioned from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year) html = render_entry_optioned(g.entry, g.calendar, day, month, year)
return sexp_response(html + day_nav_oob + post_nav_oob) return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/provisional/") @bp.post("/provisional/")
@require_admin @require_admin
@@ -498,9 +498,9 @@ def register():
# Re-read entry to get updated state # Re-read entry to get updated state
await g.s.refresh(g.entry) await g.s.refresh(g.entry)
from sexp.sexp_components import render_entry_optioned from sx.sx_components import render_entry_optioned
html = render_entry_optioned(g.entry, g.calendar, day, month, year) html = render_entry_optioned(g.entry, g.calendar, day, month, year)
return sexp_response(html + day_nav_oob + post_nav_oob) return sx_response(html + day_nav_oob + post_nav_oob)
@bp.post("/tickets/") @bp.post("/tickets/")
@require_admin @require_admin
@@ -542,9 +542,9 @@ def register():
# Return just the tickets fragment (targeted by hx-target="#entry-tickets-...") # Return just the tickets fragment (targeted by hx-target="#entry-tickets-...")
await g.s.refresh(g.entry) await g.s.refresh(g.entry)
from sexp.sexp_components import render_entry_tickets_config from sx.sx_components import render_entry_tickets_config
html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year")) html = render_entry_tickets_config(g.entry, g.calendar, request.view_args.get("day"), request.view_args.get("month"), request.view_args.get("year"))
return sexp_response(html) return sx_response(html)
@bp.get("/posts/search/") @bp.get("/posts/search/")
@require_admin @require_admin
@@ -593,11 +593,11 @@ def register():
entry_posts = await get_entry_posts(g.s, entry_id) entry_posts = await get_entry_posts(g.s, entry_id)
# Return updated posts list + OOB nav update # Return updated posts list + OOB nav update
from sexp.sexp_components import render_entry_posts_panel, render_entry_posts_nav_oob from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {} va = request.view_args or {}
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = render_entry_posts_nav_oob(entry_posts) nav_oob = render_entry_posts_nav_oob(entry_posts)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
@bp.delete("/posts/<int:post_id>/") @bp.delete("/posts/<int:post_id>/")
@require_admin @require_admin
@@ -615,10 +615,10 @@ def register():
entry_posts = await get_entry_posts(g.s, entry_id) entry_posts = await get_entry_posts(g.s, entry_id)
# Return updated posts list + OOB nav update # Return updated posts list + OOB nav update
from sexp.sexp_components import render_entry_posts_panel, render_entry_posts_nav_oob from sx.sx_components import render_entry_posts_panel, render_entry_posts_nav_oob
va = request.view_args or {} va = request.view_args or {}
html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year")) html = render_entry_posts_panel(entry_posts, g.entry, g.calendar, va.get("day"), va.get("month"), va.get("year"))
nav_oob = render_entry_posts_nav_oob(entry_posts) nav_oob = render_entry_posts_nav_oob(entry_posts)
return sexp_response(html + nav_oob) return sx_response(html + nav_oob)
return bp return bp

View File

@@ -16,7 +16,7 @@ from shared.browser.app.redis_cacher import cache_page, 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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -31,16 +31,16 @@ def register():
@bp.get("/") @bp.get("/")
@cache_page(tag="calendars") @cache_page(tag="calendars")
async def home(**kwargs): async def home(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_calendars_page, render_calendars_oob from sx.sx_components import render_calendars_page, render_calendars_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_calendars_page(ctx) html = await render_calendars_page(ctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_calendars_oob(ctx) sx_src = await render_calendars_oob(ctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/new/") @bp.post("/new/")
@@ -63,18 +63,18 @@ def register():
try: try:
await svc_create_calendar(g.s, post_id, name) await svc_create_calendar(g.s, post_id, name)
except Exception as e: except Exception as e:
from shared.sexp.jinja_bridge import render as render_comp from shared.sx.jinja_bridge import render as render_comp
return await make_response(render_comp("error-inline", message=str(e)), 422) return await make_response(render_comp("error-inline", message=str(e)), 422)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_calendars_list_panel from sx.sx_components import render_calendars_list_panel
ctx = await get_template_context() ctx = await get_template_context()
html = render_calendars_list_panel(ctx) html = render_calendars_list_panel(ctx)
# Blog-embedded mode: also update post nav # Blog-embedded mode: also update post nav
if post_data: if post_data:
from shared.services.entry_associations import get_associated_entries from shared.services.entry_associations import get_associated_entries
from sexp.sexp_components import render_post_nav_entries_oob from sx.sx_components import render_post_nav_entries_oob
cals = ( cals = (
await g.s.execute( await g.s.execute(
@@ -88,5 +88,5 @@ def register():
nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"]) nav_oob = render_post_nav_entries_oob(associated_entries, cals, post_data["post"])
html = html + nav_oob html = html + nav_oob
return sexp_response(html) return sx_response(html)
return bp return bp

View File

@@ -6,7 +6,7 @@ from quart import (
from shared.browser.app.authz import require_admin from shared.browser.app.authz import require_admin
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -18,14 +18,14 @@ def register():
async def admin(year: int, month: int, day: int, **kwargs): async def admin(year: int, month: int, day: int, **kwargs):
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_day_admin_page, render_day_admin_oob from sx.sx_components import render_day_admin_page, render_day_admin_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_day_admin_page(tctx) html = await render_day_admin_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_day_admin_oob(tctx) sx_src = await render_day_admin_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
return bp return bp

View File

@@ -18,7 +18,7 @@ from models.calendars import CalendarSlot # add this import
from sqlalchemy import select from sqlalchemy import select
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -122,16 +122,16 @@ def register():
- all confirmed + provisional + ordered entries for that day (all users) - all confirmed + provisional + ordered entries for that day (all users)
- pending only for current user/session - pending only for current user/session
""" """
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_day_page, render_day_oob from sx.sx_components import render_day_page, render_day_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_day_page(tctx) html = await render_day_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_day_oob(tctx) sx_src = await render_day_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/w/<widget_domain>/") @bp.get("/w/<widget_domain>/")
async def widget_paginate(widget_domain: str, **kwargs): async def widget_paginate(widget_domain: str, **kwargs):

View File

@@ -1,6 +1,6 @@
"""Events app fragment endpoints. """Events app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
""" """
@@ -31,9 +31,9 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
result = await handler() result = await handler()
ct = "text/html" if fragment_type in _html_types else "text/sexp" ct = "text/html" if fragment_type in _html_types else "text/sx"
return Response(result, status=200, content_type=ct) return Response(result, status=200, content_type=ct)
# --- container-nav fragment: calendar entries + calendar links ----------- # --- container-nav fragment: calendar entries + calendar links -----------
@@ -41,7 +41,7 @@ def register():
async def _container_nav_handler(): async def _container_nav_handler():
from quart import current_app from quart import current_app
from shared.infrastructure.urls import events_url from shared.infrastructure.urls import events_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
container_type = request.args.get("container_type", "page") container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0)) container_id = int(request.args.get("container_id", 0))
@@ -69,11 +69,11 @@ def register():
date_str = entry.start_at.strftime("%b %d, %Y at %H:%M") date_str = entry.start_at.strftime("%b %d, %Y at %H:%M")
if entry.end_at: if entry.end_at:
date_str += f" {entry.end_at.strftime('%H:%M')}" date_str += f" {entry.end_at.strftime('%H:%M')}"
parts.append(sexp_call("calendar-entry-nav", parts.append(sx_call("calendar-entry-nav",
href=events_url(entry_path), name=entry.name, href=events_url(entry_path), name=entry.name,
date_str=date_str, nav_class=nav_class)) date_str=date_str, nav_class=nav_class))
if has_more and paginate_url_base: if has_more and paginate_url_base:
parts.append(sexp_call("htmx-sentinel", parts.append(sx_call("htmx-sentinel",
id=f"entries-load-sentinel-{page}", id=f"entries-load-sentinel-{page}",
hx_get=f"{paginate_url_base}?page={page + 1}", hx_get=f"{paginate_url_base}?page={page + 1}",
hx_trigger="intersect once", hx_trigger="intersect once",
@@ -87,7 +87,7 @@ def register():
) )
for cal in calendars: for cal in calendars:
href = events_url(f"/{post_slug}/{cal.slug}/") href = events_url(f"/{post_slug}/{cal.slug}/")
parts.append(sexp_call("calendar-link-nav", parts.append(sx_call("calendar-link-nav",
href=href, name=cal.name, nav_class=nav_class)) href=href, name=cal.name, nav_class=nav_class))
if not parts: if not parts:
@@ -123,7 +123,7 @@ def register():
async def _account_nav_item_handler(): async def _account_nav_item_handler():
from quart import current_app from quart import current_app
from shared.infrastructure.urls import account_url from shared.infrastructure.urls import account_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
styles = current_app.jinja_env.globals.get("styles", {}) styles = current_app.jinja_env.globals.get("styles", {})
nav_class = styles.get("nav_button", "") nav_class = styles.get("nav_button", "")
@@ -135,7 +135,7 @@ def register():
bookings_url = account_url("/bookings/") bookings_url = account_url("/bookings/")
parts = [] parts = []
for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]: for href, label in [(tickets_url, "tickets"), (bookings_url, "bookings")]:
parts.append(sexp_call("nav-group-link", parts.append(sx_call("nav-group-link",
href=href, hx_select=hx_select, nav_class=nav_class, label=label)) href=href, hx_select=hx_select, nav_class=nav_class, label=label))
return "(<> " + " ".join(parts) + ")" return "(<> " + " ".join(parts) + ")"
@@ -169,13 +169,13 @@ def register():
async def _link_card_handler(): async def _link_card_handler():
from shared.infrastructure.urls import events_url from shared.infrastructure.urls import events_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
slug = request.args.get("slug", "") slug = request.args.get("slug", "")
keys_raw = request.args.get("keys", "") keys_raw = request.args.get("keys", "")
def _event_link_card_sexp(post, cal_names: str) -> str: def _event_link_card_sx(post, cal_names: str) -> str:
return sexp_call("link-card", return sx_call("link-card",
title=post.title, image=post.feature_image, title=post.title, image=post.feature_image,
subtitle=cal_names, subtitle=cal_names,
link=events_url(f"/{post.slug}")) link=events_url(f"/{post.slug}"))
@@ -193,7 +193,7 @@ def register():
g.s, "page", post.id, g.s, "page", post.id,
) )
cal_names = ", ".join(c.name for c in calendars) if calendars else "" cal_names = ", ".join(c.name for c in calendars) if calendars else ""
parts.append(_event_link_card_sexp(post, cal_names)) parts.append(_event_link_card_sx(post, cal_names))
return "\n".join(parts) return "\n".join(parts)
# Single mode # Single mode
@@ -207,7 +207,7 @@ def register():
g.s, "page", post.id, g.s, "page", post.id,
) )
cal_names = ", ".join(c.name for c in calendars) if calendars else "" cal_names = ", ".join(c.name for c in calendars) if calendars else ""
return _event_link_card_sexp(post, cal_names) return _event_link_card_sx(post, cal_names)
_handlers["link-card"] = _link_card_handler _handlers["link-card"] = _link_card_handler

View File

@@ -12,7 +12,7 @@ from .services.markets import (
from shared.browser.app.redis_cacher import cache_page, clear_cache from shared.browser.app.redis_cacher import cache_page, 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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -24,16 +24,16 @@ def register():
@bp.get("/") @bp.get("/")
async def home(**kwargs): async def home(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_markets_page, render_markets_oob from sx.sx_components import render_markets_page, render_markets_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_markets_page(ctx) html = await render_markets_page(ctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_markets_oob(ctx) sx_src = await render_markets_oob(ctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/new/") @bp.post("/new/")
@require_admin @require_admin
@@ -52,13 +52,13 @@ def register():
try: try:
await svc_create_market(g.s, post_id, name) await svc_create_market(g.s, post_id, name)
except Exception as e: except Exception as e:
from shared.sexp.jinja_bridge import render as render_comp from shared.sx.jinja_bridge import render as render_comp
return await make_response(render_comp("error-inline", message=str(e)), 422) return await make_response(render_comp("error-inline", message=str(e)), 422)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_markets_list_panel from sx.sx_components import render_markets_list_panel
ctx = await get_template_context() ctx = await get_template_context()
return sexp_response(render_markets_list_panel(ctx)) return sx_response(render_markets_list_panel(ctx))
@bp.delete("/<market_slug>/") @bp.delete("/<market_slug>/")
@require_admin @require_admin
@@ -68,9 +68,9 @@ def register():
if not deleted: if not deleted:
return await make_response("Market not found", 404) return await make_response("Market not found", 404)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_markets_list_panel from sx.sx_components import render_markets_list_panel
ctx = await get_template_context() ctx = await get_template_context()
return sexp_response(render_markets_list_panel(ctx)) return sx_response(render_markets_list_panel(ctx))
return bp return bp

View File

@@ -11,7 +11,7 @@ from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response from quart import Blueprint, g, request, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from shared.services.registry import services from shared.services.registry import services
@@ -46,13 +46,13 @@ def register() -> Blueprint:
entries, has_more, pending_tickets = await _load_entries(post["id"], page) entries, has_more, pending_tickets = await _load_entries(post["id"], page)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_page_summary_page, render_page_summary_oob from sx.sx_components import render_page_summary_page, render_page_summary_oob
ctx = await get_template_context() ctx = await get_template_context()
if is_htmx_request(): if is_htmx_request():
sexp_src = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view) sx_src = await render_page_summary_oob(ctx, entries, has_more, pending_tickets, {}, page, view)
return sexp_response(sexp_src) return sx_response(sx_src)
else: else:
html = await render_page_summary_page(ctx, entries, has_more, pending_tickets, {}, page, view) html = await render_page_summary_page(ctx, entries, has_more, pending_tickets, {}, page, view)
return await make_response(html, 200) return await make_response(html, 200)
@@ -65,9 +65,9 @@ def register() -> Blueprint:
entries, has_more, pending_tickets = await _load_entries(post["id"], page) entries, has_more, pending_tickets = await _load_entries(post["id"], page)
from sexp.sexp_components import render_page_summary_cards from sx.sx_components import render_page_summary_cards
sexp_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post) sx_src = await render_page_summary_cards(entries, has_more, pending_tickets, {}, page, view, post)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/tickets/adjust") @bp.post("/tickets/adjust")
async def adjust_ticket(): async def adjust_ticket():
@@ -106,9 +106,9 @@ def register() -> Blueprint:
if ident["session_id"] is not None: if ident["session_id"] is not None:
frag_params["session_id"] = ident["session_id"] frag_params["session_id"] = ident["session_id"]
from sexp.sexp_components import render_ticket_widget from sx.sx_components import render_ticket_widget
widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust") widget_html = render_ticket_widget(entry, qty, f"/{g.post_slug}/tickets/adjust")
mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False) mini_html = await fetch_fragment("cart", "cart-mini", params=frag_params, required=False)
return sexp_response(widget_html + (mini_html or "")) return sx_response(widget_html + (mini_html or ""))
return bp return bp

View File

@@ -24,7 +24,7 @@ from shared.browser.app.utils import (
parse_cost parse_cost
) )
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -75,8 +75,8 @@ def register():
slot = await svc_get_slot(g.s, slot_id) slot = await svc_get_slot(g.s, slot_id)
if not slot: if not slot:
return await make_response("Not found", 404) return await make_response("Not found", 404)
from sexp.sexp_components import render_slot_main_panel from sx.sx_components import render_slot_main_panel
return sexp_response(render_slot_main_panel(slot, g.calendar)) return sx_response(render_slot_main_panel(slot, g.calendar))
@bp.delete("/") @bp.delete("/")
@require_admin @require_admin
@@ -84,8 +84,8 @@ def register():
async def slot_delete(slot_id: int, **kwargs): async def slot_delete(slot_id: int, **kwargs):
await svc_delete_slot(g.s, slot_id) await svc_delete_slot(g.s, slot_id)
slots = await svc_list_slots(g.s, g.calendar.id) slots = await svc_list_slots(g.s, g.calendar.id)
from sexp.sexp_components import render_slots_table from sx.sx_components import render_slots_table
return sexp_response(render_slots_table(slots, g.calendar)) return sx_response(render_slots_table(slots, g.calendar))
@bp.put("/") @bp.put("/")
@require_admin @require_admin
@@ -166,8 +166,8 @@ def register():
} }
), 422 ), 422
from sexp.sexp_components import render_slot_main_panel from sx.sx_components import render_slot_main_panel
return sexp_response(render_slot_main_panel(slot, g.calendar, oob=True)) return sx_response(render_slot_main_panel(slot, g.calendar, oob=True))

View File

@@ -20,7 +20,7 @@ from shared.browser.app.utils import (
parse_cost parse_cost
) )
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -45,16 +45,16 @@ def register():
@bp.get("/") @bp.get("/")
async def get(**kwargs): async def get(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_slots_page, render_slots_oob from sx.sx_components import render_slots_page, render_slots_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_slots_page(tctx) html = await render_slots_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_slots_oob(tctx) sx_src = await render_slots_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/") @bp.post("/")
@@ -129,8 +129,8 @@ def register():
# Success → re-render the slots table # Success → re-render the slots table
slots = await svc_list_slots(g.s, g.calendar.id) slots = await svc_list_slots(g.s, g.calendar.id)
from sexp.sexp_components import render_slots_table from sx.sx_components import render_slots_table
return sexp_response(render_slots_table(slots, g.calendar)) return sx_response(render_slots_table(slots, g.calendar))
@bp.get("/add") @bp.get("/add")

View File

@@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload
from models.calendars import CalendarEntry, Ticket, TicketType from models.calendars import CalendarEntry, Ticket, TicketType
from shared.browser.app.authz import require_admin from shared.browser.app.authz import require_admin
from shared.browser.app.redis_cacher import clear_cache from shared.browser.app.redis_cacher import clear_cache
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from ..tickets.services.tickets import ( from ..tickets.services.tickets import (
get_ticket_by_code, get_ticket_by_code,
@@ -71,16 +71,16 @@ def register() -> Blueprint:
"reserved": reserved or 0, "reserved": reserved or 0,
} }
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_ticket_admin_page, render_ticket_admin_oob from sx.sx_components import render_ticket_admin_page, render_ticket_admin_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_ticket_admin_page(ctx, tickets, stats) html = await render_ticket_admin_page(ctx, tickets, stats)
return await make_response(html, 200) return await make_response(html, 200)
else: else:
sexp_src = await render_ticket_admin_oob(ctx, tickets, stats) sx_src = await render_ticket_admin_oob(ctx, tickets, stats)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/entry/<int:entry_id>/") @bp.get("/entry/<int:entry_id>/")
@require_admin @require_admin
@@ -101,9 +101,9 @@ def register() -> Blueprint:
tickets = await get_tickets_for_entry(g.s, entry_id) tickets = await get_tickets_for_entry(g.s, entry_id)
from sexp.sexp_components import render_entry_tickets_admin from sx.sx_components import render_entry_tickets_admin
html = render_entry_tickets_admin(entry, tickets) html = render_entry_tickets_admin(entry, tickets)
return sexp_response(html) return sx_response(html)
@bp.get("/lookup/") @bp.get("/lookup/")
@require_admin @require_admin
@@ -117,11 +117,11 @@ def register() -> Blueprint:
) )
ticket = await get_ticket_by_code(g.s, code) ticket = await get_ticket_by_code(g.s, code)
from sexp.sexp_components import render_lookup_result from sx.sx_components import render_lookup_result
if not ticket: if not ticket:
return sexp_response(render_lookup_result(None, "Ticket not found")) return sx_response(render_lookup_result(None, "Ticket not found"))
return sexp_response(render_lookup_result(ticket, None)) return sx_response(render_lookup_result(ticket, None))
@bp.post("/<code>/checkin/") @bp.post("/<code>/checkin/")
@require_admin @require_admin
@@ -130,11 +130,11 @@ def register() -> Blueprint:
"""Check in a ticket by its code.""" """Check in a ticket by its code."""
success, error = await checkin_ticket(g.s, code) success, error = await checkin_ticket(g.s, code)
from sexp.sexp_components import render_checkin_result from sx.sx_components import render_checkin_result
if not success: if not success:
return sexp_response(render_checkin_result(False, error, None)) return sx_response(render_checkin_result(False, error, None))
ticket = await get_ticket_by_code(g.s, code) ticket = await get_ticket_by_code(g.s, code)
return sexp_response(render_checkin_result(True, None, ticket)) return sx_response(render_checkin_result(True, None, ticket))
return bp return bp

View File

@@ -17,7 +17,7 @@ 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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -67,9 +67,9 @@ def register():
if not ticket_type: if not ticket_type:
return await make_response("Not found", 404) return await make_response("Not found", 404)
from sexp.sexp_components import render_ticket_type_main_panel from sx.sx_components import render_ticket_type_main_panel
va = request.view_args or {} va = request.view_args or {}
return sexp_response(render_ticket_type_main_panel( return sx_response(render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar, ticket_type, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"), va.get("day"), va.get("month"), va.get("year"),
)) ))
@@ -134,9 +134,9 @@ def register():
return await make_response("Not found", 404) return await make_response("Not found", 404)
# Return updated view with OOB flag # Return updated view with OOB flag
from sexp.sexp_components import render_ticket_type_main_panel from sx.sx_components import render_ticket_type_main_panel
va = request.view_args or {} va = request.view_args or {}
return sexp_response(render_ticket_type_main_panel( return sx_response(render_ticket_type_main_panel(
ticket_type, g.entry, g.calendar, ticket_type, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"), va.get("day"), va.get("month"), va.get("year"),
oob=True, oob=True,
@@ -153,9 +153,9 @@ def register():
# Re-render the ticket types list # Re-render the ticket types list
ticket_types = await svc_list_ticket_types(g.s, g.entry.id) ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
from sexp.sexp_components import render_ticket_types_table from sx.sx_components import render_ticket_types_table
va = request.view_args or {} va = request.view_args or {}
return sexp_response(render_ticket_types_table( return sx_response(render_ticket_types_table(
ticket_types, g.entry, g.calendar, ticket_types, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"), va.get("day"), va.get("month"), va.get("year"),
)) ))

View File

@@ -15,7 +15,7 @@ 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.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
@@ -110,9 +110,9 @@ def register():
# Success → re-render the ticket types table # Success → re-render the ticket types table
ticket_types = await svc_list_ticket_types(g.s, g.entry.id) ticket_types = await svc_list_ticket_types(g.s, g.entry.id)
from sexp.sexp_components import render_ticket_types_table from sx.sx_components import render_ticket_types_table
va = request.view_args or {} va = request.view_args or {}
return sexp_response(render_ticket_types_table( return sx_response(render_ticket_types_table(
ticket_types, g.entry, g.calendar, ticket_types, g.entry, g.calendar,
va.get("day"), va.get("month"), va.get("year"), va.get("day"), va.get("month"), va.get("year"),
)) ))

View File

@@ -20,7 +20,7 @@ from sqlalchemy.orm import selectinload
from models.calendars import CalendarEntry from models.calendars import CalendarEntry
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from shared.browser.app.redis_cacher import clear_cache from shared.browser.app.redis_cacher import clear_cache
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
from .services.tickets import ( from .services.tickets import (
create_ticket, create_ticket,
@@ -51,16 +51,16 @@ def register() -> Blueprint:
session_id=ident["session_id"], session_id=ident["session_id"],
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_tickets_page, render_tickets_oob from sx.sx_components import render_tickets_page, render_tickets_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_tickets_page(ctx, tickets) html = await render_tickets_page(ctx, tickets)
return await make_response(html, 200) return await make_response(html, 200)
else: else:
sexp_src = await render_tickets_oob(ctx, tickets) sx_src = await render_tickets_oob(ctx, tickets)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/<code>/") @bp.get("/<code>/")
async def ticket_detail(code: str): async def ticket_detail(code: str):
@@ -82,16 +82,16 @@ def register() -> Blueprint:
else: else:
return await make_response("Ticket not found", 404) return await make_response("Ticket not found", 404)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_ticket_detail_page, render_ticket_detail_oob from sx.sx_components import render_ticket_detail_page, render_ticket_detail_oob
ctx = await get_template_context() ctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_ticket_detail_page(ctx, ticket) html = await render_ticket_detail_page(ctx, ticket)
return await make_response(html, 200) return await make_response(html, 200)
else: else:
sexp_src = await render_ticket_detail_oob(ctx, ticket) sx_src = await render_ticket_detail_oob(ctx, ticket)
return sexp_response(sexp_src) 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")
@@ -182,8 +182,8 @@ def register() -> Blueprint:
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
cart_count = summary.count + summary.calendar_count + summary.ticket_count cart_count = summary.count + summary.calendar_count + summary.ticket_count
from sexp.sexp_components import render_buy_result from sx.sx_components import render_buy_result
return sexp_response(render_buy_result(entry, created, remaining, cart_count)) return sx_response(render_buy_result(entry, created, remaining, cart_count))
@bp.post("/adjust/") @bp.post("/adjust/")
@clear_cache(tag="calendars", tag_scope="all") @clear_cache(tag="calendars", tag_scope="all")
@@ -305,8 +305,8 @@ def register() -> Blueprint:
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO() summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
cart_count = summary.count + summary.calendar_count + summary.ticket_count cart_count = summary.count + summary.calendar_count + summary.ticket_count
from sexp.sexp_components import render_adjust_response from sx.sx_components import render_adjust_response
return sexp_response(render_adjust_response( return sx_response(render_adjust_response(
entry, ticket_remaining, ticket_sold_count, entry, ticket_remaining, ticket_sold_count,
user_ticket_count, user_ticket_counts_by_type, cart_count, user_ticket_count, user_ticket_counts_by_type, cart_count,
)) ))

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from pathlib import Path from pathlib import Path
from quart import g, request from quart import g, request
@@ -95,8 +95,8 @@ def create_app() -> "Quart":
@app.get("/") @app.get("/")
async def home(): async def home():
from quart import make_response from quart import make_response
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_federation_home from sx.sx_components import render_federation_home
ctx = await get_template_context() ctx = await get_template_context()
html = await render_federation_home(ctx) html = await render_federation_home(ctx)

View File

@@ -99,8 +99,8 @@ def register(url_prefix="/auth"):
# If there's a pending redirect (e.g. OAuth authorize), follow it # If there's a pending redirect (e.g. OAuth authorize), follow it
redirect_url = pop_login_redirect_target() redirect_url = pop_login_redirect_target()
return redirect(redirect_url) return redirect(redirect_url)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_login_page(ctx) return await render_login_page(ctx)
@@ -111,8 +111,8 @@ def register(url_prefix="/auth"):
is_valid, email = validate_email(email_input) is_valid, email = validate_email(email_input)
if not is_valid: if not is_valid:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error="Please enter a valid email address.", email=email_input) ctx = await get_template_context(error="Please enter a valid email address.", email=email_input)
return await render_login_page(ctx), 400 return await render_login_page(ctx), 400
@@ -132,8 +132,8 @@ def register(url_prefix="/auth"):
"Please try again in a moment." "Please try again in a moment."
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_check_email_page from sx.sx_components import render_check_email_page
ctx = await get_template_context(email=email, email_error=email_error) ctx = await get_template_context(email=email, email_error=email_error)
return await render_check_email_page(ctx) return await render_check_email_page(ctx)
@@ -148,15 +148,15 @@ def register(url_prefix="/auth"):
user, error = await validate_magic_link(s, token) user, error = await validate_magic_link(s, token)
if error: if error:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error=error) ctx = await get_template_context(error=error)
return await render_login_page(ctx), 400 return await render_login_page(ctx), 400
user_id = user.id user_id = user.id
except Exception: except Exception:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_login_page from sx.sx_components import render_login_page
ctx = await get_template_context(error="Could not sign you in right now. Please try again.") ctx = await get_template_context(error="Could not sign you in right now. Please try again.")
return await render_login_page(ctx), 502 return await render_login_page(ctx), 502

View File

@@ -1,6 +1,6 @@
"""Federation app fragment endpoints. """Federation app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
""" """
@@ -25,15 +25,15 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
src = await handler() src = await handler()
return Response(src, status=200, content_type="text/sexp") return Response(src, status=200, content_type="text/sx")
# --- link-card fragment: actor profile preview card -------------------------- # --- link-card fragment: actor profile preview card --------------------------
def _federation_link_card_sexp(actor, link: str) -> str: def _federation_link_card_sx(actor, link: str) -> str:
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
return sexp_call("link-card", return sx_call("link-card",
link=link, link=link,
title=actor.display_name or actor.preferred_username, title=actor.display_name or actor.preferred_username,
image=None, image=None,
@@ -59,7 +59,7 @@ def register():
parts.append(f"<!-- fragment:{u} -->") parts.append(f"<!-- fragment:{u} -->")
actor = await services.federation.get_actor_by_username(g.s, u) actor = await services.federation.get_actor_by_username(g.s, u)
if actor: if actor:
parts.append(_federation_link_card_sexp( parts.append(_federation_link_card_sx(
actor, federation_url(f"/users/{actor.preferred_username}"), actor, federation_url(f"/users/{actor.preferred_username}"),
)) ))
return "\n".join(parts) return "\n".join(parts)
@@ -71,7 +71,7 @@ def register():
actor = await services.federation.get_actor_by_username(g.s, lookup) actor = await services.federation.get_actor_by_username(g.s, lookup)
if not actor: if not actor:
return "" return ""
return _federation_link_card_sexp( return _federation_link_card_sx(
actor, federation_url(f"/users/{actor.preferred_username}"), actor, federation_url(f"/users/{actor.preferred_username}"),
) )

View File

@@ -39,8 +39,8 @@ def register(url_prefix="/identity"):
if actor: if actor:
return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username)) return redirect(url_for("activitypub.actor_profile", username=actor.preferred_username))
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_choose_username_page from sx.sx_components import render_choose_username_page
ctx = await get_template_context() ctx = await get_template_context()
ctx["actor"] = actor ctx["actor"] = actor
return await render_choose_username_page(ctx) return await render_choose_username_page(ctx)
@@ -71,8 +71,8 @@ def register(url_prefix="/identity"):
error = "This username is already taken." error = "This username is already taken."
if error: if error:
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_choose_username_page from sx.sx_components import render_choose_username_page
ctx = await get_template_context(error=error, username=username) ctx = await get_template_context(error=error, username=username)
ctx["actor"] = None ctx["actor"] = None
return await render_choose_username_page(ctx), 400 return await render_choose_username_page(ctx), 400

View File

@@ -7,7 +7,7 @@ from datetime import datetime
from quart import Blueprint, request, g, redirect, url_for, abort, Response from quart import Blueprint, request, g, redirect, url_for, abort, Response
from shared.services.registry import services from shared.services.registry import services
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@@ -40,8 +40,8 @@ def register(url_prefix="/social"):
return redirect(url_for("auth.login_form")) return redirect(url_for("auth.login_form"))
actor = _require_actor() actor = _require_actor()
items = await services.federation.get_home_timeline(g.s, actor.id) items = await services.federation.get_home_timeline(g.s, actor.id)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_timeline_page from sx.sx_components import render_timeline_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_timeline_page(ctx, items, "home", actor) return await render_timeline_page(ctx, items, "home", actor)
@@ -58,16 +58,16 @@ def register(url_prefix="/social"):
items = await services.federation.get_home_timeline( items = await services.federation.get_home_timeline(
g.s, actor.id, before=before, g.s, actor.id, before=before,
) )
from sexp.sexp_components import render_timeline_items from sx.sx_components import render_timeline_items
sexp_src = await render_timeline_items(items, "home", actor) sx_src = await render_timeline_items(items, "home", actor)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/public") @bp.get("/public")
async def public_timeline(): async def public_timeline():
items = await services.federation.get_public_timeline(g.s) items = await services.federation.get_public_timeline(g.s)
actor = getattr(g, "_social_actor", None) actor = getattr(g, "_social_actor", None)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_timeline_page from sx.sx_components import render_timeline_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_timeline_page(ctx, items, "public", actor) return await render_timeline_page(ctx, items, "public", actor)
@@ -82,9 +82,9 @@ def register(url_prefix="/social"):
pass pass
items = await services.federation.get_public_timeline(g.s, before=before) items = await services.federation.get_public_timeline(g.s, before=before)
actor = getattr(g, "_social_actor", None) actor = getattr(g, "_social_actor", None)
from sexp.sexp_components import render_timeline_items from sx.sx_components import render_timeline_items
sexp_src = await render_timeline_items(items, "public", actor) sx_src = await render_timeline_items(items, "public", actor)
return sexp_response(sexp_src) return sx_response(sx_src)
# -- Compose -------------------------------------------------------------- # -- Compose --------------------------------------------------------------
@@ -92,8 +92,8 @@ def register(url_prefix="/social"):
async def compose_form(): async def compose_form():
actor = _require_actor() actor = _require_actor()
reply_to = request.args.get("reply_to") reply_to = request.args.get("reply_to")
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_compose_page from sx.sx_components import render_compose_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_compose_page(ctx, actor, reply_to) return await render_compose_page(ctx, actor, reply_to)
@@ -138,8 +138,8 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000, g.s, actor.preferred_username, page=1, per_page=1000,
) )
followed_urls = {a.actor_url for a in following} followed_urls = {a.actor_url for a in following}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_search_page from sx.sx_components import render_search_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor) return await render_search_page(ctx, query, actors, total, 1, followed_urls, actor)
@@ -160,9 +160,9 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000, g.s, actor.preferred_username, page=1, per_page=1000,
) )
followed_urls = {a.actor_url for a in following} followed_urls = {a.actor_url for a in following}
from sexp.sexp_components import render_search_results from sx.sx_components import render_search_results
sexp_src = await render_search_results(actors, query, page, followed_urls, actor) sx_src = await render_search_results(actors, query, page, followed_urls, actor)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.post("/follow") @bp.post("/follow")
async def follow(): async def follow():
@@ -204,8 +204,8 @@ def register(url_prefix="/social"):
list_type = "followers" list_type = "followers"
else: else:
list_type = "following" list_type = "following"
from sexp.sexp_components import render_actor_card from sx.sx_components import render_actor_card
return sexp_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 ---------------------------------------------------------
@@ -293,8 +293,8 @@ def register(url_prefix="/social"):
).limit(1) ).limit(1)
)).scalar()) )).scalar())
from sexp.sexp_components import render_interaction_buttons from sx.sx_components import render_interaction_buttons
return sexp_response(render_interaction_buttons( return sx_response(render_interaction_buttons(
object_id=object_id, object_id=object_id,
author_inbox=author_inbox, author_inbox=author_inbox,
like_count=like_count, like_count=like_count,
@@ -312,8 +312,8 @@ def register(url_prefix="/social"):
actors, total = await services.federation.get_following( actors, total = await services.federation.get_following(
g.s, actor.preferred_username, g.s, actor.preferred_username,
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_following_page from sx.sx_components import render_following_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_following_page(ctx, actors, total, actor) return await render_following_page(ctx, actors, total, actor)
@@ -324,9 +324,9 @@ def register(url_prefix="/social"):
actors, total = await services.federation.get_following( actors, total = await services.federation.get_following(
g.s, actor.preferred_username, page=page, g.s, actor.preferred_username, page=page,
) )
from sexp.sexp_components import render_following_items from sx.sx_components import render_following_items
sexp_src = await render_following_items(actors, page, actor) sx_src = await render_following_items(actors, page, actor)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/followers") @bp.get("/followers")
async def followers_list(): async def followers_list():
@@ -339,8 +339,8 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000, g.s, actor.preferred_username, page=1, per_page=1000,
) )
followed_urls = {a.actor_url for a in following} followed_urls = {a.actor_url for a in following}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_followers_page from sx.sx_components import render_followers_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_followers_page(ctx, actors, total, followed_urls, actor) return await render_followers_page(ctx, actors, total, followed_urls, actor)
@@ -355,9 +355,9 @@ def register(url_prefix="/social"):
g.s, actor.preferred_username, page=1, per_page=1000, g.s, actor.preferred_username, page=1, per_page=1000,
) )
followed_urls = {a.actor_url for a in following} followed_urls = {a.actor_url for a in following}
from sexp.sexp_components import render_followers_items from sx.sx_components import render_followers_items
sexp_src = await render_followers_items(actors, page, followed_urls, actor) sx_src = await render_followers_items(actors, page, followed_urls, actor)
return sexp_response(sexp_src) return sx_response(sx_src)
@bp.get("/actor/<int:id>") @bp.get("/actor/<int:id>")
async def actor_timeline(id: int): async def actor_timeline(id: int):
@@ -388,8 +388,8 @@ def register(url_prefix="/social"):
) )
).scalar_one_or_none() ).scalar_one_or_none()
is_following = existing is not None is_following = existing is not None
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_actor_timeline_page from sx.sx_components import render_actor_timeline_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_actor_timeline_page(ctx, remote_dto, items, is_following, actor) return await render_actor_timeline_page(ctx, remote_dto, items, is_following, actor)
@@ -406,9 +406,9 @@ def register(url_prefix="/social"):
items = await services.federation.get_actor_timeline( items = await services.federation.get_actor_timeline(
g.s, id, before=before, g.s, id, before=before,
) )
from sexp.sexp_components import render_actor_timeline_items from sx.sx_components import render_actor_timeline_items
sexp_src = await render_actor_timeline_items(items, id, actor) sx_src = await render_actor_timeline_items(items, id, actor)
return sexp_response(sexp_src) return sx_response(sx_src)
# -- Notifications -------------------------------------------------------- # -- Notifications --------------------------------------------------------
@@ -417,8 +417,8 @@ def register(url_prefix="/social"):
actor = _require_actor() actor = _require_actor()
items = await services.federation.get_notifications(g.s, actor.id) items = await services.federation.get_notifications(g.s, actor.id)
await services.federation.mark_notifications_read(g.s, actor.id) await services.federation.mark_notifications_read(g.s, actor.id)
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_notifications_page from sx.sx_components import render_notifications_page
ctx = await get_template_context() ctx = await get_template_context()
return await render_notifications_page(ctx, items, actor) return await render_notifications_page(ctx, items, actor)
@@ -429,7 +429,7 @@ def register(url_prefix="/social"):
return Response("0", content_type="text/plain") return Response("0", content_type="text/plain")
count = await services.federation.unread_notification_count(g.s, actor.id) count = await services.federation.unread_notification_count(g.s, actor.id)
if count > 0: if count > 0:
from shared.sexp.jinja_bridge import render as render_comp from shared.sx.jinja_bridge import render as render_comp
return Response( return Response(
render_comp("notification-badge", count=str(count)), render_comp("notification-badge", count=str(count)),
content_type="text/html", content_type="text/html",

View File

@@ -10,13 +10,13 @@ import os
from typing import Any from typing import Any
from markupsafe import escape from markupsafe import escape
from shared.sexp.jinja_bridge import load_service_components from shared.sx.jinja_bridge import load_service_components
from shared.sexp.helpers import ( from shared.sx.helpers import (
sexp_call, SexpExpr, sx_call, SxExpr,
root_header_sexp, full_page_sexp, header_child_sexp, root_header_sx, full_page_sx, header_child_sx,
) )
# Load federation-specific .sexpr components at import time # Load federation-specific .sx components at import time
load_service_components(os.path.dirname(os.path.dirname(__file__))) load_service_components(os.path.dirname(os.path.dirname(__file__)))
@@ -24,13 +24,13 @@ load_service_components(os.path.dirname(os.path.dirname(__file__)))
# Social header nav # Social header nav
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _social_nav_sexp(actor: Any) -> str: def _social_nav_sx(actor: Any) -> str:
"""Build the social header nav bar content.""" """Build the social header nav bar content."""
from quart import url_for, request from quart import url_for, request
if not actor: if not actor:
choose_url = url_for("identity.choose_username_form") choose_url = url_for("identity.choose_username_form")
return sexp_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.home_timeline", "Timeline"),
@@ -45,7 +45,7 @@ def _social_nav_sexp(actor: Any) -> str:
for endpoint, label in links: for endpoint, label in links:
href = url_for(endpoint) href = url_for(endpoint)
bold = " font-bold" if request.path == href else "" bold = " font-bold" if request.path == href else ""
parts.append(sexp_call( parts.append(sx_call(
"federation-nav-link", "federation-nav-link",
href=href, href=href,
cls=f"px-2 py-1 rounded hover:bg-stone-200{bold}", cls=f"px-2 py-1 rounded hover:bg-stone-200{bold}",
@@ -56,7 +56,7 @@ def _social_nav_sexp(actor: Any) -> str:
notif_url = url_for("social.notifications") notif_url = url_for("social.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(sexp_call( parts.append(sx_call(
"federation-nav-notification-link", "federation-nav-notification-link",
href=notif_url, href=notif_url,
cls=f"px-2 py-1 rounded hover:bg-stone-200 relative{notif_bold}", cls=f"px-2 py-1 rounded hover:bg-stone-200 relative{notif_bold}",
@@ -65,31 +65,31 @@ def _social_nav_sexp(actor: Any) -> str:
# Profile link # Profile link
profile_url = url_for("activitypub.actor_profile", username=actor.preferred_username) profile_url = url_for("activitypub.actor_profile", username=actor.preferred_username)
parts.append(sexp_call( parts.append(sx_call(
"federation-nav-link", "federation-nav-link",
href=profile_url, href=profile_url,
cls="px-2 py-1 rounded hover:bg-stone-200", cls="px-2 py-1 rounded hover:bg-stone-200",
label=f"@{actor.preferred_username}", label=f"@{actor.preferred_username}",
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
return sexp_call("federation-nav-bar", items=SexpExpr(items_sexp)) return sx_call("federation-nav-bar", items=SxExpr(items_sx))
def _social_header_sexp(actor: Any) -> str: def _social_header_sx(actor: Any) -> str:
"""Build the social section header row.""" """Build the social section header row."""
nav_sexp = _social_nav_sexp(actor) nav_sx = _social_nav_sx(actor)
return sexp_call("federation-social-header", nav=SexpExpr(nav_sexp)) return sx_call("federation-social-header", nav=SxExpr(nav_sx))
def _social_page(ctx: dict, actor: Any, *, content: str, def _social_page(ctx: dict, actor: Any, *, content: str,
title: str = "Rose Ash", meta_html: str = "") -> str: title: str = "Rose Ash", meta_html: str = "") -> str:
"""Render a social page with header and content.""" """Render a social page with header and content."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
social_hdr = _social_header_sexp(actor) social_hdr = _social_header_sx(actor)
child = header_child_sexp(social_hdr) child = header_child_sx(social_hdr)
header_rows = "(<> " + hdr + " " + child + ")" header_rows = "(<> " + hdr + " " + child + ")"
return full_page_sexp(ctx, header_rows=header_rows, content=content, return full_page_sx(ctx, header_rows=header_rows, content=content,
meta_html=meta_html or f'<title>{escape(title)}</title>') meta_html=meta_html or f'<title>{escape(title)}</title>')
@@ -97,7 +97,7 @@ def _social_page(ctx: dict, actor: Any, *, content: str,
# Post card # Post card
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _interaction_buttons_sexp(item: Any, actor: Any) -> str: def _interaction_buttons_sx(item: Any, actor: Any) -> str:
"""Render like/boost/reply buttons for a post.""" """Render like/boost/reply buttons for a post."""
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
@@ -130,31 +130,31 @@ def _interaction_buttons_sexp(item: Any, actor: Any) -> str:
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.compose_form", reply_to=oid) if oid else ""
reply_sexp = sexp_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 = sexp_call( like_form = sx_call(
"federation-like-form", "federation-like-form",
action=like_action, target=target, oid=oid, ainbox=ainbox, action=like_action, target=target, oid=oid, ainbox=ainbox,
csrf=csrf, cls=f"flex items-center gap-1 {like_cls}", csrf=csrf, cls=f"flex items-center gap-1 {like_cls}",
icon=like_icon, count=str(lcount), icon=like_icon, count=str(lcount),
) )
boost_form = sexp_call( boost_form = sx_call(
"federation-boost-form", "federation-boost-form",
action=boost_action, target=target, oid=oid, ainbox=ainbox, action=boost_action, target=target, oid=oid, ainbox=ainbox,
csrf=csrf, cls=f"flex items-center gap-1 {boost_cls}", csrf=csrf, cls=f"flex items-center gap-1 {boost_cls}",
count=str(bcount), count=str(bcount),
) )
return sexp_call( return sx_call(
"federation-interaction-buttons", "federation-interaction-buttons",
like=SexpExpr(like_form), like=SxExpr(like_form),
boost=SexpExpr(boost_form), boost=SxExpr(boost_form),
reply=SexpExpr(reply_sexp) if reply_sexp else None, reply=SxExpr(reply_sx) if reply_sx else None,
) )
def _post_card_sexp(item: Any, actor: Any) -> str: def _post_card_sx(item: Any, actor: Any) -> str:
"""Render a single timeline post card.""" """Render a single timeline post card."""
boosted_by = getattr(item, "boosted_by", None) boosted_by = getattr(item, "boosted_by", None)
actor_icon = getattr(item, "actor_icon", None) actor_icon = getattr(item, "actor_icon", None)
@@ -167,15 +167,15 @@ def _post_card_sexp(item: Any, actor: Any) -> str:
url = getattr(item, "url", None) url = getattr(item, "url", None)
post_type = getattr(item, "post_type", "") post_type = getattr(item, "post_type", "")
boost_sexp = sexp_call( boost_sx = sx_call(
"federation-boost-label", name=str(escape(boosted_by)), "federation-boost-label", name=str(escape(boosted_by)),
) if boosted_by else "" ) if boosted_by else ""
if actor_icon: if actor_icon:
avatar = sexp_call("federation-avatar-img", src=actor_icon, cls="w-10 h-10 rounded-full") avatar = sx_call("federation-avatar-img", src=actor_icon, cls="w-10 h-10 rounded-full")
else: else:
initial = actor_name[0].upper() if actor_name else "?" initial = actor_name[0].upper() if actor_name else "?"
avatar = sexp_call( avatar = sx_call(
"federation-avatar-placeholder", "federation-avatar-placeholder",
cls="w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm", cls="w-10 h-10 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-sm",
initial=initial, initial=initial,
@@ -185,37 +185,37 @@ def _post_card_sexp(item: Any, actor: Any) -> str:
time_str = published.strftime("%b %d, %H:%M") if published else "" time_str = published.strftime("%b %d, %H:%M") if published else ""
if summary: if summary:
content_sexp = sexp_call( content_sx = sx_call(
"federation-content-cw", "federation-content-cw",
summary=str(escape(summary)), content=content, summary=str(escape(summary)), content=content,
) )
else: else:
content_sexp = sexp_call("federation-content-plain", content=content) content_sx = sx_call("federation-content-plain", content=content)
original_sexp = "" original_sx = ""
if url and post_type == "remote": if url and post_type == "remote":
original_sexp = sexp_call("federation-original-link", url=url) original_sx = sx_call("federation-original-link", url=url)
interactions_sexp = "" interactions_sx = ""
if actor: if actor:
oid = getattr(item, "object_id", "") or "" oid = getattr(item, "object_id", "") or ""
safe_id = oid.replace("/", "_").replace(":", "_") safe_id = oid.replace("/", "_").replace(":", "_")
interactions_sexp = sexp_call( interactions_sx = sx_call(
"federation-interactions-wrap", "federation-interactions-wrap",
id=f"interactions-{safe_id}", id=f"interactions-{safe_id}",
buttons=SexpExpr(_interaction_buttons_sexp(item, actor)), buttons=SxExpr(_interaction_buttons_sx(item, actor)),
) )
return sexp_call( return sx_call(
"federation-post-card", "federation-post-card",
boost=SexpExpr(boost_sexp) if boost_sexp else None, boost=SxExpr(boost_sx) if boost_sx else None,
avatar=SexpExpr(avatar), avatar=SxExpr(avatar),
actor_name=str(escape(actor_name)), actor_name=str(escape(actor_name)),
actor_username=str(escape(actor_username)), actor_username=str(escape(actor_username)),
domain=domain_str, time=time_str, domain=domain_str, time=time_str,
content=SexpExpr(content_sexp), content=SxExpr(content_sx),
original=SexpExpr(original_sexp) if original_sexp else None, original=SxExpr(original_sx) if original_sx else None,
interactions=SexpExpr(interactions_sexp) if interactions_sexp else None, interactions=SxExpr(interactions_sx) if interactions_sx else None,
) )
@@ -223,12 +223,12 @@ def _post_card_sexp(item: Any, actor: Any) -> str:
# Timeline items (pagination fragment) # Timeline items (pagination fragment)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _timeline_items_sexp(items: list, timeline_type: str, actor: Any, def _timeline_items_sx(items: list, timeline_type: str, actor: Any,
actor_id: int | None = None) -> str: actor_id: int | None = None) -> str:
"""Render timeline items with infinite scroll sentinel.""" """Render timeline items with infinite scroll sentinel."""
from quart import url_for from quart import url_for
parts = [_post_card_sexp(item, actor) for item in items] parts = [_post_card_sx(item, actor) for item in items]
if items: if items:
last = items[-1] last = items[-1]
@@ -237,7 +237,7 @@ def _timeline_items_sexp(items: list, timeline_type: str, actor: Any,
next_url = url_for("social.actor_timeline_page", id=actor_id, before=before) next_url = url_for("social.actor_timeline_page", id=actor_id, before=before)
else: else:
next_url = url_for(f"social.{timeline_type}_timeline_page", before=before) next_url = url_for(f"social.{timeline_type}_timeline_page", before=before)
parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else "" return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -246,7 +246,7 @@ def _timeline_items_sexp(items: list, timeline_type: str, actor: Any,
# Search results (pagination fragment) # Search results (pagination fragment)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _actor_card_sexp(a: Any, actor: Any, followed_urls: set, def _actor_card_sx(a: Any, actor: Any, followed_urls: set,
*, list_type: str = "search") -> str: *, list_type: str = "search") -> str:
"""Render a single actor card with follow/unfollow button.""" """Render a single actor card with follow/unfollow button."""
from shared.browser.app.csrf import generate_csrf_token from shared.browser.app.csrf import generate_csrf_token
@@ -264,10 +264,10 @@ def _actor_card_sexp(a: Any, actor: Any, followed_urls: set,
safe_id = actor_url.replace("/", "_").replace(":", "_") safe_id = actor_url.replace("/", "_").replace(":", "_")
if icon_url: if icon_url:
avatar = sexp_call("federation-actor-avatar-img", src=icon_url, cls="w-12 h-12 rounded-full") avatar = sx_call("federation-actor-avatar-img", src=icon_url, cls="w-12 h-12 rounded-full")
else: else:
initial = (display_name or username)[0].upper() if (display_name or username) else "?" initial = (display_name or username)[0].upper() if (display_name or username) else "?"
avatar = sexp_call( avatar = sx_call(
"federation-actor-avatar-placeholder", "federation-actor-avatar-placeholder",
cls="w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold", cls="w-12 h-12 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold",
initial=initial, initial=initial,
@@ -275,69 +275,69 @@ def _actor_card_sexp(a: Any, actor: Any, followed_urls: set,
# Name link # Name link
if (list_type in ("following", "search")) and aid: if (list_type in ("following", "search")) and aid:
name_sexp = sexp_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.actor_timeline", id=aid),
name=str(escape(display_name)), name=str(escape(display_name)),
) )
else: else:
name_sexp = sexp_call( name_sx = sx_call(
"federation-actor-name-link-external", "federation-actor-name-link-external",
href=f"https://{domain}/@{username}", href=f"https://{domain}/@{username}",
name=str(escape(display_name)), name=str(escape(display_name)),
) )
summary_sexp = sexp_call("federation-actor-summary", summary=summary) if summary else "" summary_sx = sx_call("federation-actor-summary", summary=summary) if summary else ""
# Follow/unfollow button # Follow/unfollow button
button_sexp = "" button_sx = ""
if actor: if actor:
is_followed = actor_url in (followed_urls or set()) is_followed = actor_url in (followed_urls or set())
if list_type == "following" or is_followed: if list_type == "following" or is_followed:
button_sexp = sexp_call( button_sx = sx_call(
"federation-unfollow-button", "federation-unfollow-button",
action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url, action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url,
) )
else: else:
label = "Follow Back" if list_type == "followers" else "Follow" label = "Follow Back" if list_type == "followers" else "Follow"
button_sexp = sexp_call( button_sx = sx_call(
"federation-follow-button", "federation-follow-button",
action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label=label, action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, label=label,
) )
return sexp_call( return sx_call(
"federation-actor-card", "federation-actor-card",
cls="bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4", cls="bg-white rounded-lg shadow-sm border border-stone-200 p-4 mb-3 flex items-center gap-4",
id=f"actor-{safe_id}", id=f"actor-{safe_id}",
avatar=SexpExpr(avatar), avatar=SxExpr(avatar),
name=SexpExpr(name_sexp), name=SxExpr(name_sx),
username=str(escape(username)), domain=str(escape(domain)), username=str(escape(username)), domain=str(escape(domain)),
summary=SexpExpr(summary_sexp) if summary_sexp else None, summary=SxExpr(summary_sx) if summary_sx else None,
button=SexpExpr(button_sexp) if button_sexp else None, button=SxExpr(button_sx) if button_sx else None,
) )
def _search_results_sexp(actors: list, query: str, page: int, def _search_results_sx(actors: list, query: str, page: int,
followed_urls: set, actor: Any) -> str: followed_urls: set, actor: Any) -> str:
"""Render search results with pagination sentinel.""" """Render search results with pagination sentinel."""
from quart import url_for from quart import url_for
parts = [_actor_card_sexp(a, actor, followed_urls, list_type="search") for a in actors] parts = [_actor_card_sx(a, actor, followed_urls, list_type="search") for a in actors]
if len(actors) >= 20: if len(actors) >= 20:
next_url = url_for("social.search_page", q=query, page=page + 1) next_url = url_for("social.search_page", q=query, page=page + 1)
parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else "" return "(<> " + " ".join(parts) + ")" if parts else ""
def _actor_list_items_sexp(actors: list, page: int, list_type: str, def _actor_list_items_sx(actors: list, page: int, list_type: str,
followed_urls: set, actor: Any) -> str: followed_urls: set, actor: Any) -> str:
"""Render actor list items (following/followers) with pagination sentinel.""" """Render actor list items (following/followers) with pagination sentinel."""
from quart import url_for from quart import url_for
parts = [_actor_card_sexp(a, actor, followed_urls, list_type=list_type) for a in actors] parts = [_actor_card_sx(a, actor, followed_urls, list_type=list_type) for a in actors]
if len(actors) >= 20: if len(actors) >= 20:
next_url = url_for(f"social.{list_type}_list_page", page=page + 1) next_url = url_for(f"social.{list_type}_list_page", page=page + 1)
parts.append(sexp_call("federation-scroll-sentinel", url=next_url)) parts.append(sx_call("federation-scroll-sentinel", url=next_url))
return "(<> " + " ".join(parts) + ")" if parts else "" return "(<> " + " ".join(parts) + ")" if parts else ""
@@ -345,7 +345,7 @@ def _actor_list_items_sexp(actors: list, page: int, list_type: str,
# Notification card # Notification card
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def _notification_sexp(notif: Any) -> str: def _notification_sx(notif: Any) -> str:
"""Render a single notification.""" """Render a single notification."""
from_name = getattr(notif, "from_actor_name", "?") from_name = getattr(notif, "from_actor_name", "?")
from_username = getattr(notif, "from_actor_username", "") from_username = getattr(notif, "from_actor_username", "")
@@ -360,10 +360,10 @@ def _notification_sexp(notif: Any) -> str:
border = " border-l-4 border-l-stone-400" if not read else "" border = " border-l-4 border-l-stone-400" if not read else ""
if from_icon: if from_icon:
avatar = sexp_call("federation-avatar-img", src=from_icon, cls="w-8 h-8 rounded-full") avatar = sx_call("federation-avatar-img", src=from_icon, cls="w-8 h-8 rounded-full")
else: else:
initial = from_name[0].upper() if from_name else "?" initial = from_name[0].upper() if from_name else "?"
avatar = sexp_call( avatar = sx_call(
"federation-avatar-placeholder", "federation-avatar-placeholder",
cls="w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs", cls="w-8 h-8 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xs",
initial=initial, initial=initial,
@@ -382,19 +382,19 @@ def _notification_sexp(notif: Any) -> str:
if ntype == "follow" and app_domain and app_domain != "federation": if ntype == "follow" and app_domain and app_domain != "federation":
action += f" on {escape(app_domain)}" action += f" on {escape(app_domain)}"
preview_sexp = sexp_call( preview_sx = sx_call(
"federation-notification-preview", preview=str(escape(preview)), "federation-notification-preview", preview=str(escape(preview)),
) if preview else "" ) if preview else ""
time_str = created.strftime("%b %d, %H:%M") if created else "" time_str = created.strftime("%b %d, %H:%M") if created else ""
return sexp_call( return sx_call(
"federation-notification-card", "federation-notification-card",
cls=f"bg-white rounded-lg shadow-sm border border-stone-200 p-4{border}", cls=f"bg-white rounded-lg shadow-sm border border-stone-200 p-4{border}",
avatar=SexpExpr(avatar), avatar=SxExpr(avatar),
from_name=str(escape(from_name)), from_name=str(escape(from_name)),
from_username=str(escape(from_username)), from_username=str(escape(from_username)),
from_domain=domain_str, action_text=action, from_domain=domain_str, action_text=action,
preview=SexpExpr(preview_sexp) if preview_sexp else None, preview=SxExpr(preview_sx) if preview_sx else None,
time=time_str, time=time_str,
) )
@@ -405,8 +405,8 @@ def _notification_sexp(notif: Any) -> str:
async def render_federation_home(ctx: dict) -> str: async def render_federation_home(ctx: dict) -> str:
"""Full page: federation home (minimal).""" """Full page: federation home (minimal)."""
hdr = root_header_sexp(ctx) hdr = root_header_sx(ctx)
return full_page_sexp(ctx, header_rows=hdr) return full_page_sx(ctx, header_rows=hdr)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -423,11 +423,11 @@ async def render_login_page(ctx: dict) -> str:
action = url_for("auth.start_login") action = url_for("auth.start_login")
csrf = generate_csrf_token() csrf = generate_csrf_token()
error_sexp = sexp_call("federation-error-banner", error=error) if error else "" error_sx = sx_call("federation-error-banner", error=error) if error else ""
content = sexp_call( content = sx_call(
"federation-login-form", "federation-login-form",
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
action=action, csrf=csrf, action=action, csrf=csrf,
email=str(escape(email)), email=str(escape(email)),
) )
@@ -441,14 +441,14 @@ async def render_check_email_page(ctx: dict) -> str:
email = ctx.get("email", "") email = ctx.get("email", "")
email_error = ctx.get("email_error") email_error = ctx.get("email_error")
error_sexp = sexp_call( error_sx = sx_call(
"federation-check-email-error", error=str(escape(email_error)), "federation-check-email-error", error=str(escape(email_error)),
) if email_error else "" ) if email_error else ""
content = sexp_call( content = sx_call(
"federation-check-email", "federation-check-email",
email=str(escape(email)), email=str(escape(email)),
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
) )
return _social_page(ctx, None, content=content, return _social_page(ctx, None, content=content,
@@ -465,18 +465,18 @@ async def render_timeline_page(ctx: dict, items: list, timeline_type: str,
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_sexp = "" compose_sx = ""
if actor: if actor:
compose_url = url_for("social.compose_form") compose_url = url_for("social.compose_form")
compose_sexp = sexp_call("federation-compose-button", url=compose_url) compose_sx = sx_call("federation-compose-button", url=compose_url)
timeline_sexp = _timeline_items_sexp(items, timeline_type, actor) timeline_sx = _timeline_items_sx(items, timeline_type, actor)
content = sexp_call( content = sx_call(
"federation-timeline-page", "federation-timeline-page",
label=label, label=label,
compose=SexpExpr(compose_sexp) if compose_sexp else None, compose=SxExpr(compose_sx) if compose_sx else None,
timeline=SexpExpr(timeline_sexp) if timeline_sexp else None, timeline=SxExpr(timeline_sx) if timeline_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
@@ -486,7 +486,7 @@ async def render_timeline_page(ctx: dict, items: list, timeline_type: str,
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:
"""Pagination fragment: timeline items.""" """Pagination fragment: timeline items."""
return _timeline_items_sexp(items, timeline_type, actor, actor_id) return _timeline_items_sx(items, timeline_type, actor, actor_id)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -501,17 +501,17 @@ async def render_compose_page(ctx: dict, actor: Any, reply_to: str | None) -> st
csrf = generate_csrf_token() csrf = generate_csrf_token()
action = url_for("social.compose_submit") action = url_for("social.compose_submit")
reply_sexp = "" reply_sx = ""
if reply_to: if reply_to:
reply_sexp = sexp_call( reply_sx = sx_call(
"federation-compose-reply", "federation-compose-reply",
reply_to=str(escape(reply_to)), reply_to=str(escape(reply_to)),
) )
content = sexp_call( content = sx_call(
"federation-compose-form", "federation-compose-form",
action=action, csrf=csrf, action=action, csrf=csrf,
reply=SexpExpr(reply_sexp) if reply_sexp else None, reply=SxExpr(reply_sx) if reply_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
@@ -530,29 +530,29 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int,
search_url = url_for("social.search") search_url = url_for("social.search")
search_page_url = url_for("social.search_page") search_page_url = url_for("social.search_page")
results_sexp = _search_results_sexp(actors, query, page, followed_urls, actor) results_sx = _search_results_sx(actors, query, page, followed_urls, actor)
info_sexp = "" info_sx = ""
if query and total: if query and total:
s = "s" if total != 1 else "" s = "s" if total != 1 else ""
info_sexp = sexp_call( info_sx = sx_call(
"federation-search-info", "federation-search-info",
cls="text-sm text-stone-500 mb-4", cls="text-sm text-stone-500 mb-4",
text=f"{total} result{s} for <strong>{escape(query)}</strong>", text=f"{total} result{s} for <strong>{escape(query)}</strong>",
) )
elif query: elif query:
info_sexp = sexp_call( info_sx = sx_call(
"federation-search-info", "federation-search-info",
cls="text-stone-500 mb-4", cls="text-stone-500 mb-4",
text=f"No results found for <strong>{escape(query)}</strong>", text=f"No results found for <strong>{escape(query)}</strong>",
) )
content = sexp_call( content = 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)),
info=SexpExpr(info_sexp) if info_sexp else None, info=SxExpr(info_sx) if info_sx else None,
results=SexpExpr(results_sexp) if results_sexp else None, results=SxExpr(results_sx) if results_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
@@ -562,7 +562,7 @@ async def render_search_page(ctx: dict, query: str, actors: list, total: int,
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:
"""Pagination fragment: search results.""" """Pagination fragment: search results."""
return _search_results_sexp(actors, query, page, followed_urls, actor) return _search_results_sx(actors, query, page, followed_urls, actor)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -572,11 +572,11 @@ async def render_search_results(actors: list, query: str, page: int,
async def render_following_page(ctx: dict, actors: list, total: int, async def render_following_page(ctx: dict, actors: list, total: int,
actor: Any) -> str: actor: Any) -> str:
"""Full page: following list.""" """Full page: following list."""
items_sexp = _actor_list_items_sexp(actors, 1, "following", set(), actor) items_sx = _actor_list_items_sx(actors, 1, "following", set(), actor)
content = sexp_call( content = sx_call(
"federation-actor-list-page", "federation-actor-list-page",
title="Following", count_str=f"({total})", title="Following", count_str=f"({total})",
items=SexpExpr(items_sexp) if items_sexp else None, items=SxExpr(items_sx) if items_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
title="Following \u2014 Rose Ash") title="Following \u2014 Rose Ash")
@@ -584,17 +584,17 @@ async def render_following_page(ctx: dict, actors: list, total: int,
async def render_following_items(actors: list, page: int, actor: Any) -> str: async def render_following_items(actors: list, page: int, actor: Any) -> str:
"""Pagination fragment: following items.""" """Pagination fragment: following items."""
return _actor_list_items_sexp(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, async def render_followers_page(ctx: dict, actors: list, total: int,
followed_urls: set, actor: Any) -> str: followed_urls: set, actor: Any) -> str:
"""Full page: followers list.""" """Full page: followers list."""
items_sexp = _actor_list_items_sexp(actors, 1, "followers", followed_urls, actor) items_sx = _actor_list_items_sx(actors, 1, "followers", followed_urls, actor)
content = sexp_call( content = sx_call(
"federation-actor-list-page", "federation-actor-list-page",
title="Followers", count_str=f"({total})", title="Followers", count_str=f"({total})",
items=SexpExpr(items_sexp) if items_sexp else None, items=SxExpr(items_sx) if items_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
title="Followers \u2014 Rose Ash") title="Followers \u2014 Rose Ash")
@@ -603,7 +603,7 @@ async def render_followers_page(ctx: dict, actors: list, total: int,
async def render_followers_items(actors: list, page: int, async def render_followers_items(actors: list, page: int,
followed_urls: set, actor: Any) -> str: followed_urls: set, actor: Any) -> str:
"""Pagination fragment: followers items.""" """Pagination fragment: followers items."""
return _actor_list_items_sexp(actors, page, "followers", followed_urls, actor) return _actor_list_items_sx(actors, page, "followers", followed_urls, actor)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -623,50 +623,50 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list,
actor_url = getattr(remote_actor, "actor_url", "") actor_url = getattr(remote_actor, "actor_url", "")
if icon_url: if icon_url:
avatar = sexp_call("federation-avatar-img", src=icon_url, cls="w-16 h-16 rounded-full") avatar = sx_call("federation-avatar-img", src=icon_url, cls="w-16 h-16 rounded-full")
else: else:
initial = display_name[0].upper() if display_name else "?" initial = display_name[0].upper() if display_name else "?"
avatar = sexp_call( avatar = sx_call(
"federation-avatar-placeholder", "federation-avatar-placeholder",
cls="w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl", cls="w-16 h-16 rounded-full bg-stone-300 flex items-center justify-center text-stone-600 font-bold text-xl",
initial=initial, initial=initial,
) )
summary_sexp = sexp_call("federation-profile-summary", summary=summary) if summary else "" summary_sx = sx_call("federation-profile-summary", summary=summary) if summary else ""
follow_sexp = "" follow_sx = ""
if actor: if actor:
if is_following: if is_following:
follow_sexp = sexp_call( follow_sx = sx_call(
"federation-follow-form", "federation-follow-form",
action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url, action=url_for("social.unfollow"), csrf=csrf, actor_url=actor_url,
label="Unfollow", label="Unfollow",
cls="border border-stone-300 rounded px-4 py-2 hover:bg-stone-100", cls="border border-stone-300 rounded px-4 py-2 hover:bg-stone-100",
) )
else: else:
follow_sexp = sexp_call( follow_sx = sx_call(
"federation-follow-form", "federation-follow-form",
action=url_for("social.follow"), csrf=csrf, actor_url=actor_url, action=url_for("social.follow"), csrf=csrf, actor_url=actor_url,
label="Follow", label="Follow",
cls="bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700", cls="bg-stone-800 text-white rounded px-4 py-2 hover:bg-stone-700",
) )
timeline_sexp = _timeline_items_sexp(items, "actor", actor, remote_actor.id) timeline_sx = _timeline_items_sx(items, "actor", actor, remote_actor.id)
header_sexp = sexp_call( header_sx = sx_call(
"federation-actor-profile-header", "federation-actor-profile-header",
avatar=SexpExpr(avatar), avatar=SxExpr(avatar),
display_name=str(escape(display_name)), display_name=str(escape(display_name)),
username=str(escape(remote_actor.preferred_username)), username=str(escape(remote_actor.preferred_username)),
domain=str(escape(remote_actor.domain)), domain=str(escape(remote_actor.domain)),
summary=SexpExpr(summary_sexp) if summary_sexp else None, summary=SxExpr(summary_sx) if summary_sx else None,
follow=SexpExpr(follow_sexp) if follow_sexp else None, follow=SxExpr(follow_sx) if follow_sx else None,
) )
content = sexp_call( content = sx_call(
"federation-actor-timeline-layout", "federation-actor-timeline-layout",
header=SexpExpr(header_sexp), header=SxExpr(header_sx),
timeline=SexpExpr(timeline_sexp) if timeline_sexp else None, timeline=SxExpr(timeline_sx) if timeline_sx else None,
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
@@ -676,7 +676,7 @@ async def render_actor_timeline_page(ctx: dict, remote_actor: Any, items: list,
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:
"""Pagination fragment: actor timeline items.""" """Pagination fragment: actor timeline items."""
return _timeline_items_sexp(items, "actor", actor, actor_id) return _timeline_items_sx(items, "actor", actor, actor_id)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -687,15 +687,15 @@ async def render_notifications_page(ctx: dict, notifications: list,
actor: Any) -> str: actor: Any) -> str:
"""Full page: notifications.""" """Full page: notifications."""
if not notifications: if not notifications:
notif_sexp = sexp_call("federation-notifications-empty") notif_sx = sx_call("federation-notifications-empty")
else: else:
items_sexp = "(<> " + " ".join(_notification_sexp(n) for n in notifications) + ")" items_sx = "(<> " + " ".join(_notification_sx(n) for n in notifications) + ")"
notif_sexp = sexp_call( notif_sx = sx_call(
"federation-notifications-list", "federation-notifications-list",
items=SexpExpr(items_sexp), items=SxExpr(items_sx),
) )
content = sexp_call("federation-notifications-page", notifs=SexpExpr(notif_sexp)) content = sx_call("federation-notifications-page", notifs=SxExpr(notif_sx))
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
title="Notifications \u2014 Rose Ash") title="Notifications \u2014 Rose Ash")
@@ -717,12 +717,12 @@ async def render_choose_username_page(ctx: dict) -> str:
check_url = url_for("identity.check_username") check_url = url_for("identity.check_username")
actor = ctx.get("actor") actor = ctx.get("actor")
error_sexp = sexp_call("federation-error-banner", error=error) if error else "" error_sx = sx_call("federation-error-banner", error=error) if error else ""
content = sexp_call( content = sx_call(
"federation-choose-username", "federation-choose-username",
domain=str(escape(ap_domain)), domain=str(escape(ap_domain)),
error=SexpExpr(error_sexp) if error_sexp else None, error=SxExpr(error_sx) if error_sx else None,
csrf=csrf, username=str(escape(username)), csrf=csrf, username=str(escape(username)),
check_url=check_url, check_url=check_url,
) )
@@ -742,36 +742,36 @@ async def render_profile_page(ctx: dict, actor: Any, activities: list,
ap_domain = config().get("ap_domain", "rose-ash.com") ap_domain = config().get("ap_domain", "rose-ash.com")
display_name = actor.display_name or actor.preferred_username display_name = actor.display_name or actor.preferred_username
summary_sexp = sexp_call( summary_sx = sx_call(
"federation-profile-summary-text", text=str(escape(actor.summary)), "federation-profile-summary-text", text=str(escape(actor.summary)),
) if actor.summary else "" ) if actor.summary else ""
activities_sexp = "" activities_sx = ""
if activities: if activities:
parts = [] parts = []
for a in activities: for a in activities:
published = a.published.strftime("%Y-%m-%d %H:%M") if a.published else "" published = a.published.strftime("%Y-%m-%d %H:%M") if a.published else ""
obj_type_sexp = sexp_call( obj_type_sx = sx_call(
"federation-activity-obj-type", obj_type=a.object_type, "federation-activity-obj-type", obj_type=a.object_type,
) if a.object_type else "" ) if a.object_type else ""
parts.append(sexp_call( parts.append(sx_call(
"federation-activity-card", "federation-activity-card",
activity_type=a.activity_type, published=published, activity_type=a.activity_type, published=published,
obj_type=SexpExpr(obj_type_sexp) if obj_type_sexp else None, obj_type=SxExpr(obj_type_sx) if obj_type_sx else None,
)) ))
items_sexp = "(<> " + " ".join(parts) + ")" items_sx = "(<> " + " ".join(parts) + ")"
activities_sexp = sexp_call("federation-activities-list", items=SexpExpr(items_sexp)) activities_sx = sx_call("federation-activities-list", items=SxExpr(items_sx))
else: else:
activities_sexp = sexp_call("federation-activities-empty") activities_sx = sx_call("federation-activities-empty")
content = sexp_call( content = sx_call(
"federation-profile-page", "federation-profile-page",
display_name=str(escape(display_name)), display_name=str(escape(display_name)),
username=str(escape(actor.preferred_username)), username=str(escape(actor.preferred_username)),
domain=str(escape(ap_domain)), domain=str(escape(ap_domain)),
summary=SexpExpr(summary_sexp) if summary_sexp else None, summary=SxExpr(summary_sx) if summary_sx else None,
activities_heading=f"Activities ({total})", activities_heading=f"Activities ({total})",
activities=SexpExpr(activities_sexp), activities=SxExpr(activities_sx),
) )
return _social_page(ctx, actor, content=content, return _social_page(ctx, actor, content=content,
@@ -796,10 +796,10 @@ def render_interaction_buttons(object_id: str, author_inbox: str,
liked_by_me=liked_by_me, liked_by_me=liked_by_me,
boosted_by_me=boosted_by_me, boosted_by_me=boosted_by_me,
) )
return _interaction_buttons_sexp(item, actor) return _interaction_buttons_sx(item, actor)
def render_actor_card(actor_dto: Any, actor: Any, followed_urls: set, def render_actor_card(actor_dto: Any, actor: Any, followed_urls: set,
*, list_type: str = "following") -> str: *, list_type: str = "following") -> str:
"""Render a single actor card fragment for HTMX POST response.""" """Render a single actor card fragment for HTMX POST response."""
return _actor_card_sexp(actor_dto, actor, followed_urls, list_type=list_type) return _actor_card_sx(actor_dto, actor, followed_urls, list_type=list_type)

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import path_setup # noqa: F401 # adds shared/ to sys.path import path_setup # noqa: F401 # adds shared/ to sys.path
import sexp.sexp_components as sexp_components # noqa: F401 # ensure Hypercorn --reload watches this file import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file
from pathlib import Path from pathlib import Path

View File

@@ -12,7 +12,7 @@ from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response from quart import Blueprint, g, request, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_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
@@ -56,13 +56,13 @@ def register() -> Blueprint:
page=page, page=page,
) )
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_all_markets_page, render_all_markets_oob from sx.sx_components import render_all_markets_page, render_all_markets_oob
tctx = await get_template_context() tctx = await get_template_context()
if is_htmx_request(): if is_htmx_request():
sexp_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page) sx_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page)
return sexp_response(sexp_src) return sx_response(sx_src)
else: else:
html = await render_all_markets_page(tctx, markets, has_more, page_info, page) html = await render_all_markets_page(tctx, markets, has_more, page_info, page)
return await make_response(html, 200) return await make_response(html, 200)
@@ -72,8 +72,8 @@ def register() -> Blueprint:
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)
from sexp.sexp_components import render_all_markets_cards from sx.sx_components import render_all_markets_cards
sexp_src = await render_all_markets_cards(markets, has_more, page_info, page) sx_src = await render_all_markets_cards(markets, has_more, page_info, page)
return sexp_response(sexp_src) return sx_response(sx_src)
return bp return bp

View File

@@ -23,7 +23,7 @@ from .services import (
from shared.browser.app.redis_cacher import cache_page from shared.browser.app.redis_cacher import cache_page
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
def register(): def register():
browse_bp = Blueprint("browse", __name__) browse_bp = Blueprint("browse", __name__)
@@ -43,8 +43,8 @@ def register():
p_data = getattr(g, "post_data", None) or {} p_data = getattr(g, "post_data", None) or {}
# Determine which template to use based on request type # Determine which template to use based on request type
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_market_home_page, render_market_home_oob from sx.sx_components import render_market_home_page, render_market_home_oob
ctx = await get_template_context() ctx = await get_template_context()
ctx.update(p_data) ctx.update(p_data)
@@ -52,8 +52,8 @@ def register():
html = await render_market_home_page(ctx) html = await render_market_home_page(ctx)
return await make_response(html) return await make_response(html)
else: else:
sexp_src = await render_market_home_oob(ctx) sx_src = await render_market_home_oob(ctx)
return sexp_response(sexp_src) return sx_response(sx_src)
@browse_bp.get("/all/") @browse_bp.get("/all/")
@cache_page(tag="browse") @cache_page(tag="browse")
@@ -74,8 +74,8 @@ def register():
product_info = await _productInfo() product_info = await _productInfo()
full_context = {**product_info, **ctx} full_context = {**product_info, **ctx}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(full_context) tctx.update(full_context)
@@ -84,11 +84,11 @@ def register():
resp = await make_response(html) resp = await make_response(html)
elif product_info["page"] > 1: elif product_info["page"] > 1:
tctx.update(product_info) tctx.update(product_info)
sexp_src = await render_browse_cards(tctx) sx_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src) resp = sx_response(sx_src)
else: else:
sexp_src = await render_browse_oob(tctx) sx_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src) 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)
@@ -115,8 +115,8 @@ def register():
product_info = await _productInfo(top_slug) product_info = await _productInfo(top_slug)
full_context = {**product_info, **ctx} full_context = {**product_info, **ctx}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(full_context) tctx.update(full_context)
@@ -125,11 +125,11 @@ def register():
resp = await make_response(html) resp = await make_response(html)
elif product_info["page"] > 1: elif product_info["page"] > 1:
tctx.update(product_info) tctx.update(product_info)
sexp_src = await render_browse_cards(tctx) sx_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src) resp = sx_response(sx_src)
else: else:
sexp_src = await render_browse_oob(tctx) sx_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src) 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)
@@ -156,8 +156,8 @@ def register():
product_info = await _productInfo(top_slug, sub_slug) product_info = await _productInfo(top_slug, sub_slug)
full_context = {**product_info, **ctx} full_context = {**product_info, **ctx}
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_browse_page, render_browse_oob, render_browse_cards from sx.sx_components import render_browse_page, render_browse_oob, render_browse_cards
tctx = await get_template_context() tctx = await get_template_context()
tctx.update(full_context) tctx.update(full_context)
@@ -166,11 +166,11 @@ def register():
resp = await make_response(html) resp = await make_response(html)
elif product_info["page"] > 1: elif product_info["page"] > 1:
tctx.update(product_info) tctx.update(product_info)
sexp_src = await render_browse_cards(tctx) sx_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src) resp = sx_response(sx_src)
else: else:
sexp_src = await render_browse_oob(tctx) sx_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src) 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)

View File

@@ -1,6 +1,6 @@
"""Market app fragment endpoints. """Market app fragment endpoints.
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption Exposes sx fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client. by other coop apps via the fragment client.
""" """
@@ -26,16 +26,16 @@ def register():
async def get_fragment(fragment_type: str): async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type) handler = _handlers.get(fragment_type)
if handler is None: if handler is None:
return Response("", status=200, content_type="text/sexp") return Response("", status=200, content_type="text/sx")
src = await handler() src = await handler()
return Response(src, status=200, content_type="text/sexp") return Response(src, status=200, content_type="text/sx")
# --- container-nav fragment: market links -------------------------------- # --- container-nav fragment: market links --------------------------------
async def _container_nav_handler(): async def _container_nav_handler():
from quart import current_app from quart import current_app
from shared.infrastructure.urls import market_url from shared.infrastructure.urls import market_url
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
container_type = request.args.get("container_type", "page") container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0)) container_id = int(request.args.get("container_id", 0))
@@ -51,7 +51,7 @@ def register():
parts = [] parts = []
for m in markets: for m in markets:
href = market_url(f"/{post_slug}/{m.slug}/") href = market_url(f"/{post_slug}/{m.slug}/")
parts.append(sexp_call("market-link-nav", parts.append(sx_call("market-link-nav",
href=href, name=m.name, nav_class=nav_class)) href=href, name=m.name, nav_class=nav_class))
return "(<> " + " ".join(parts) + ")" return "(<> " + " ".join(parts) + ")"
@@ -59,15 +59,15 @@ def register():
# --- link-card fragment: product preview card -------------------------------- # --- link-card fragment: product preview card --------------------------------
def _product_link_card_sexp(product, link: str) -> str: def _product_link_card_sx(product, link: str) -> str:
from shared.sexp.helpers import sexp_call from shared.sx.helpers import sx_call
subtitle = product.brand or "" subtitle = product.brand or ""
detail = "" detail = ""
if product.special_price: if product.special_price:
detail = f"{product.regular_price}{product.special_price}" detail = f"{product.regular_price}{product.special_price}"
elif product.regular_price: elif product.regular_price:
detail = str(product.regular_price) detail = str(product.regular_price)
return sexp_call("link-card", return sx_call("link-card",
title=product.title, image=product.image, title=product.title, image=product.image,
subtitle=subtitle, detail=detail, subtitle=subtitle, detail=detail,
link=link) link=link)
@@ -90,7 +90,7 @@ def register():
await g.s.execute(select(Product).where(Product.slug == s)) await g.s.execute(select(Product).where(Product.slug == s))
).scalar_one_or_none() ).scalar_one_or_none()
if product: if product:
parts.append(_product_link_card_sexp( parts.append(_product_link_card_sx(
product, market_url(f"/product/{product.slug}/"))) product, market_url(f"/product/{product.slug}/")))
return "\n".join(parts) return "\n".join(parts)
@@ -102,7 +102,7 @@ def register():
).scalar_one_or_none() ).scalar_one_or_none()
if not product: if not product:
return "" return ""
return _product_link_card_sexp(product, market_url(f"/product/{product.slug}/")) return _product_link_card_sx(product, market_url(f"/product/{product.slug}/"))
_handlers["link-card"] = _link_card_handler _handlers["link-card"] = _link_card_handler

View File

@@ -17,15 +17,15 @@ def register():
async def admin(): async def admin():
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_market_admin_page, render_market_admin_oob from sx.sx_components import render_market_admin_page, render_market_admin_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_market_admin_page(tctx) html = await render_market_admin_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
sexp_src = await render_market_admin_oob(tctx) sx_src = await render_market_admin_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
return bp return bp

View File

@@ -12,16 +12,16 @@ def register():
@bp.get("/") @bp.get("/")
@require_admin @require_admin
async def admin(**kwargs): async def admin(**kwargs):
from shared.sexp.page import get_template_context from shared.sx.page import get_template_context
from sexp.sexp_components import render_page_admin_page, render_page_admin_oob from sx.sx_components import render_page_admin_page, render_page_admin_oob
tctx = await get_template_context() tctx = await get_template_context()
if not is_htmx_request(): if not is_htmx_request():
html = await render_page_admin_page(tctx) html = await render_page_admin_page(tctx)
return await make_response(html) return await make_response(html)
else: else:
from shared.sexp.helpers import sexp_response from shared.sx.helpers import sx_response
sexp_src = await render_page_admin_oob(tctx) sx_src = await render_page_admin_oob(tctx)
return sexp_response(sexp_src) return sx_response(sx_src)
return bp return bp

Some files were not shown because too many files have changed in this diff Show More