58400ffdf7
The person page fetched the entire tree on every open — all persons (to build a
name map + power the relative pickers) and all events (to find partnership
events). On a 2k-person tree that's a ~230KB person list + ~600KB event list per
view. Now it loads only what the page shows:
Frontend:
- The relationship & spouse pickers use the backend's fuzzy pg_trgm search
(debounced, typo-tolerant) instead of substring-filtering a preloaded array —
better search, and no need to preload every person. PersonCombobox gained an
`onSearch` server mode (client `people` mode still works).
- The page drops the all-persons and all-events fetches; it resolves just this
person's relatives' names via GET /persons?ids=..., and reads partnership
events from the per-person events endpoint.
Backend:
- GET /trees/{id}/persons?ids=a,b,c — batch by id (privacy-filtered, names
batched), for relative-name display.
- list_events_for_person (member path) now also returns the person's partnership
events, so the page needn't scan every event in the tree.
Adversarial review (frontend logic + backend/privacy) found no issues. Suite 105
passing.
Signed-off-by: Justin Paul <justin@jpaul.me>
61 lines
2.6 KiB
Python
61 lines
2.6 KiB
Python
"""Backing the trimmed person-page fetch: batch persons by id (for relative-name
|
|
display) and partnership events on the per-person events endpoint (so the page
|
|
doesn't load every event in the tree)."""
|
|
|
|
from tests.conftest import auth, register
|
|
|
|
|
|
async def _tree(client, h):
|
|
return (await client.post("/api/v1/trees", json={"name": "T"}, headers=h)).json()["id"]
|
|
|
|
|
|
async def test_list_persons_by_ids(client):
|
|
h = auth(await register(client, "ids@ex.com"))
|
|
tid = await _tree(client, h)
|
|
a = (await client.post(f"/api/v1/trees/{tid}/persons", json={"given": "Aaa"}, headers=h)).json()["id"]
|
|
b = (await client.post(f"/api/v1/trees/{tid}/persons", json={"given": "Bbb"}, headers=h)).json()["id"]
|
|
c = (await client.post(f"/api/v1/trees/{tid}/persons", json={"given": "Ccc"}, headers=h)).json()["id"]
|
|
|
|
r = await client.get(f"/api/v1/trees/{tid}/persons", params={"ids": f"{a},{c}"}, headers=h)
|
|
assert r.status_code == 200
|
|
assert {p["id"] for p in r.json()} == {a, c} # only the requested, not b
|
|
assert all(p["primary_name"] for p in r.json()) # names resolved
|
|
|
|
assert (
|
|
await client.get(f"/api/v1/trees/{tid}/persons", params={"ids": "nope"}, headers=h)
|
|
).status_code == 422
|
|
assert (
|
|
await client.get(f"/api/v1/trees/{tid}/persons", params={"ids": ""}, headers=h)
|
|
).json() == []
|
|
|
|
|
|
async def test_person_events_include_partnership(client):
|
|
h = auth(await register(client, "pev@ex.com"))
|
|
tid = await _tree(client, h)
|
|
p1 = (await client.post(f"/api/v1/trees/{tid}/persons", json={"given": "P1"}, headers=h)).json()["id"]
|
|
p2 = (await client.post(f"/api/v1/trees/{tid}/persons", json={"given": "P2"}, headers=h)).json()["id"]
|
|
await client.post(
|
|
f"/api/v1/trees/{tid}/events",
|
|
json={"event_type": "birth", "person_id": p1, "date_value": "1900"},
|
|
headers=h,
|
|
)
|
|
rel = (
|
|
await client.post(
|
|
f"/api/v1/trees/{tid}/relationships",
|
|
json={"type": "partnership", "person_from_id": p1, "person_to_id": p2},
|
|
headers=h,
|
|
)
|
|
).json()["id"]
|
|
await client.post(
|
|
f"/api/v1/trees/{tid}/events",
|
|
json={"event_type": "marriage", "relationship_id": rel, "date_value": "1925"},
|
|
headers=h,
|
|
)
|
|
|
|
# P1's events: own birth + the partnership marriage, in one call.
|
|
e1 = {e["event_type"] for e in (await client.get(f"/api/v1/trees/{tid}/persons/{p1}/events", headers=h)).json()}
|
|
assert {"birth", "marriage"} <= e1
|
|
# The marriage shows on BOTH partners' pages.
|
|
e2 = {e["event_type"] for e in (await client.get(f"/api/v1/trees/{tid}/persons/{p2}/events", headers=h)).json()}
|
|
assert "marriage" in e2
|