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:
parent
00dbb86c93
commit
affed64839
24 changed files with 1812 additions and 151 deletions
|
|
@ -1,11 +1,13 @@
|
|||
"""Space queries scoped by tenant membership."""
|
||||
"""Space queries and PageOps-backed mutations (ADR-0003 / ADR-0004)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
from coulomb_social.apps.members.models import Member
|
||||
|
||||
from . import pageops
|
||||
from .models import Space, SpaceMembership
|
||||
|
||||
|
||||
|
|
@ -37,3 +39,159 @@ def get_space_for_member(member: Member | None, slug: str) -> Space | None:
|
|||
if member is None:
|
||||
return None
|
||||
return spaces_for_member(member).filter(slug=slug).first()
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_space(
|
||||
member: Member,
|
||||
*,
|
||||
title: str,
|
||||
abstractor: str = "",
|
||||
slug: str | None = None,
|
||||
visual: str = "",
|
||||
) -> Space:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
raise pageops.PageOpsError("Title is required")
|
||||
space_slug = pageops.normalize_slug(slug or Space.suggest_slug(title))
|
||||
if Space.objects.filter(tenant_id=member.tenant_id, slug=space_slug).exists():
|
||||
raise pageops.PageOpsError("A space with this slug already exists")
|
||||
|
||||
space = Space.objects.create(
|
||||
tenant_id=member.tenant_id,
|
||||
slug=space_slug,
|
||||
title=title,
|
||||
description=abstractor or "",
|
||||
visual=visual or "",
|
||||
created_by=member,
|
||||
content_root="pages",
|
||||
default_branch="main",
|
||||
)
|
||||
SpaceMembership.objects.get_or_create(
|
||||
space=space,
|
||||
member=member,
|
||||
defaults={"role": SpaceMembership.Role.OWNER},
|
||||
)
|
||||
pageops.init_space_content(
|
||||
space_slug,
|
||||
title=title,
|
||||
abstractor=abstractor or "",
|
||||
visual=visual or "",
|
||||
)
|
||||
return space
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def update_space(
|
||||
member: Member,
|
||||
space: Space,
|
||||
*,
|
||||
title: str | None = None,
|
||||
abstractor: str | None = None,
|
||||
visual: str | None = None,
|
||||
) -> Space:
|
||||
if get_space_for_member(member, space.slug) is None:
|
||||
raise pageops.PageOpsError("Space not found")
|
||||
if title is not None:
|
||||
space.title = title.strip() or space.title
|
||||
if abstractor is not None:
|
||||
space.description = abstractor
|
||||
if visual is not None:
|
||||
space.visual = visual
|
||||
space.save()
|
||||
# Keep index.md in sync
|
||||
index = pageops.read_page(space.slug, "index")
|
||||
body = index.body if index.exists else f"# {space.title}\n"
|
||||
pageops.write_page(
|
||||
space.slug,
|
||||
"index",
|
||||
title=space.title,
|
||||
abstractor=space.description,
|
||||
visual=space.visual,
|
||||
body=body,
|
||||
extra_meta={"bubble_type": "space"},
|
||||
)
|
||||
return space
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def archive_space(member: Member, space: Space) -> Space:
|
||||
if get_space_for_member(member, space.slug) is None:
|
||||
raise pageops.PageOpsError("Space not found")
|
||||
space.is_active = False
|
||||
space.save(update_fields=["is_active", "updated_at"])
|
||||
try:
|
||||
pageops.archive_space_tree(space.slug)
|
||||
except Exception:
|
||||
# Tree archive is best-effort
|
||||
pass
|
||||
return space
|
||||
|
||||
|
||||
def list_space_pages(space: Space) -> list[pageops.PageDoc]:
|
||||
return pageops.list_pages(space.slug)
|
||||
|
||||
|
||||
def put_space_page(
|
||||
member: Member,
|
||||
space: Space,
|
||||
page_slug: str,
|
||||
*,
|
||||
title: str,
|
||||
abstractor: str = "",
|
||||
visual: str = "",
|
||||
body: str = "",
|
||||
) -> pageops.PageDoc:
|
||||
if get_space_for_member(member, space.slug) is None:
|
||||
raise pageops.PageOpsError("Space not found")
|
||||
return pageops.write_page(
|
||||
space.slug,
|
||||
page_slug,
|
||||
title=title,
|
||||
abstractor=abstractor,
|
||||
visual=visual,
|
||||
body=body,
|
||||
)
|
||||
|
||||
|
||||
def copy_space_page(
|
||||
member: Member,
|
||||
space: Space,
|
||||
page_slug: str,
|
||||
*,
|
||||
dest_slug: str,
|
||||
dest_space: Space | None = None,
|
||||
title: str | None = None,
|
||||
) -> pageops.PageDoc:
|
||||
if get_space_for_member(member, space.slug) is None:
|
||||
raise pageops.PageOpsError("Source space not found")
|
||||
target = dest_space or space
|
||||
if get_space_for_member(member, target.slug) is None:
|
||||
raise pageops.PageOpsError("Destination space not found")
|
||||
return pageops.copy_page(
|
||||
space.slug,
|
||||
page_slug,
|
||||
target.slug,
|
||||
dest_slug,
|
||||
title=title,
|
||||
)
|
||||
|
||||
|
||||
def transfer_space_page(
|
||||
member: Member,
|
||||
space: Space,
|
||||
page_slug: str,
|
||||
dest_space: Space,
|
||||
*,
|
||||
dest_slug: str | None = None,
|
||||
) -> pageops.PageDoc:
|
||||
if get_space_for_member(member, space.slug) is None:
|
||||
raise pageops.PageOpsError("Source space not found")
|
||||
if get_space_for_member(member, dest_space.slug) is None:
|
||||
raise pageops.PageOpsError("Destination space not found")
|
||||
return pageops.transfer_page(
|
||||
space.slug,
|
||||
page_slug,
|
||||
dest_space.slug,
|
||||
dest_slug,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue