Files
provenance/backend/tests/test_person_page_fetch.py
justin 58400ffdf7 Person page: server-side search; stop loading the whole tree
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>
2026-06-11 08:29:13 -04:00

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