Files
provenance/backend/app/models/tree.py
T
justin 0262ed3d97 Account menu + Settings (change password); per-tree home person; full-width tree
- Sidebar bottom-left now shows the signed-in user; clicking opens a menu with
  Settings and Sign out. New /settings page: account info + change password
  (POST /auth/change-password, re-verifies current password). Export/restore/
  delete are stubbed there for the next pass.
- Per-tree default/home person: tree.home_person_id (migration) + TreeUpdate/
  Read; the tree and family views open focused on it; the person page gets a
  "Set as default" control and "Default person" badge. Cleared if that person
  is deleted. Complements the account-level "this is me" link.
- Tree visualization now fills the content area (AppShell drops the max-width
  column on the /tree route); other pages stay centered.
- Audit records are coerced JSON-safe (UUIDs/enums), so PATCHing UUID fields
  like home_person_id audits cleanly.

50 backend tests pass; migration up/down verified; frontend builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:05:04 -04:00

54 lines
1.9 KiB
Python

"""Tree — the top-level tenant boundary for genealogical data — and
TreeMembership, the basis for authorization (ARCHITECTURE §5).
"""
import uuid
from sqlalchemy import Enum as SAEnum
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.models.base import Base
from app.models.enums import MembershipRole, TreeVisibility
from app.models.mixins import SoftDelete, Timestamps, UUIDPrimaryKey
class Tree(Base, UUIDPrimaryKey, Timestamps, SoftDelete):
__tablename__ = "trees"
owner_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="RESTRICT"), index=True
)
name: Mapped[str] = mapped_column(String(255))
description: Mapped[str | None] = mapped_column(Text)
visibility: Mapped[TreeVisibility] = mapped_column(
SAEnum(TreeVisibility, name="tree_visibility"),
default=TreeVisibility.private,
server_default=TreeVisibility.private.value,
)
# The person a tree opens focused on (its "home"/root person). Cleared if
# that person is deleted. use_alter + name: trees<->persons form an FK cycle.
home_person_id: Mapped[uuid.UUID | None] = mapped_column(
ForeignKey(
"persons.id",
ondelete="SET NULL",
name="fk_trees_home_person_id",
use_alter=True,
)
)
class TreeMembership(Base, UUIDPrimaryKey, Timestamps):
__tablename__ = "tree_memberships"
__table_args__ = (
UniqueConstraint("tree_id", "user_id", name="uq_tree_memberships_tree_user"),
)
tree_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("trees.id", ondelete="CASCADE"), index=True
)
user_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("users.id", ondelete="CASCADE"), index=True
)
role: Mapped[MembershipRole] = mapped_column(SAEnum(MembershipRole, name="membership_role"))