""" Account service s-expression page components. Renders account dashboard, newsletters, fragment pages, login, and device auth pages. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations import os from typing import Any from shared.sexp.jinja_bridge import load_service_components from shared.sexp.helpers import ( call_url, sexp_call, SexpExpr, root_header_sexp, full_page_sexp, header_child_sexp, oob_page_sexp, ) # Load account-specific .sexpr components at import time load_service_components(os.path.dirname(os.path.dirname(__file__))) # --------------------------------------------------------------------------- # Header helpers # --------------------------------------------------------------------------- def _auth_nav_sexp(ctx: dict) -> str: """Auth section desktop nav items.""" parts = [ sexp_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), ) ] account_nav = ctx.get("account_nav") if account_nav: parts.append(account_nav) return "(<> " + " ".join(parts) + ")" def _auth_header_sexp(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row.""" return sexp_call( "menu-row-sx", id="auth-row", level=1, colour="sky", link_href=call_url(ctx, "account_url", "/"), link_label="account", icon="fa-solid fa-user", nav=SexpExpr(_auth_nav_sexp(ctx)), child_id="auth-header-child", oob=oob, ) def _auth_nav_mobile_sexp(ctx: dict) -> str: """Mobile nav menu for auth section.""" parts = [ sexp_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), ) ] account_nav = ctx.get("account_nav") if account_nav: parts.append(account_nav) return "(<> " + " ".join(parts) + ")" # --------------------------------------------------------------------------- # Account dashboard (GET /) # --------------------------------------------------------------------------- def _account_main_panel_sexp(ctx: dict) -> str: """Account info panel with user details and logout.""" from quart import g from shared.browser.app.csrf import generate_csrf_token user = getattr(g, "user", None) error = ctx.get("error", "") error_sexp = sexp_call("account-error-banner", error=error) if error else "" user_email_sexp = "" user_name_sexp = "" if user: user_email_sexp = sexp_call("account-user-email", email=user.email) if user.name: user_name_sexp = sexp_call("account-user-name", name=user.name) logout_sexp = sexp_call("account-logout-form", csrf_token=generate_csrf_token()) labels_sexp = "" if user and hasattr(user, "labels") and user.labels: label_items = " ".join( sexp_call("account-label-item", name=label.name) for label in user.labels ) labels_sexp = sexp_call("account-labels-section", items=SexpExpr("(<> " + label_items + ")")) return sexp_call( "account-main-panel", error=SexpExpr(error_sexp) if error_sexp else None, email=SexpExpr(user_email_sexp) if user_email_sexp else None, name=SexpExpr(user_name_sexp) if user_name_sexp else None, logout=SexpExpr(logout_sexp), labels=SexpExpr(labels_sexp) if labels_sexp else None, ) # --------------------------------------------------------------------------- # Newsletters (GET /newsletters/) # --------------------------------------------------------------------------- def _newsletter_toggle_sexp(un: Any, account_url_fn: Any, csrf_token: str) -> str: """Render a single newsletter toggle switch.""" nid = un.newsletter_id toggle_url = account_url_fn(f"/newsletter/{nid}/toggle/") if un.subscribed: bg = "bg-emerald-500" translate = "translate-x-6" checked = "true" else: bg = "bg-stone-300" translate = "translate-x-1" checked = "false" return sexp_call( "account-newsletter-toggle", id=f"nl-{nid}", url=toggle_url, hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', target=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_cls=f"inline-block h-4 w-4 rounded-full bg-white shadow transform transition-transform {translate}", ) def _newsletter_toggle_off_sexp(nid: int, toggle_url: str, csrf_token: str) -> str: """Render an unsubscribed newsletter toggle (no subscription record yet).""" return sexp_call( "account-newsletter-toggle-off", id=f"nl-{nid}", url=toggle_url, hdrs=f'{{"X-CSRFToken": "{csrf_token}"}}', target=f"#nl-{nid}", ) def _newsletters_panel_sexp(ctx: dict, newsletter_list: list) -> str: """Newsletters management panel.""" from shared.browser.app.csrf import generate_csrf_token account_url_fn = ctx.get("account_url") or (lambda p: p) csrf = generate_csrf_token() if newsletter_list: items = [] for item in newsletter_list: nl = item["newsletter"] un = item.get("un") desc_sexp = sexp_call( "account-newsletter-desc", description=nl.description ) if nl.description else "" if un: toggle = _newsletter_toggle_sexp(un, account_url_fn, csrf) else: toggle_url = account_url_fn(f"/newsletter/{nl.id}/toggle/") toggle = _newsletter_toggle_off_sexp(nl.id, toggle_url, csrf) items.append(sexp_call( "account-newsletter-item", name=nl.name, desc=SexpExpr(desc_sexp) if desc_sexp else None, toggle=SexpExpr(toggle), )) list_sexp = sexp_call( "account-newsletter-list", items=SexpExpr("(<> " + " ".join(items) + ")"), ) else: list_sexp = sexp_call("account-newsletter-empty") return sexp_call("account-newsletters-panel", list=SexpExpr(list_sexp)) # --------------------------------------------------------------------------- # Auth pages (login, device, check_email) # --------------------------------------------------------------------------- def _login_page_content(ctx: dict) -> str: """Login form content.""" from shared.browser.app.csrf import generate_csrf_token from quart import url_for error = ctx.get("error", "") email = ctx.get("email", "") action = url_for("auth.start_login") error_sexp = sexp_call("account-login-error", error=error) if error else "" return sexp_call( "account-login-form", error=SexpExpr(error_sexp) if error_sexp else None, action=action, csrf_token=generate_csrf_token(), email=email, ) def _device_page_content(ctx: dict) -> str: """Device authorization form content.""" from shared.browser.app.csrf import generate_csrf_token from quart import url_for error = ctx.get("error", "") code = ctx.get("code", "") action = url_for("auth.device_submit") error_sexp = sexp_call("account-device-error", error=error) if error else "" return sexp_call( "account-device-form", error=SexpExpr(error_sexp) if error_sexp else None, action=action, csrf_token=generate_csrf_token(), code=code, ) def _device_approved_content() -> str: """Device approved success content.""" return sexp_call("account-device-approved") # --------------------------------------------------------------------------- # Public API: Account dashboard # --------------------------------------------------------------------------- async def render_account_page(ctx: dict) -> str: """Full page: account dashboard.""" main = _account_main_panel_sexp(ctx) hdr = root_header_sexp(ctx) hdr_child = header_child_sexp(_auth_header_sexp(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" return full_page_sexp(ctx, header_rows=header_rows, content=main, menu=_auth_nav_mobile_sexp(ctx)) async def render_account_oob(ctx: dict) -> str: """OOB response for account dashboard.""" main = _account_main_panel_sexp(ctx) oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" return oob_page_sexp(oobs=oobs, content=main, menu=_auth_nav_mobile_sexp(ctx)) # --------------------------------------------------------------------------- # Public API: Newsletters # --------------------------------------------------------------------------- async def render_newsletters_page(ctx: dict, newsletter_list: list) -> str: """Full page: newsletters.""" main = _newsletters_panel_sexp(ctx, newsletter_list) hdr = root_header_sexp(ctx) hdr_child = header_child_sexp(_auth_header_sexp(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" return full_page_sexp(ctx, header_rows=header_rows, content=main, menu=_auth_nav_mobile_sexp(ctx)) async def render_newsletters_oob(ctx: dict, newsletter_list: list) -> str: """OOB response for newsletters.""" main = _newsletters_panel_sexp(ctx, newsletter_list) oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" return oob_page_sexp(oobs=oobs, content=main, menu=_auth_nav_mobile_sexp(ctx)) # --------------------------------------------------------------------------- # Public API: Fragment pages # --------------------------------------------------------------------------- async def render_fragment_page(ctx: dict, page_fragment_html: str) -> str: """Full page: fragment-provided content.""" hdr = root_header_sexp(ctx) hdr_child = header_child_sexp(_auth_header_sexp(ctx)) header_rows = "(<> " + hdr + " " + hdr_child + ")" return full_page_sexp(ctx, header_rows=header_rows, content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', menu=_auth_nav_mobile_sexp(ctx)) async def render_fragment_oob(ctx: dict, page_fragment_html: str) -> str: """OOB response for fragment pages.""" oobs = "(<> " + _auth_header_sexp(ctx, oob=True) + " " + root_header_sexp(ctx, oob=True) + ")" return oob_page_sexp(oobs=oobs, content=f'(~rich-text :html "{_sexp_escape(page_fragment_html)}")', menu=_auth_nav_mobile_sexp(ctx)) # --------------------------------------------------------------------------- # Public API: Auth pages (login, device) # --------------------------------------------------------------------------- async def render_login_page(ctx: dict) -> str: """Full page: login form.""" hdr = root_header_sexp(ctx) return full_page_sexp(ctx, header_rows=hdr, content=_login_page_content(ctx), meta_html='