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

@ -1,4 +1,4 @@
"""Space metadata — content bodies live in Forgejo (markdown), not here."""
"""Space metadata index — page bodies live on the content plane (markdown)."""
from __future__ import annotations
@ -23,15 +23,24 @@ def validate_space_slug(value: str) -> None:
class Space(models.Model):
"""Tenant-scoped co-creation space.
Postgres holds metadata and Forgejo binding pointers only. Long-form pages
are markdown in the bound repository (ADR-0002).
Postgres holds metadata and optional Forgejo binding pointers. Long-form
pages are markdown under CONTENT_ROOT (ADR-0003 / ADR-0004); Forgejo is an
optional read/edit fallback (ADR-0002).
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant_id = models.CharField(max_length=64, db_index=True)
slug = models.SlugField(max_length=80, validators=[validate_space_slug])
title = models.CharField(max_length=255)
description = models.TextField(blank=True)
description = models.TextField(
blank=True,
help_text="Short description (product label: Abstractor).",
)
visual = models.CharField(
max_length=512,
blank=True,
help_text="Cover/visual path relative to space tree (e.g. assets/cover.jpg).",
)
created_by = models.ForeignKey(
"members.Member",
on_delete=models.PROTECT,
@ -80,6 +89,15 @@ class Space(models.Model):
def has_content_binding(self) -> bool:
return bool(self.forgejo_owner and self.forgejo_repo)
@property
def abstractor(self) -> str:
"""Product name for the short description field."""
return self.description
@abstractor.setter
def abstractor(self, value: str) -> None:
self.description = value or ""
@classmethod
def suggest_slug(cls, title: str) -> str:
return slugify(title)[:80] or "space"