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>
55 lines
2.5 KiB
Python
55 lines
2.5 KiB
Python
"""Soft-delete + recovery for trees and people."""
|
|
|
|
from tests.conftest import auth, register
|
|
|
|
|
|
async def test_tree_delete_and_restore(client):
|
|
h = auth(await register(client, "rec1@example.com"))
|
|
tree_id = (await client.post("/api/v1/trees", json={"name": "T"}, headers=h)).json()["id"]
|
|
|
|
# Delete -> gone from active lists, present in the recovery list.
|
|
assert (await client.delete(f"/api/v1/trees/{tree_id}", headers=h)).status_code == 204
|
|
assert len((await client.get("/api/v1/trees", headers=h)).json()) == 0
|
|
# A soft-deleted tree is no longer visible (404 to the would-be viewer).
|
|
gone = await client.get(f"/api/v1/trees/{tree_id}", headers=h)
|
|
assert gone.status_code == 404
|
|
deleted = (await client.get("/api/v1/trees?deleted=true", headers=h)).json()
|
|
assert len(deleted) == 1 and deleted[0]["id"] == tree_id
|
|
|
|
# Restore -> back in active lists.
|
|
assert (await client.post(f"/api/v1/trees/{tree_id}/restore", headers=h)).status_code == 200
|
|
assert len((await client.get("/api/v1/trees", headers=h)).json()) == 1
|
|
assert (await client.get(f"/api/v1/trees/{tree_id}", headers=h)).status_code == 200
|
|
|
|
|
|
async def test_only_owner_can_delete_tree(client):
|
|
owner = auth(await register(client, "rec-owner@example.com"))
|
|
tree_id = (await client.post("/api/v1/trees", json={"name": "T"}, headers=owner)).json()["id"]
|
|
other = auth(await register(client, "rec-other@example.com"))
|
|
blocked = await client.delete(f"/api/v1/trees/{tree_id}", headers=other)
|
|
assert blocked.status_code in (403, 404)
|
|
|
|
|
|
async def test_person_delete_and_restore(client):
|
|
h = auth(await register(client, "rec2@example.com"))
|
|
tree_id = (await client.post("/api/v1/trees", json={"name": "T"}, headers=h)).json()["id"]
|
|
person_id = (
|
|
await client.post(
|
|
f"/api/v1/trees/{tree_id}/persons", json={"given": "Ada"}, headers=h
|
|
)
|
|
).json()["id"]
|
|
|
|
assert (
|
|
await client.delete(f"/api/v1/trees/{tree_id}/persons/{person_id}", headers=h)
|
|
).status_code == 200
|
|
assert len((await client.get(f"/api/v1/trees/{tree_id}/persons", headers=h)).json()) == 0
|
|
deleted = (
|
|
await client.get(f"/api/v1/trees/{tree_id}/persons?deleted=true", headers=h)
|
|
).json()
|
|
assert len(deleted) == 1 and deleted[0]["primary_name"] == "Ada"
|
|
|
|
assert (
|
|
await client.post(f"/api/v1/trees/{tree_id}/persons/{person_id}/restore", headers=h)
|
|
).status_code == 200
|
|
assert len((await client.get(f"/api/v1/trees/{tree_id}/persons", headers=h)).json()) == 1
|