04ccdbf96a
Names (the genealogy standard: maiden name primary, married/alias as typed
alternates):
- Name model already supported multiple typed names; expose full CRUD —
NameCreate/Read/Update schemas, name_service (one-primary invariant,
promote-on-delete), nested /persons/{id}/names routes.
- Person page gains a Names card: add/edit/delete + "make primary", with a
curated name_type dropdown (birth/maiden, married, alias, nickname, …).
Self-person ("who am I"):
- users.self_person_id FK (use_alter for the users<->persons<->trees cycle)
+ migration; PATCH /users/me/self-person; "This is me" / "This is you"
on the person page. Soft-deleting the linked person clears it.
Deletion integrity (fixes the broken tree view):
- delete_person now soft-deletes the relationships touching the person, so no
dangling edges remain; family-chart also filters links to missing people.
- Optional cascade=true recursively deletes descendants (GEDCOM cleanup);
the person page asks "only this person" vs "with all descendants".
- DELETE returns {deleted: n}.
Family view surfaces "Not connected to anyone" so dangling people aren't lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
35 lines
1.3 KiB
Python
35 lines
1.3 KiB
Python
"""User — a person with login. Identity is internal so one user can link
|
|
multiple auth providers later (the provider-link table arrives with the auth
|
|
slice). ``hashed_password`` is nullable: external/OIDC users have none.
|
|
"""
|
|
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import DateTime, ForeignKey, String
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from app.models.base import Base
|
|
from app.models.mixins import SoftDelete, Timestamps, UUIDPrimaryKey
|
|
|
|
|
|
class User(Base, UUIDPrimaryKey, Timestamps, SoftDelete):
|
|
__tablename__ = "users"
|
|
|
|
email: Mapped[str] = mapped_column(String(320), unique=True, index=True)
|
|
email_verified_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
|
display_name: Mapped[str | None] = mapped_column(String(255))
|
|
hashed_password: Mapped[str | None] = mapped_column(String(255))
|
|
# The Person record that *is* this user ("home person"). Cleared if that
|
|
# person is deleted, so the link can never dangle.
|
|
self_person_id: Mapped[uuid.UUID | None] = mapped_column(
|
|
# use_alter + explicit name: users<->persons<->trees form an FK cycle,
|
|
# so this constraint must be created/dropped via ALTER, not inline.
|
|
ForeignKey(
|
|
"persons.id",
|
|
ondelete="SET NULL",
|
|
name="fk_users_self_person_id",
|
|
use_alter=True,
|
|
)
|
|
)
|