Implement flexible entity relation system (Phases A–E)

Declarative relation registry via defrelation s-expressions with
cardinality enforcement (one-to-one, one-to-many, many-to-many),
registry-aware relate/unrelate/can-relate API endpoints, generic
container-nav fragment, and relation-driven UI components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 08:35:17 +00:00
parent 6f1d5bac3c
commit a0a0f5ebc2
17 changed files with 928 additions and 28 deletions

View File

@@ -264,6 +264,80 @@ class TestErrorPage:
assert "<img" not in html
# ---------------------------------------------------------------------------
# ~relation-nav
# ---------------------------------------------------------------------------
class TestRelationNav:
def test_renders_link(self):
html = sexp(
'(~relation-nav :href "/market/farm/" :name "Farm Shop" :icon "fa fa-shopping-bag")',
)
assert 'href="/market/farm/"' in html
assert "Farm Shop" in html
assert "fa fa-shopping-bag" in html
def test_no_icon(self):
html = sexp(
'(~relation-nav :href "/cal/" :name "Events")',
)
assert 'href="/cal/"' in html
assert "Events" in html
assert "fa " not in html
def test_custom_nav_class(self):
html = sexp(
'(~relation-nav :href "/" :name "X" :nav-class "custom-class")',
**{"nav-class": "custom-class"},
)
assert 'class="custom-class"' in html
# ---------------------------------------------------------------------------
# ~relation-attach
# ---------------------------------------------------------------------------
class TestRelationAttach:
def test_renders_button(self):
html = sexp(
'(~relation-attach :create-url "/market/create/" :label "Add Market" :icon "fa fa-plus")',
**{"create-url": "/market/create/"},
)
assert 'href="/market/create/"' in html
assert 'hx-get="/market/create/"' in html
assert "Add Market" in html
assert "fa fa-plus" in html
def test_default_label(self):
html = sexp(
'(~relation-attach :create-url "/create/")',
**{"create-url": "/create/"},
)
assert "Add" in html
# ---------------------------------------------------------------------------
# ~relation-detach
# ---------------------------------------------------------------------------
class TestRelationDetach:
def test_renders_button(self):
html = sexp(
'(~relation-detach :detach-url "/api/unrelate" :name "Farm Shop")',
**{"detach-url": "/api/unrelate"},
)
assert 'hx-delete="/api/unrelate"' in html
assert 'hx-confirm="Remove Farm Shop?"' in html
assert "fa fa-times" in html
def test_default_name(self):
html = sexp(
'(~relation-detach :detach-url "/api/unrelate")',
**{"detach-url": "/api/unrelate"},
)
assert "this item" in html
# ---------------------------------------------------------------------------
# render_page() helper
# ---------------------------------------------------------------------------