Convert account, orders, and federation sexp_components.py to pure sexp() calls
Eliminates all f-string HTML from the remaining three services, completing the migration of all sexp_components.py files to the s-expression rendering system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,43 +69,59 @@ def _account_main_panel_html(ctx: dict) -> str:
|
||||
user = getattr(g, "user", None)
|
||||
error = ctx.get("error", "")
|
||||
|
||||
parts = ['<div class="w-full max-w-3xl mx-auto px-4 py-6">',
|
||||
'<div class="bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8">']
|
||||
error_html = sexp(
|
||||
'(div :class "rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm" (raw! e))',
|
||||
e=error,
|
||||
) if error else ""
|
||||
|
||||
if error:
|
||||
parts.append(
|
||||
f'<div class="rounded-lg border border-red-200 bg-red-50 text-red-800 px-4 py-3 text-sm">{error}</div>'
|
||||
user_email_html = ""
|
||||
user_name_html = ""
|
||||
if user:
|
||||
user_email_html = sexp(
|
||||
'(p :class "text-sm text-stone-500 mt-1" (raw! e))',
|
||||
e=user.email,
|
||||
)
|
||||
if user.name:
|
||||
user_name_html = sexp(
|
||||
'(p :class "text-sm text-stone-600" (raw! n))',
|
||||
n=user.name,
|
||||
)
|
||||
|
||||
logout_html = sexp(
|
||||
'(form :action "/auth/logout/" :method "post"'
|
||||
' (input :type "hidden" :name "csrf_token" :value csrf)'
|
||||
' (button :type "submit"'
|
||||
' :class "inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition"'
|
||||
' (i :class "fa-solid fa-right-from-bracket text-xs") " Sign out"))',
|
||||
csrf=generate_csrf_token(),
|
||||
)
|
||||
|
||||
labels_html = ""
|
||||
if user and hasattr(user, "labels") and user.labels:
|
||||
label_items = "".join(
|
||||
sexp(
|
||||
'(span :class "inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60" (raw! n))',
|
||||
n=label.name,
|
||||
)
|
||||
for label in user.labels
|
||||
)
|
||||
labels_html = sexp(
|
||||
'(div (h2 :class "text-base font-semibold tracking-tight mb-3" "Labels")'
|
||||
' (div :class "flex flex-wrap gap-2" (raw! items)))',
|
||||
items=label_items,
|
||||
)
|
||||
|
||||
# Account header with logout
|
||||
parts.append('<div class="flex items-center justify-between"><div>')
|
||||
parts.append('<h1 class="text-xl font-semibold tracking-tight">Account</h1>')
|
||||
if user:
|
||||
parts.append(f'<p class="text-sm text-stone-500 mt-1">{user.email}</p>')
|
||||
if user.name:
|
||||
parts.append(f'<p class="text-sm text-stone-600">{user.name}</p>')
|
||||
parts.append('</div>')
|
||||
parts.append(
|
||||
f'<form action="/auth/logout/" method="post">'
|
||||
f'<input type="hidden" name="csrf_token" value="{generate_csrf_token()}">'
|
||||
f'<button type="submit" class="inline-flex items-center gap-2 rounded-full border border-stone-300 px-4 py-2 text-sm font-medium text-stone-700 hover:bg-stone-50 transition">'
|
||||
f'<i class="fa-solid fa-right-from-bracket text-xs"></i> Sign out</button></form>'
|
||||
return sexp(
|
||||
'(div :class "w-full max-w-3xl mx-auto px-4 py-6"'
|
||||
' (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-8"'
|
||||
' (raw! err)'
|
||||
' (div :class "flex items-center justify-between"'
|
||||
' (div (h1 :class "text-xl font-semibold tracking-tight" "Account") (raw! email) (raw! name))'
|
||||
' (raw! logout))'
|
||||
' (raw! labels)))',
|
||||
err=error_html, email=user_email_html, name=user_name_html,
|
||||
logout=logout_html, labels=labels_html,
|
||||
)
|
||||
parts.append('</div>')
|
||||
|
||||
# Labels
|
||||
if user and hasattr(user, "labels") and user.labels:
|
||||
parts.append('<div><h2 class="text-base font-semibold tracking-tight mb-3">Labels</h2>')
|
||||
parts.append('<div class="flex flex-wrap gap-2">')
|
||||
for label in user.labels:
|
||||
parts.append(
|
||||
f'<span class="inline-flex items-center rounded-full border border-stone-200 px-3 py-1 text-xs font-medium bg-white/60">'
|
||||
f'{label.name}</span>'
|
||||
)
|
||||
parts.append('</div></div>')
|
||||
|
||||
parts.append('</div></div>')
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -124,16 +140,31 @@ def _newsletter_toggle_html(un: Any, account_url_fn: Any, csrf_token: str) -> st
|
||||
bg = "bg-stone-300"
|
||||
translate = "translate-x-1"
|
||||
checked = "false"
|
||||
return (
|
||||
f'<div id="nl-{nid}" class="flex items-center">'
|
||||
f'<button hx-post="{toggle_url}"'
|
||||
f' hx-headers=\'{{"X-CSRFToken": "{csrf_token}"}}\''
|
||||
f' hx-target="#nl-{nid}" hx-swap="outerHTML"'
|
||||
f' class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors'
|
||||
f' focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}"'
|
||||
f' role="switch" aria-checked="{checked}">'
|
||||
f'<span class="inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}"></span>'
|
||||
f'</button></div>'
|
||||
return sexp(
|
||||
'(div :id id :class "flex items-center"'
|
||||
' (button :hx-post url :hx-headers hdrs :hx-target tgt :hx-swap "outerHTML"'
|
||||
' :class cls :role "switch" :aria-checked checked'
|
||||
' (span :class knob)))',
|
||||
id=f"nl-{nid}", url=toggle_url,
|
||||
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
|
||||
tgt=f"#nl-{nid}",
|
||||
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
|
||||
checked=checked,
|
||||
knob=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}",
|
||||
)
|
||||
|
||||
|
||||
def _newsletter_toggle_off_html(nid: int, toggle_url: str, csrf_token: str) -> str:
|
||||
"""Render an unsubscribed newsletter toggle (no subscription record yet)."""
|
||||
return sexp(
|
||||
'(div :id id :class "flex items-center"'
|
||||
' (button :hx-post url :hx-headers hdrs :hx-target tgt :hx-swap "outerHTML"'
|
||||
' :class "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 bg-stone-300"'
|
||||
' :role "switch" :aria-checked "false"'
|
||||
' (span :class "inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform translate-x-1")))',
|
||||
id=f"nl-{nid}", url=toggle_url,
|
||||
hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}',
|
||||
tgt=f"#nl-{nid}",
|
||||
)
|
||||
|
||||
|
||||
@@ -144,44 +175,45 @@ def _newsletters_panel_html(ctx: dict, newsletter_list: list) -> str:
|
||||
account_url_fn = ctx.get("account_url") or (lambda p: p)
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
parts = ['<div class="w-full max-w-3xl mx-auto px-4 py-6">',
|
||||
'<div class="bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6">',
|
||||
'<h1 class="text-xl font-semibold tracking-tight">Newsletters</h1>']
|
||||
|
||||
if newsletter_list:
|
||||
parts.append('<div class="divide-y divide-stone-100">')
|
||||
items = []
|
||||
for item in newsletter_list:
|
||||
nl = item["newsletter"]
|
||||
un = item.get("un")
|
||||
parts.append('<div class="flex items-center justify-between py-4 first:pt-0 last:pb-0">')
|
||||
parts.append(f'<div class="min-w-0 flex-1"><p class="text-sm font-medium text-stone-800">{nl.name}</p>')
|
||||
if nl.description:
|
||||
parts.append(f'<p class="text-xs text-stone-500 mt-0.5 truncate">{nl.description}</p>')
|
||||
parts.append('</div><div class="ml-4 flex-shrink-0">')
|
||||
|
||||
desc_html = sexp(
|
||||
'(p :class "text-xs text-stone-500 mt-0.5 truncate" (raw! d))',
|
||||
d=nl.description,
|
||||
) if nl.description else ""
|
||||
|
||||
if un:
|
||||
parts.append(_newsletter_toggle_html(un, account_url_fn, csrf))
|
||||
toggle = _newsletter_toggle_html(un, account_url_fn, csrf)
|
||||
else:
|
||||
# No subscription yet — show off toggle
|
||||
toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/")
|
||||
parts.append(
|
||||
f'<div id="nl-{nl.id}" class="flex items-center">'
|
||||
f'<button hx-post="{toggle_url}"'
|
||||
f' hx-headers=\'{{"X-CSRFToken": "{csrf}"}}\''
|
||||
f' hx-target="#nl-{nl.id}" hx-swap="outerHTML"'
|
||||
f' class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors'
|
||||
f' focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 bg-stone-300"'
|
||||
f' role="switch" aria-checked="false">'
|
||||
f'<span class="inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform translate-x-1"></span>'
|
||||
f'</button></div>'
|
||||
)
|
||||
parts.append('</div></div>')
|
||||
parts.append('</div>')
|
||||
else:
|
||||
parts.append('<p class="text-sm text-stone-500">No newsletters available.</p>')
|
||||
toggle = _newsletter_toggle_off_html(nl.id, toggle_url, csrf)
|
||||
|
||||
parts.append('</div></div>')
|
||||
return "".join(parts)
|
||||
items.append(sexp(
|
||||
'(div :class "flex items-center justify-between py-4 first:pt-0 last:pb-0"'
|
||||
' (div :class "min-w-0 flex-1"'
|
||||
' (p :class "text-sm font-medium text-stone-800" (raw! name))'
|
||||
' (raw! desc))'
|
||||
' (div :class "ml-4 flex-shrink-0" (raw! toggle)))',
|
||||
name=nl.name, desc=desc_html, toggle=toggle,
|
||||
))
|
||||
list_html = sexp(
|
||||
'(div :class "divide-y divide-stone-100" (raw! items))',
|
||||
items="".join(items),
|
||||
)
|
||||
else:
|
||||
list_html = sexp('(p :class "text-sm text-stone-500" "No newsletters available.")')
|
||||
|
||||
return sexp(
|
||||
'(div :class "w-full max-w-3xl mx-auto px-4 py-6"'
|
||||
' (div :class "bg-white/70 backdrop-blur rounded-2xl shadow border border-stone-200 p-6 sm:p-8 space-y-6"'
|
||||
' (h1 :class "text-xl font-semibold tracking-tight" "Newsletters")'
|
||||
' (raw! list)))',
|
||||
list=list_html,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -195,25 +227,29 @@ def _login_page_content(ctx: dict) -> str:
|
||||
|
||||
error = ctx.get("error", "")
|
||||
email = ctx.get("email", "")
|
||||
|
||||
parts = ['<div class="py-8 max-w-md mx-auto">',
|
||||
'<h1 class="text-2xl font-bold mb-6">Sign in</h1>']
|
||||
if error:
|
||||
parts.append(
|
||||
f'<div class="bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4">{error}</div>'
|
||||
)
|
||||
action = url_for("auth.start_login")
|
||||
parts.append(
|
||||
f'<form method="post" action="{action}" class="space-y-4">'
|
||||
f'<input type="hidden" name="csrf_token" value="{generate_csrf_token()}">'
|
||||
f'<div><label for="email" class="block text-sm font-medium mb-1">Email address</label>'
|
||||
f'<input type="email" name="email" id="email" value="{email}" required autofocus'
|
||||
f' class="w-full border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"></div>'
|
||||
f'<button type="submit" class="w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition">'
|
||||
f'Send magic link</button></form>'
|
||||
|
||||
error_html = sexp(
|
||||
'(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (raw! e))',
|
||||
e=error,
|
||||
) if error else ""
|
||||
|
||||
return sexp(
|
||||
'(div :class "py-8 max-w-md mx-auto"'
|
||||
' (h1 :class "text-2xl font-bold mb-6" "Sign in")'
|
||||
' (raw! err)'
|
||||
' (form :method "post" :action action :class "space-y-4"'
|
||||
' (input :type "hidden" :name "csrf_token" :value csrf)'
|
||||
' (div'
|
||||
' (label :for "email" :class "block text-sm font-medium mb-1" "Email address")'
|
||||
' (input :type "email" :name "email" :id "email" :value email :required true :autofocus true'
|
||||
' :class "w-full border border-stone-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-stone-500"))'
|
||||
' (button :type "submit"'
|
||||
' :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"'
|
||||
' "Send magic link")))',
|
||||
err=error_html, action=action,
|
||||
csrf=generate_csrf_token(), email=email,
|
||||
)
|
||||
parts.append('</div>')
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _device_page_content(ctx: dict) -> str:
|
||||
@@ -223,36 +259,39 @@ def _device_page_content(ctx: dict) -> str:
|
||||
|
||||
error = ctx.get("error", "")
|
||||
code = ctx.get("code", "")
|
||||
|
||||
parts = ['<div class="py-8 max-w-md mx-auto">',
|
||||
'<h1 class="text-2xl font-bold mb-6">Authorize device</h1>',
|
||||
'<p class="text-stone-600 mb-4">Enter the code shown in your terminal to sign in.</p>']
|
||||
if error:
|
||||
parts.append(
|
||||
f'<div class="bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4">{error}</div>'
|
||||
)
|
||||
action = url_for("auth.device_submit")
|
||||
parts.append(
|
||||
f'<form method="post" action="{action}" class="space-y-4">'
|
||||
f'<input type="hidden" name="csrf_token" value="{generate_csrf_token()}">'
|
||||
f'<div><label for="code" class="block text-sm font-medium mb-1">Device code</label>'
|
||||
f'<input type="text" name="code" id="code" value="{code}" placeholder="XXXX-XXXX"'
|
||||
f' required autofocus maxlength="9" autocomplete="off" spellcheck="false"'
|
||||
f' class="w-full border border-stone-300 rounded px-3 py-3 text-center text-2xl tracking-widest font-mono uppercase focus:outline-none focus:ring-2 focus:ring-stone-500"></div>'
|
||||
f'<button type="submit" class="w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition">'
|
||||
f'Authorize</button></form>'
|
||||
|
||||
error_html = sexp(
|
||||
'(div :class "bg-red-50 border border-red-200 text-red-700 p-3 rounded mb-4" (raw! e))',
|
||||
e=error,
|
||||
) if error else ""
|
||||
|
||||
return sexp(
|
||||
'(div :class "py-8 max-w-md mx-auto"'
|
||||
' (h1 :class "text-2xl font-bold mb-6" "Authorize device")'
|
||||
' (p :class "text-stone-600 mb-4" "Enter the code shown in your terminal to sign in.")'
|
||||
' (raw! err)'
|
||||
' (form :method "post" :action action :class "space-y-4"'
|
||||
' (input :type "hidden" :name "csrf_token" :value csrf)'
|
||||
' (div'
|
||||
' (label :for "code" :class "block text-sm font-medium mb-1" "Device code")'
|
||||
' (input :type "text" :name "code" :id "code" :value code :placeholder "XXXX-XXXX"'
|
||||
' :required true :autofocus true :maxlength "9" :autocomplete "off" :spellcheck "false"'
|
||||
' :class "w-full border border-stone-300 rounded px-3 py-3 text-center text-2xl tracking-widest font-mono uppercase focus:outline-none focus:ring-2 focus:ring-stone-500"))'
|
||||
' (button :type "submit"'
|
||||
' :class "w-full bg-stone-800 text-white py-2 px-4 rounded hover:bg-stone-700 transition"'
|
||||
' "Authorize")))',
|
||||
err=error_html, action=action,
|
||||
csrf=generate_csrf_token(), code=code,
|
||||
)
|
||||
parts.append('</div>')
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def _device_approved_content() -> str:
|
||||
"""Device approved success content."""
|
||||
return (
|
||||
'<div class="py-8 max-w-md mx-auto text-center">'
|
||||
'<h1 class="text-2xl font-bold mb-4">Device authorized</h1>'
|
||||
'<p class="text-stone-600">You can close this window and return to your terminal.</p>'
|
||||
'</div>'
|
||||
return sexp(
|
||||
'(div :class "py-8 max-w-md mx-auto text-center"'
|
||||
' (h1 :class "text-2xl font-bold mb-4" "Device authorized")'
|
||||
' (p :class "text-stone-600" "You can close this window and return to your terminal."))',
|
||||
)
|
||||
|
||||
|
||||
@@ -387,18 +426,18 @@ def _check_email_content(email: str, email_error: str | None = None) -> str:
|
||||
"""Check email confirmation content."""
|
||||
from markupsafe import escape
|
||||
|
||||
error_html = ""
|
||||
if email_error:
|
||||
error_html = (
|
||||
f'<div class="bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4">'
|
||||
f'{escape(email_error)}</div>'
|
||||
)
|
||||
return (
|
||||
'<div class="py-8 max-w-md mx-auto text-center">'
|
||||
'<h1 class="text-2xl font-bold mb-4">Check your email</h1>'
|
||||
f'<p class="text-stone-600 mb-2">We sent a sign-in link to <strong>{escape(email)}</strong>.</p>'
|
||||
'<p class="text-stone-500 text-sm">Click the link in the email to sign in. The link expires in 15 minutes.</p>'
|
||||
f'{error_html}</div>'
|
||||
error_html = sexp(
|
||||
'(div :class "bg-yellow-50 border border-yellow-200 text-yellow-700 p-3 rounded mt-4" (raw! e))',
|
||||
e=str(escape(email_error)),
|
||||
) if email_error else ""
|
||||
|
||||
return sexp(
|
||||
'(div :class "py-8 max-w-md mx-auto text-center"'
|
||||
' (h1 :class "text-2xl font-bold mb-4" "Check your email")'
|
||||
' (p :class "text-stone-600 mb-2" "We sent a sign-in link to " (strong (raw! email)) ".")'
|
||||
' (p :class "text-stone-500 text-sm" "Click the link in the email to sign in. The link expires in 15 minutes.")'
|
||||
' (raw! err))',
|
||||
email=str(escape(email)), err=error_html,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user