Move blog composition from Python to .sx defcomps (Phase 7)
Convert all 8 blog page helpers from returning sx_call() strings to returning data dicts. Defpages now use :data + :content pattern: helpers load data, SX composes markup. Newsletter options and footer badges composed inline with map/fn in defpage expressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
; Blog app defpage declarations
|
||||
; Pages kept as Python: home, index, post-detail (cache_page / complex branching)
|
||||
; All helpers return data dicts — markup composition in SX.
|
||||
|
||||
; --- New post/page editors ---
|
||||
|
||||
@@ -7,13 +8,23 @@
|
||||
:path "/new/"
|
||||
:auth :admin
|
||||
:layout :blog
|
||||
:content (editor-content))
|
||||
:data (editor-data)
|
||||
:content (~blog-editor-content
|
||||
:csrf csrf :title-placeholder title-placeholder
|
||||
:create-label create-label :css-href css-href
|
||||
:js-src js-src :sx-editor-js-src sx-editor-js-src
|
||||
:init-js init-js))
|
||||
|
||||
(defpage new-page
|
||||
:path "/new-page/"
|
||||
:auth :admin
|
||||
:layout :blog
|
||||
:content (editor-page-content))
|
||||
:data (editor-page-data)
|
||||
:content (~blog-editor-content
|
||||
:csrf csrf :title-placeholder title-placeholder
|
||||
:create-label create-label :css-href css-href
|
||||
:js-src js-src :sx-editor-js-src sx-editor-js-src
|
||||
:init-js init-js))
|
||||
|
||||
; --- Post admin pages (absolute paths under /<slug>/admin/) ---
|
||||
|
||||
@@ -21,37 +32,71 @@
|
||||
:path "/<slug>/admin/"
|
||||
:auth :admin
|
||||
:layout (:post-admin :selected "admin")
|
||||
:content (post-admin-content slug))
|
||||
:data (post-admin-data slug)
|
||||
:content (~blog-admin-placeholder))
|
||||
|
||||
(defpage post-data
|
||||
:path "/<slug>/admin/data/"
|
||||
:auth :admin
|
||||
:layout (:post-admin :selected "data")
|
||||
:content (post-data-content slug))
|
||||
:data (post-data-data slug)
|
||||
:content (~blog-data-table-content :tablename tablename :model-data model-data))
|
||||
|
||||
(defpage post-preview
|
||||
:path "/<slug>/admin/preview/"
|
||||
:auth :admin
|
||||
:layout (:post-admin :selected "preview")
|
||||
:content (post-preview-content slug))
|
||||
:data (post-preview-data slug)
|
||||
:content (~blog-preview-content
|
||||
:sx-pretty sx-pretty :json-pretty json-pretty
|
||||
:sx-rendered sx-rendered :lex-rendered lex-rendered))
|
||||
|
||||
(defpage post-entries
|
||||
:path "/<slug>/admin/entries/"
|
||||
:auth :admin
|
||||
:layout (:post-admin :selected "entries")
|
||||
:content (post-entries-content slug))
|
||||
:data (post-entries-data slug)
|
||||
:content (~blog-entries-browser-content
|
||||
:entries-panel (~blog-associated-entries-from-data :entries entries :csrf csrf)
|
||||
:calendars calendars))
|
||||
|
||||
(defpage post-settings
|
||||
:path "/<slug>/admin/settings/"
|
||||
:auth :post_author
|
||||
:layout (:post-admin :selected "settings")
|
||||
:content (post-settings-content slug))
|
||||
:data (post-settings-data slug)
|
||||
:content (~blog-settings-form-content
|
||||
:csrf csrf :updated-at updated-at :is-page is-page
|
||||
:save-success save-success :slug settings-slug
|
||||
:published-at published-at :featured featured
|
||||
:visibility visibility :email-only email-only
|
||||
:tags tags :feature-image-alt feature-image-alt
|
||||
:meta-title meta-title :meta-description meta-description
|
||||
:canonical-url canonical-url :og-title og-title
|
||||
:og-description og-description :og-image og-image
|
||||
:twitter-title twitter-title :twitter-description twitter-description
|
||||
:twitter-image twitter-image :custom-template custom-template))
|
||||
|
||||
(defpage post-edit
|
||||
:path "/<slug>/admin/edit/"
|
||||
:auth :post_author
|
||||
:layout (:post-admin :selected "edit")
|
||||
:content (post-edit-content slug))
|
||||
:data (post-edit-data slug)
|
||||
:content (~blog-edit-content
|
||||
:csrf csrf :updated-at updated-at
|
||||
:title-val title-val :excerpt-val excerpt-val
|
||||
:feature-image feature-image :feature-image-caption feature-image-caption
|
||||
:sx-content-val sx-content-val :lexical-json lexical-json
|
||||
:has-sx has-sx :title-placeholder title-placeholder
|
||||
:status status :already-emailed already-emailed
|
||||
:newsletter-options (<>
|
||||
(option :value "" "Select newsletter\u2026")
|
||||
(map (fn (nl) (option :value (get nl "slug") (get nl "name"))) newsletters))
|
||||
:footer-extra (when badges
|
||||
(<> (map (fn (b) (span :class (get b "cls") (get b "text"))) badges)))
|
||||
:css-href css-href :js-src js-src
|
||||
:sx-editor-js-src sx-editor-js-src
|
||||
:init-js init-js :save-error save-error))
|
||||
|
||||
; --- Settings pages (absolute paths) ---
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""Blog page helpers — async functions available in .sx defpage expressions."""
|
||||
"""Blog page helpers — async functions available in .sx defpage expressions.
|
||||
|
||||
All helpers return data values (dicts, lists) — no sx_call().
|
||||
Markup composition lives entirely in .sx defpage and .sx defcomp files.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared hydration helpers
|
||||
# Shared hydration helpers (kept for auth/g._defpage_ctx side effects)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _add_to_defpage_ctx(**kwargs: Any) -> None:
|
||||
@@ -95,20 +99,20 @@ async def _inject_post_context(p_data: dict) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Page helpers (async functions available in .sx defpage expressions)
|
||||
# Registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _register_blog_helpers() -> None:
|
||||
from shared.sx.pages import register_page_helpers
|
||||
register_page_helpers("blog", {
|
||||
"editor-content": _h_editor_content,
|
||||
"editor-page-content": _h_editor_page_content,
|
||||
"post-admin-content": _h_post_admin_content,
|
||||
"post-data-content": _h_post_data_content,
|
||||
"post-preview-content": _h_post_preview_content,
|
||||
"post-entries-content": _h_post_entries_content,
|
||||
"post-settings-content": _h_post_settings_content,
|
||||
"post-edit-content": _h_post_edit_content,
|
||||
"editor-data": _h_editor_data,
|
||||
"editor-page-data": _h_editor_page_data,
|
||||
"post-admin-data": _h_post_admin_data,
|
||||
"post-data-data": _h_post_data_data,
|
||||
"post-preview-data": _h_post_preview_data,
|
||||
"post-entries-data": _h_post_entries_data,
|
||||
"post-settings-data": _h_post_settings_data,
|
||||
"post-edit-data": _h_post_edit_data,
|
||||
})
|
||||
|
||||
|
||||
@@ -264,52 +268,51 @@ def _editor_urls() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _h_editor_content(**kw):
|
||||
"""New post editor panel."""
|
||||
from shared.sx.helpers import sx_call
|
||||
def _h_editor_data(**kw) -> dict:
|
||||
"""New post editor — return data for ~blog-editor-content."""
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
urls = _editor_urls()
|
||||
csrf = generate_csrf_token()
|
||||
init_js = _editor_init_js(urls, form_id="post-new-form", has_initial_json=False)
|
||||
|
||||
return sx_call("blog-editor-content",
|
||||
csrf=csrf,
|
||||
title_placeholder="Post title...",
|
||||
create_label="Create Post",
|
||||
css_href=urls["css_href"],
|
||||
js_src=urls["js_src"],
|
||||
sx_editor_js_src=urls["sx_editor_js_src"],
|
||||
init_js=init_js)
|
||||
return {
|
||||
"csrf": csrf,
|
||||
"title-placeholder": "Post title...",
|
||||
"create-label": "Create Post",
|
||||
"css-href": urls["css_href"],
|
||||
"js-src": urls["js_src"],
|
||||
"sx-editor-js-src": urls["sx_editor_js_src"],
|
||||
"init-js": init_js,
|
||||
}
|
||||
|
||||
|
||||
def _h_editor_page_content(**kw):
|
||||
"""New page editor panel."""
|
||||
from shared.sx.helpers import sx_call
|
||||
def _h_editor_page_data(**kw) -> dict:
|
||||
"""New page editor — return data for ~blog-editor-content."""
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
|
||||
urls = _editor_urls()
|
||||
csrf = generate_csrf_token()
|
||||
init_js = _editor_init_js(urls, form_id="post-new-form", has_initial_json=False)
|
||||
|
||||
return sx_call("blog-editor-content",
|
||||
csrf=csrf,
|
||||
title_placeholder="Page title...",
|
||||
create_label="Create Page",
|
||||
css_href=urls["css_href"],
|
||||
js_src=urls["js_src"],
|
||||
sx_editor_js_src=urls["sx_editor_js_src"],
|
||||
init_js=init_js)
|
||||
return {
|
||||
"csrf": csrf,
|
||||
"title-placeholder": "Page title...",
|
||||
"create-label": "Create Page",
|
||||
"css-href": urls["css_href"],
|
||||
"js-src": urls["js_src"],
|
||||
"sx-editor-js-src": urls["sx_editor_js_src"],
|
||||
"init-js": init_js,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post admin helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_post_admin_content(slug=None, **kw):
|
||||
async def _h_post_admin_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from shared.sx.helpers import sx_call
|
||||
return sx_call("blog-admin-placeholder")
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -388,40 +391,38 @@ def _obj_summary(obj) -> str:
|
||||
return str(esc(" \u2022 ".join(ident_parts) if ident_parts else str(obj)))
|
||||
|
||||
|
||||
async def _h_post_data_content(slug=None, **kw):
|
||||
async def _h_post_data_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from quart import g
|
||||
from shared.sx.helpers import sx_call
|
||||
|
||||
original_post = getattr(g, "post_data", {}).get("original_post")
|
||||
if original_post is None:
|
||||
return sx_call("blog-data-table-content")
|
||||
return {"tablename": None, "model-data": None}
|
||||
|
||||
tablename = getattr(original_post, "__tablename__", "?")
|
||||
model_data = _extract_model_data(original_post, 0, 2)
|
||||
|
||||
return sx_call("blog-data-table-content",
|
||||
tablename=tablename, model_data=model_data)
|
||||
return {"tablename": tablename, "model-data": model_data}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Preview content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_post_preview_content(slug=None, **kw):
|
||||
async def _h_post_preview_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from quart import g
|
||||
from shared.services.registry import services
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
from shared.sx.helpers import SxExpr
|
||||
|
||||
preview = await services.blog_page.preview_data(g.s)
|
||||
|
||||
return sx_call("blog-preview-content",
|
||||
sx_pretty=SxExpr(preview["sx_pretty"]) if preview.get("sx_pretty") else None,
|
||||
json_pretty=SxExpr(preview["json_pretty"]) if preview.get("json_pretty") else None,
|
||||
sx_rendered=preview.get("sx_rendered") or None,
|
||||
lex_rendered=preview.get("lex_rendered") or None)
|
||||
return {
|
||||
"sx-pretty": SxExpr(preview["sx_pretty"]) if preview.get("sx_pretty") else None,
|
||||
"json-pretty": SxExpr(preview["json_pretty"]) if preview.get("json_pretty") else None,
|
||||
"sx-rendered": preview.get("sx_rendered") or None,
|
||||
"lex-rendered": preview.get("lex_rendered") or None,
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -493,13 +494,11 @@ def _extract_calendar_browser_data(all_calendars, post_slug: str) -> list:
|
||||
return calendars
|
||||
|
||||
|
||||
async def _h_post_entries_content(slug=None, **kw):
|
||||
async def _h_post_entries_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from quart import g
|
||||
from sqlalchemy import select
|
||||
from shared.models.calendars import Calendar
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from bp.post.services.entry_associations import get_post_entry_ids
|
||||
|
||||
@@ -516,30 +515,24 @@ async def _h_post_entries_content(slug=None, **kw):
|
||||
await g.s.refresh(calendar, ["entries", "post"])
|
||||
|
||||
csrf = generate_csrf_token()
|
||||
entry_data = _extract_associated_entries_data(
|
||||
entries = _extract_associated_entries_data(
|
||||
all_calendars, associated_entry_ids, post_slug)
|
||||
calendar_data = _extract_calendar_browser_data(all_calendars, post_slug)
|
||||
calendars = _extract_calendar_browser_data(all_calendars, post_slug)
|
||||
|
||||
entries_panel = sx_call("blog-associated-entries-from-data",
|
||||
entries=entry_data, csrf=csrf)
|
||||
|
||||
return sx_call("blog-entries-browser-content",
|
||||
entries_panel=SxExpr(entries_panel),
|
||||
calendars=calendar_data)
|
||||
return {"entries": entries, "calendars": calendars, "csrf": csrf}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings form
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def _h_post_settings_content(slug=None, **kw):
|
||||
async def _h_post_settings_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from quart import g, request
|
||||
from models.ghost_content import Post
|
||||
from sqlalchemy import select as sa_select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from shared.sx.helpers import sx_call
|
||||
from bp.post.admin.routes import _post_to_edit_dict
|
||||
|
||||
post_id = g.post_data["post"]["id"]
|
||||
@@ -570,28 +563,29 @@ async def _h_post_settings_content(slug=None, **kw):
|
||||
pub_at = gp.get("published_at") or ""
|
||||
pub_at_val = pub_at[:16] if pub_at else ""
|
||||
|
||||
return sx_call("blog-settings-form-content",
|
||||
csrf=csrf,
|
||||
updated_at=gp.get("updated_at") or "",
|
||||
is_page=is_page,
|
||||
save_success=save_success,
|
||||
slug=gp.get("slug") or "",
|
||||
published_at=pub_at_val,
|
||||
featured=bool(gp.get("featured")),
|
||||
visibility=gp.get("visibility") or "public",
|
||||
email_only=bool(gp.get("email_only")),
|
||||
tags=tag_names,
|
||||
feature_image_alt=gp.get("feature_image_alt") or "",
|
||||
meta_title=gp.get("meta_title") or "",
|
||||
meta_description=gp.get("meta_description") or "",
|
||||
canonical_url=gp.get("canonical_url") or "",
|
||||
og_title=gp.get("og_title") or "",
|
||||
og_description=gp.get("og_description") or "",
|
||||
og_image=gp.get("og_image") or "",
|
||||
twitter_title=gp.get("twitter_title") or "",
|
||||
twitter_description=gp.get("twitter_description") or "",
|
||||
twitter_image=gp.get("twitter_image") or "",
|
||||
custom_template=gp.get("custom_template") or "")
|
||||
return {
|
||||
"csrf": csrf,
|
||||
"updated-at": gp.get("updated_at") or "",
|
||||
"is-page": is_page,
|
||||
"save-success": save_success,
|
||||
"settings-slug": gp.get("slug") or "",
|
||||
"published-at": pub_at_val,
|
||||
"featured": bool(gp.get("featured")),
|
||||
"visibility": gp.get("visibility") or "public",
|
||||
"email-only": bool(gp.get("email_only")),
|
||||
"tags": tag_names,
|
||||
"feature-image-alt": gp.get("feature_image_alt") or "",
|
||||
"meta-title": gp.get("meta_title") or "",
|
||||
"meta-description": gp.get("meta_description") or "",
|
||||
"canonical-url": gp.get("canonical_url") or "",
|
||||
"og-title": gp.get("og_title") or "",
|
||||
"og-description": gp.get("og_description") or "",
|
||||
"og-image": gp.get("og_image") or "",
|
||||
"twitter-title": gp.get("twitter_title") or "",
|
||||
"twitter-description": gp.get("twitter_description") or "",
|
||||
"twitter-image": gp.get("twitter_image") or "",
|
||||
"custom-template": gp.get("custom_template") or "",
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -629,7 +623,7 @@ def _extract_footer_badges(ghost_post: dict, post: dict, save_success: bool,
|
||||
return badges
|
||||
|
||||
|
||||
async def _h_post_edit_content(slug=None, **kw):
|
||||
async def _h_post_edit_data(slug=None, **kw) -> dict:
|
||||
await _ensure_post_data(slug)
|
||||
from quart import g, request as qrequest
|
||||
from models.ghost_content import Post
|
||||
@@ -637,8 +631,6 @@ async def _h_post_edit_content(slug=None, **kw):
|
||||
from sqlalchemy.orm import selectinload
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.browser.app.csrf import generate_csrf_token
|
||||
from shared.sx.helpers import sx_call
|
||||
from shared.sx.parser import SxExpr, serialize as sx_serialize
|
||||
from bp.post.admin.routes import _post_to_edit_dict
|
||||
|
||||
post_id = g.post_data["post"]["id"]
|
||||
@@ -678,36 +670,34 @@ async def _h_post_edit_content(slug=None, **kw):
|
||||
|
||||
title_placeholder = "Page title..." if is_page else "Post title..."
|
||||
|
||||
# Newsletter options as SX fragment
|
||||
nl_parts = ['(option :value "" "Select newsletter\u2026")']
|
||||
for nl in newsletters:
|
||||
nl_slug = sx_serialize(getattr(nl, "slug", ""))
|
||||
nl_name = sx_serialize(getattr(nl, "name", ""))
|
||||
nl_parts.append(f"(option :value {nl_slug} {nl_name})")
|
||||
nl_opts_sx = SxExpr("(<> " + " ".join(nl_parts) + ")")
|
||||
# Return newsletter data as list of dicts (composed in SX)
|
||||
nl_options = _extract_newsletter_options(newsletters)
|
||||
|
||||
# Footer extra badges as SX fragment
|
||||
# Return footer badge data as list of dicts (composed in SX)
|
||||
publish_requested = bool(qrequest.args.get("publish_requested")) if hasattr(qrequest, 'args') else False
|
||||
badges = _extract_footer_badges(ghost_post, post, save_success,
|
||||
publish_requested, already_emailed)
|
||||
if badges:
|
||||
badge_parts = [f'(span :class "{b["cls"]}" {sx_serialize(b["text"])})'
|
||||
for b in badges]
|
||||
footer_extra_sx = SxExpr("(<> " + " ".join(badge_parts) + ")")
|
||||
else:
|
||||
footer_extra_sx = None
|
||||
|
||||
init_js = _editor_init_js(urls, form_id="post-edit-form", has_initial_json=True)
|
||||
|
||||
return sx_call("blog-edit-content",
|
||||
csrf=csrf, updated_at=str(updated_at),
|
||||
title_val=title_val, excerpt_val=excerpt_val,
|
||||
feature_image=feature_image,
|
||||
feature_image_caption=feature_image_caption,
|
||||
sx_content_val=sx_content, lexical_json=lexical_json,
|
||||
has_sx=has_sx, title_placeholder=title_placeholder,
|
||||
status=status, already_emailed=already_emailed,
|
||||
newsletter_options=nl_opts_sx, footer_extra=footer_extra_sx,
|
||||
css_href=urls["css_href"], js_src=urls["js_src"],
|
||||
sx_editor_js_src=urls["sx_editor_js_src"],
|
||||
init_js=init_js, save_error=save_error or None)
|
||||
return {
|
||||
"csrf": csrf,
|
||||
"updated-at": str(updated_at),
|
||||
"title-val": title_val,
|
||||
"excerpt-val": excerpt_val,
|
||||
"feature-image": feature_image,
|
||||
"feature-image-caption": feature_image_caption,
|
||||
"sx-content-val": sx_content,
|
||||
"lexical-json": lexical_json,
|
||||
"has-sx": has_sx,
|
||||
"title-placeholder": title_placeholder,
|
||||
"status": status,
|
||||
"already-emailed": already_emailed,
|
||||
"newsletters": nl_options,
|
||||
"badges": badges,
|
||||
"css-href": urls["css_href"],
|
||||
"js-src": urls["js_src"],
|
||||
"sx-editor-js-src": urls["sx_editor_js_src"],
|
||||
"init-js": init_js,
|
||||
"save-error": save_error or None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user