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>
84 lines
3.1 KiB
Python
84 lines
3.1 KiB
Python
"""Deletion integrity (relationship cleanup + cascade) and the self-person link."""
|
|
|
|
from tests.conftest import auth, register
|
|
|
|
|
|
async def _setup(client, email):
|
|
h = auth(await register(client, email))
|
|
tid = (await client.post("/api/v1/trees", json={"name": "T"}, headers=h)).json()["id"]
|
|
return h, tid
|
|
|
|
|
|
async def _person(client, h, tid, given):
|
|
return (
|
|
await client.post(f"/api/v1/trees/{tid}/persons", json={"given": given}, headers=h)
|
|
).json()["id"]
|
|
|
|
|
|
async def _link_parent(client, h, tid, parent, child):
|
|
await client.post(
|
|
f"/api/v1/trees/{tid}/relationships",
|
|
json={"type": "parent_child", "person_from_id": parent, "person_to_id": child},
|
|
headers=h,
|
|
)
|
|
|
|
|
|
async def test_delete_removes_relationships(client):
|
|
h, tid = await _setup(client, "d-rels@example.com")
|
|
gp = await _person(client, h, tid, "Grandpa")
|
|
dad = await _person(client, h, tid, "Dad")
|
|
await _link_parent(client, h, tid, gp, dad)
|
|
|
|
r = await client.delete(f"/api/v1/trees/{tid}/persons/{gp}", headers=h)
|
|
assert r.status_code == 200 and r.json()["deleted"] == 1
|
|
|
|
# The dangling edge is gone, so the tree view can't break on it.
|
|
rels = (
|
|
await client.get(f"/api/v1/trees/{tid}/relationships", headers=h)
|
|
).json()
|
|
assert rels == []
|
|
# Dad survives.
|
|
ppl = {p["id"] for p in (await client.get(f"/api/v1/trees/{tid}/persons", headers=h)).json()}
|
|
assert dad in ppl and gp not in ppl
|
|
|
|
|
|
async def test_cascade_deletes_descendants(client):
|
|
h, tid = await _setup(client, "d-cascade@example.com")
|
|
gp = await _person(client, h, tid, "Grandpa")
|
|
dad = await _person(client, h, tid, "Dad")
|
|
kid = await _person(client, h, tid, "Kid")
|
|
await _link_parent(client, h, tid, gp, dad)
|
|
await _link_parent(client, h, tid, dad, kid)
|
|
|
|
r = await client.delete(f"/api/v1/trees/{tid}/persons/{gp}?cascade=true", headers=h)
|
|
assert r.status_code == 200 and r.json()["deleted"] == 3
|
|
ppl = (await client.get(f"/api/v1/trees/{tid}/persons", headers=h)).json()
|
|
assert ppl == []
|
|
|
|
|
|
async def test_self_person_link(client):
|
|
h, tid = await _setup(client, "self@example.com")
|
|
me = await _person(client, h, tid, "Me")
|
|
|
|
r = await client.patch(
|
|
"/api/v1/users/me/self-person", json={"self_person_id": me}, headers=h
|
|
)
|
|
assert r.status_code == 200 and r.json()["self_person_id"] == me
|
|
|
|
# Reflected on /me.
|
|
assert (await client.get("/api/v1/users/me", headers=h)).json()["self_person_id"] == me
|
|
|
|
# Deleting that person clears the link (SET NULL).
|
|
await client.delete(f"/api/v1/trees/{tid}/persons/{me}", headers=h)
|
|
assert (await client.get("/api/v1/users/me", headers=h)).json()["self_person_id"] is None
|
|
|
|
|
|
async def test_self_person_clear(client):
|
|
h, tid = await _setup(client, "self-clear@example.com")
|
|
me = await _person(client, h, tid, "Me")
|
|
await client.patch("/api/v1/users/me/self-person", json={"self_person_id": me}, headers=h)
|
|
r = await client.patch(
|
|
"/api/v1/users/me/self-person", json={"self_person_id": None}, headers=h
|
|
)
|
|
assert r.status_code == 200 and r.json()["self_person_id"] is None
|