Land CSOC-WP-0006 PageOps: member space/page CRUD, copy, transfer

Thin CONTENT_ROOT content plane (ADR-0003/0004) with space visual field,
product UI for Title/Abstract/Visual, page list sidebar, and tests. Update
capability model and smoke checklist; mark WP-0006 finished.
This commit is contained in:
tegwick 2026-08-13 12:46:39 +02:00
parent 00dbb86c93
commit affed64839
24 changed files with 1812 additions and 151 deletions

View file

@ -106,7 +106,7 @@ def _title_from_markdown(text: str, fallback: str) -> str:
def load_space_page(space: Space, page: str | None = None) -> RenderedPage:
"""Fetch and render a page for a space. Fail closed with error field set."""
"""Fetch and render a page for a space. Prefer CONTENT_ROOT (ADR-0004)."""
try:
page_slug = normalize_page_slug(page)
path = page_path_for(space, page_slug)
@ -120,24 +120,21 @@ def load_space_page(space: Space, page: str | None = None) -> RenderedPage:
error=exc.message,
)
if not space.has_content_binding:
return RenderedPage(
slug=page_slug,
path=path,
title=space.title,
html="",
source="",
error="This space is not bound to a Forgejo repository yet.",
)
try:
fetched = _fetch(space, path)
html = render_markdown(fetched.text)
title = _title_from_markdown(fetched.text, space.title)
# Prefer frontmatter title when present
from .pageops import parse_frontmatter
meta, body = parse_frontmatter(fetched.text)
display_title = (
str(meta.get("title") or "").strip()
or _title_from_markdown(body or fetched.text, space.title)
)
html = render_markdown(body if meta else fetched.text)
return RenderedPage(
slug=page_slug,
path=path,
title=title,
title=display_title,
html=html,
source=fetched.source,
error=None,
@ -154,6 +151,17 @@ def load_space_page(space: Space, page: str | None = None) -> RenderedPage:
def _fetch(space: Space, path: str) -> FetchedFile:
# ADR-0004: CONTENT_ROOT / spaces/<slug>/pages/…
from . import pageops
try:
local_doc = pageops.read_page(space.slug, Path(path).stem)
if local_doc.exists and local_doc.path:
text = Path(local_doc.path).read_text(encoding="utf-8")
return FetchedFile(path=path, text=text, source="content-plane")
except pageops.PageOpsError:
pass
# Optional local fixture root for offline tests / air-gapped demos
fixture_root = (getattr(settings, "SPACE_CONTENT_FIXTURE_ROOT", None) or "").strip()
if fixture_root:
@ -161,6 +169,11 @@ def _fetch(space: Space, path: str) -> FetchedFile:
if local.is_file():
return FetchedFile(path=path, text=local.read_text(encoding="utf-8"), source="fixture")
if not space.has_content_binding:
raise ContentFetchError(
"No local page on the content plane and no Forgejo binding."
)
cache_key = (
space.forgejo_owner,
space.forgejo_repo,