Files
mono/relations/bp/fragments/routes.py
giles eda95ec58b Enable cross-subdomain htmx and purify layout to sexp
- Disable htmx selfRequestsOnly, add CORS headers for *.rose-ash.com
- Remove same-origin guards from ~menu-row and ~nav-link htmx attrs
- Convert ~app-layout from string-concatenated HTML to pure sexp tree
- Extract ~app-head component, replace ~app-shell with inline structure
- Convert hamburger SVG from Python HTML constant to ~hamburger sexp component
- Fix cross-domain fragment URLs (events_url, market_url)
- Fix starts-with? primitive to handle nil values
- Fix duplicate admin menu rows on OOB swaps
- Add calendar admin nav links (slots, description)
- Convert slots page from Jinja to sexp rendering
- Disable page caching in development mode
- Backfill migration to clean orphaned container_relations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 12:09:00 +00:00

106 lines
3.6 KiB
Python

"""Relations app fragment endpoints.
Generic container-nav fragment that renders navigation items for all
related entities, driven by the relation registry.
"""
from __future__ import annotations
from quart import Blueprint, Response, g, request
from shared.infrastructure.fragments import FRAGMENT_HEADER
def register():
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
_handlers: dict[str, object] = {}
@bp.before_request
async def _require_fragment_header():
if not request.headers.get(FRAGMENT_HEADER):
return Response("", status=403)
@bp.get("/<fragment_type>")
async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type)
if handler is None:
return Response("", status=200, content_type="text/html")
html = await handler()
return Response(html, status=200, content_type="text/html")
# --- generic container-nav fragment ----------------------------------------
async def _container_nav_handler():
"""Render nav items for all visible relations of a container entity.
Query params:
container_type: entity type (e.g. "page")
container_id: entity id
post_slug: used for URL construction
exclude: comma-separated relation types to skip
"""
from shared.sexp.jinja_bridge import sexp as render_sexp
from shared.sexp.relations import relations_from
from shared.services.relationships import get_children
from shared.infrastructure.urls import events_url, market_url
_SERVICE_URL = {
"calendar": events_url,
"market": market_url,
}
container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0))
post_slug = request.args.get("post_slug", "")
nav_class = request.args.get("nav_class", "")
exclude_raw = request.args.get("exclude", "")
exclude = set(exclude_raw.split(",")) if exclude_raw else set()
nav_defs = [
d for d in relations_from(container_type)
if d.nav != "hidden" and d.name not in exclude
]
if not nav_defs:
return ""
parts = []
for defn in nav_defs:
children = await get_children(
g.s,
parent_type=container_type,
parent_id=container_id,
child_type=defn.to_type,
relation_type=defn.name,
)
for child in children:
slug = (child.metadata_ or {}).get("slug", "")
if not slug:
continue
nav_label = defn.nav_label or ""
if post_slug and nav_label:
path = f"/{post_slug}/{nav_label}/{slug}/"
elif post_slug:
path = f"/{post_slug}/{slug}/"
else:
path = f"/{slug}/"
url_fn = _SERVICE_URL.get(defn.to_type)
href = url_fn(path) if url_fn else path
parts.append(render_sexp(
'(~relation-nav :href href :name name :icon icon :nav-class nav-class :relation-type relation-type)',
href=href,
name=child.label or "",
icon=defn.nav_icon or "",
**{
"nav-class": nav_class,
"relation-type": defn.name,
},
))
return "\n".join(parts)
_handlers["container-nav"] = _container_nav_handler
return bp