Fix #169: keep citation links on GEDCOM export

Export emitted SOUR records but never the per-fact SOUR links, so a
Provenance→Provenance round-trip destroyed the sources graph (citations were
dropped). Emit citation links on the facts they sit on:
- person-level → 1 SOUR @Sx@ (2 PAGE)
- name-level   → 2 SOUR under 1 NAME
- event-level  → 2 SOUR under the event (incl. partnership events in FAM)
- relationship → 1 SOUR under FAM
Citations whose source didn't export are skipped.

Test: a person + event citation round-trips through export→import into a fresh
tree with their pages intact. GEDCOM suite 6 passed.

Closes #169

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-09 12:37:03 -04:00
parent 39e3eac3df
commit b4434cb5dd
2 changed files with 92 additions and 0 deletions
+52
View File
@@ -77,6 +77,58 @@ async def test_gedcom_export_and_reimport(client):
assert resp.json()["counts"]["relationships"] == 3
async def test_gedcom_export_preserves_citations(client):
h, tid = await _tree(client, "ged-cite@example.com")
pid = (
await client.post(
f"/api/v1/trees/{tid}/persons", json={"given": "Ada", "surname": "Vance"}, headers=h
)
).json()["id"]
eid = (
await client.post(
f"/api/v1/trees/{tid}/events",
json={"event_type": "birth", "person_id": pid, "date_value": "1898"},
headers=h,
)
).json()["id"]
sid = (
await client.post(
f"/api/v1/trees/{tid}/sources", json={"title": "1900 Census"}, headers=h
)
).json()["id"]
# A person-level and an event-level citation on the same source.
await client.post(
f"/api/v1/trees/{tid}/citations",
json={"source_id": sid, "person_id": pid, "page": "p.12"},
headers=h,
)
await client.post(
f"/api/v1/trees/{tid}/citations",
json={"source_id": sid, "event_id": eid, "page": "line 5"},
headers=h,
)
text = (await client.get(f"/api/v1/trees/{tid}/gedcom/export", headers=h)).text
# Citation links + pages are emitted (previously dropped).
assert "1 SOUR @S1@" in text # person-level
assert "2 PAGE p.12" in text
assert "2 SOUR @S1@" in text # event-level (under 1 BIRT)
assert "3 PAGE line 5" in text
# Round-trip into a fresh tree: the citations survive.
tid2 = (await client.post("/api/v1/trees", json={"name": "RT"}, headers=h)).json()["id"]
await client.post(
f"/api/v1/trees/{tid2}/gedcom/import",
files={"file": ("rt.ged", text.encode(), "text/plain")},
headers=h,
)
cites = (await client.get(f"/api/v1/trees/{tid2}/citations", headers=h)).json()
assert len(cites) >= 2
assert any(c["person_id"] for c in cites)
assert any(c["event_id"] for c in cites)
assert {"p.12", "line 5"} <= {c.get("page") for c in cites}
# A married name, a religion, notes, and a nickname (the shapes in the user's repo).
RICH = b"""0 HEAD
1 CHAR UTF-8