Merge pull request 'Tree Cleanup tool: bulk deceased / gender-from-source / name fixes (preview-first)' (#31) from tree-cleanup into main
This commit was merged in pull request #31.
This commit is contained in:
@@ -5,6 +5,7 @@ from fastapi import APIRouter
|
|||||||
from app.api.v1 import (
|
from app.api.v1 import (
|
||||||
auth,
|
auth,
|
||||||
citations,
|
citations,
|
||||||
|
cleanup,
|
||||||
events,
|
events,
|
||||||
gedcom,
|
gedcom,
|
||||||
media,
|
media,
|
||||||
@@ -28,3 +29,4 @@ api_router.include_router(sources.router)
|
|||||||
api_router.include_router(citations.router)
|
api_router.include_router(citations.router)
|
||||||
api_router.include_router(media.router)
|
api_router.include_router(media.router)
|
||||||
api_router.include_router(gedcom.router)
|
api_router.include_router(gedcom.router)
|
||||||
|
api_router.include_router(cleanup.router)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, File, UploadFile
|
||||||
|
|
||||||
|
from app.api.deps import CurrentUser, SessionDep
|
||||||
|
from app.schemas.cleanup import (
|
||||||
|
CleanupResult,
|
||||||
|
DeceasedApply,
|
||||||
|
DeceasedCandidate,
|
||||||
|
GenderApply,
|
||||||
|
GenderProposal,
|
||||||
|
NameApply,
|
||||||
|
NameIssue,
|
||||||
|
)
|
||||||
|
from app.services import cleanup_service, tree_service
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/trees", tags=["cleanup"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tree_id}/cleanup/deceased", response_model=list[DeceasedCandidate])
|
||||||
|
async def preview_deceased(
|
||||||
|
tree_id: uuid.UUID,
|
||||||
|
session: SessionDep,
|
||||||
|
current: CurrentUser,
|
||||||
|
born_on_or_before: int = 1930,
|
||||||
|
) -> list[DeceasedCandidate]:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
rows = await cleanup_service.preview_deceased(
|
||||||
|
session, actor=current, tree=tree, year=born_on_or_before
|
||||||
|
)
|
||||||
|
return [DeceasedCandidate(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{tree_id}/cleanup/deceased", response_model=CleanupResult)
|
||||||
|
async def apply_deceased(
|
||||||
|
tree_id: uuid.UUID, data: DeceasedApply, session: SessionDep, current: CurrentUser
|
||||||
|
) -> CleanupResult:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
n = await cleanup_service.apply_deceased(
|
||||||
|
session, actor=current, tree=tree, person_ids=data.person_ids
|
||||||
|
)
|
||||||
|
return CleanupResult(updated=n)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{tree_id}/cleanup/gender/preview", response_model=list[GenderProposal])
|
||||||
|
async def preview_gender(
|
||||||
|
tree_id: uuid.UUID,
|
||||||
|
session: SessionDep,
|
||||||
|
current: CurrentUser,
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
) -> list[GenderProposal]:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
text = (await file.read()).decode("utf-8", errors="replace")
|
||||||
|
rows = await cleanup_service.preview_gender(
|
||||||
|
session, actor=current, tree=tree, gedcom_text=text
|
||||||
|
)
|
||||||
|
return [GenderProposal(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{tree_id}/cleanup/gender", response_model=CleanupResult)
|
||||||
|
async def apply_gender(
|
||||||
|
tree_id: uuid.UUID, data: GenderApply, session: SessionDep, current: CurrentUser
|
||||||
|
) -> CleanupResult:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
n = await cleanup_service.apply_gender(
|
||||||
|
session,
|
||||||
|
actor=current,
|
||||||
|
tree=tree,
|
||||||
|
updates=[u.model_dump() for u in data.updates],
|
||||||
|
)
|
||||||
|
return CleanupResult(updated=n)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{tree_id}/cleanup/names", response_model=list[NameIssue])
|
||||||
|
async def preview_names(
|
||||||
|
tree_id: uuid.UUID, session: SessionDep, current: CurrentUser
|
||||||
|
) -> list[NameIssue]:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
rows = await cleanup_service.preview_names(session, actor=current, tree=tree)
|
||||||
|
return [NameIssue(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{tree_id}/cleanup/names", response_model=CleanupResult)
|
||||||
|
async def apply_names(
|
||||||
|
tree_id: uuid.UUID, data: NameApply, session: SessionDep, current: CurrentUser
|
||||||
|
) -> CleanupResult:
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
n = await cleanup_service.apply_names(
|
||||||
|
session, actor=current, tree=tree, edits=[e.model_dump() for e in data.edits]
|
||||||
|
)
|
||||||
|
return CleanupResult(updated=n)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class DeceasedCandidate(BaseModel):
|
||||||
|
person_id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
birth_year: int
|
||||||
|
|
||||||
|
|
||||||
|
class DeceasedApply(BaseModel):
|
||||||
|
person_ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|
||||||
|
class GenderProposal(BaseModel):
|
||||||
|
person_id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
proposed_gender: str
|
||||||
|
|
||||||
|
|
||||||
|
class GenderUpdate(BaseModel):
|
||||||
|
person_id: uuid.UUID
|
||||||
|
gender: str
|
||||||
|
|
||||||
|
|
||||||
|
class GenderApply(BaseModel):
|
||||||
|
updates: list[GenderUpdate]
|
||||||
|
|
||||||
|
|
||||||
|
class NameIssue(BaseModel):
|
||||||
|
name_id: uuid.UUID
|
||||||
|
person_id: uuid.UUID
|
||||||
|
given: str | None = None
|
||||||
|
surname: str | None = None
|
||||||
|
issue: str
|
||||||
|
|
||||||
|
|
||||||
|
class NameEdit(BaseModel):
|
||||||
|
name_id: uuid.UUID
|
||||||
|
given: str | None = None
|
||||||
|
surname: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class NameApply(BaseModel):
|
||||||
|
edits: list[NameEdit]
|
||||||
|
|
||||||
|
|
||||||
|
class CleanupResult(BaseModel):
|
||||||
|
updated: int
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
"""Bulk tree cleanup — preview/apply pairs for common import messes.
|
||||||
|
|
||||||
|
Per the project's #1 rule (the assistant proposes, humans approve), each fix has
|
||||||
|
a *preview* that returns the proposed changes and an *apply* that commits only
|
||||||
|
the ids/edits the user confirmed. Nothing here mutates without an explicit apply
|
||||||
|
call carrying the user's selections.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.models.event import Event
|
||||||
|
from app.models.person import Name, Person
|
||||||
|
from app.models.tree import Tree
|
||||||
|
from app.models.user import User
|
||||||
|
from app.services import gedcom, privacy
|
||||||
|
from app.services.audit import record_audit
|
||||||
|
from app.services.exceptions import Forbidden, NotFound
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_editor(session: AsyncSession, *, actor: User, tree: Tree) -> None:
|
||||||
|
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
|
||||||
|
raise Forbidden("not an editor of this tree")
|
||||||
|
|
||||||
|
|
||||||
|
async def _persons(session: AsyncSession, tree_id: uuid.UUID) -> list[Person]:
|
||||||
|
return list(
|
||||||
|
(
|
||||||
|
await session.execute(
|
||||||
|
select(Person).where(Person.tree_id == tree_id, Person.deleted_at.is_(None))
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _primary_name_by_person(
|
||||||
|
session: AsyncSession, tree_id: uuid.UUID
|
||||||
|
) -> dict[uuid.UUID, Name]:
|
||||||
|
names = (
|
||||||
|
await session.execute(
|
||||||
|
select(Name)
|
||||||
|
.where(Name.tree_id == tree_id, Name.deleted_at.is_(None))
|
||||||
|
.order_by(Name.is_primary.desc(), Name.sort_order)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
out: dict[uuid.UUID, Name] = {}
|
||||||
|
for n in names:
|
||||||
|
out.setdefault(n.person_id, n)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def _birth_year_by_person(session: AsyncSession, tree_id: uuid.UUID) -> dict[uuid.UUID, int]:
|
||||||
|
evs = (
|
||||||
|
await session.execute(
|
||||||
|
select(Event).where(
|
||||||
|
Event.tree_id == tree_id,
|
||||||
|
Event.deleted_at.is_(None),
|
||||||
|
Event.event_type == "birth",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
out: dict[uuid.UUID, int] = {}
|
||||||
|
for e in evs:
|
||||||
|
if not e.person_id or e.person_id in out:
|
||||||
|
continue
|
||||||
|
y = e.date_start.year if e.date_start else None
|
||||||
|
if y is None:
|
||||||
|
ys = gedcom._year(e.date_value)
|
||||||
|
y = int(ys) if ys else None
|
||||||
|
if y is not None:
|
||||||
|
out[e.person_id] = y
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _display(n: Name | None) -> str:
|
||||||
|
if n is None:
|
||||||
|
return "Unnamed"
|
||||||
|
return " ".join(x for x in (n.given, n.surname) if x) or (n.display_name or "Unnamed")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 1. Mark deceased by birth year -------------------------------------------------
|
||||||
|
|
||||||
|
async def preview_deceased(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, year: int
|
||||||
|
) -> list[dict]:
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
names = await _primary_name_by_person(session, tree.id)
|
||||||
|
years = await _birth_year_by_person(session, tree.id)
|
||||||
|
out: list[dict] = []
|
||||||
|
for p in await _persons(session, tree.id):
|
||||||
|
if p.is_living is False: # already deceased
|
||||||
|
continue
|
||||||
|
by = years.get(p.id)
|
||||||
|
if by is not None and by <= year:
|
||||||
|
out.append(
|
||||||
|
{"person_id": str(p.id), "name": _display(names.get(p.id)), "birth_year": by}
|
||||||
|
)
|
||||||
|
out.sort(key=lambda r: r["birth_year"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def apply_deceased(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, person_ids: list[uuid.UUID]
|
||||||
|
) -> int:
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
persons = (
|
||||||
|
await session.execute(
|
||||||
|
select(Person).where(
|
||||||
|
Person.tree_id == tree.id,
|
||||||
|
Person.deleted_at.is_(None),
|
||||||
|
Person.id.in_(person_ids),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
for p in persons:
|
||||||
|
p.is_living = False
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="cleanup_deceased",
|
||||||
|
entity_type="Tree",
|
||||||
|
entity_id=tree.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
after={"count": len(persons)},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return len(persons)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 2. Re-derive gender from a source GEDCOM (matches by name) ----------------------
|
||||||
|
|
||||||
|
async def preview_gender(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, gedcom_text: str
|
||||||
|
) -> list[dict]:
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
name2sex: dict[str, str] = {}
|
||||||
|
for rec in gedcom.parse_records(gedcom_text):
|
||||||
|
if rec.tag != "INDI":
|
||||||
|
continue
|
||||||
|
summ = gedcom._person_summary(rec)
|
||||||
|
sex = gedcom._sex(rec.text("SEX"))
|
||||||
|
if sex and summ["norm"]:
|
||||||
|
name2sex.setdefault(summ["norm"], sex)
|
||||||
|
|
||||||
|
names = await _primary_name_by_person(session, tree.id)
|
||||||
|
out: list[dict] = []
|
||||||
|
for p in await _persons(session, tree.id):
|
||||||
|
if p.gender: # only fill in what's missing
|
||||||
|
continue
|
||||||
|
nm = names.get(p.id)
|
||||||
|
if nm is None:
|
||||||
|
continue
|
||||||
|
proposed = name2sex.get(gedcom._norm(nm.given, nm.surname))
|
||||||
|
if proposed:
|
||||||
|
out.append({"person_id": str(p.id), "name": _display(nm), "proposed_gender": proposed})
|
||||||
|
out.sort(key=lambda r: r["name"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def apply_gender(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, updates: list[dict]
|
||||||
|
) -> int:
|
||||||
|
"""updates: [{person_id, gender}]."""
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
wanted = {uuid.UUID(str(u["person_id"])): u["gender"] for u in updates if u.get("gender")}
|
||||||
|
persons = (
|
||||||
|
await session.execute(
|
||||||
|
select(Person).where(
|
||||||
|
Person.tree_id == tree.id,
|
||||||
|
Person.deleted_at.is_(None),
|
||||||
|
Person.id.in_(wanted.keys()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
for p in persons:
|
||||||
|
p.gender = wanted[p.id]
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="cleanup_gender",
|
||||||
|
entity_type="Tree",
|
||||||
|
entity_id=tree.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
after={"count": len(persons)},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return len(persons)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 3. Flag malformed names for review --------------------------------------------
|
||||||
|
|
||||||
|
_YEAR_RE = re.compile(r"\b\d{3,4}\b")
|
||||||
|
|
||||||
|
|
||||||
|
def _name_issue(n: Name) -> str | None:
|
||||||
|
given = (n.given or "").strip()
|
||||||
|
surname = (n.surname or "").strip()
|
||||||
|
if _YEAR_RE.search(surname) or re.search(r"\d", surname):
|
||||||
|
return "date_in_surname"
|
||||||
|
if re.search(r"\d", given):
|
||||||
|
return "date_in_given"
|
||||||
|
# A given name with many tokens often means a maiden+married name was packed
|
||||||
|
# in (e.g. "Mary Smith Jones") — surface it for a human to split.
|
||||||
|
if surname == "" and len(given.split()) >= 2:
|
||||||
|
return "no_surname"
|
||||||
|
if len(given.split()) >= 3:
|
||||||
|
return "packed_given"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def preview_names(session: AsyncSession, *, actor: User, tree: Tree) -> list[dict]:
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
names = (
|
||||||
|
await session.execute(
|
||||||
|
select(Name).where(Name.tree_id == tree.id, Name.deleted_at.is_(None))
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
out: list[dict] = []
|
||||||
|
for n in names:
|
||||||
|
issue = _name_issue(n)
|
||||||
|
if issue:
|
||||||
|
out.append({
|
||||||
|
"name_id": str(n.id),
|
||||||
|
"person_id": str(n.person_id),
|
||||||
|
"given": n.given,
|
||||||
|
"surname": n.surname,
|
||||||
|
"issue": issue,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
async def apply_names(
|
||||||
|
session: AsyncSession, *, actor: User, tree: Tree, edits: list[dict]
|
||||||
|
) -> int:
|
||||||
|
"""edits: [{name_id, given, surname}] — the user's corrected values."""
|
||||||
|
await _require_editor(session, actor=actor, tree=tree)
|
||||||
|
by_id = {uuid.UUID(str(e["name_id"])): e for e in edits}
|
||||||
|
rows = (
|
||||||
|
await session.execute(
|
||||||
|
select(Name).where(
|
||||||
|
Name.tree_id == tree.id,
|
||||||
|
Name.deleted_at.is_(None),
|
||||||
|
Name.id.in_(by_id.keys()),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
if len(rows) != len(by_id):
|
||||||
|
raise NotFound("one or more names not found in this tree")
|
||||||
|
for n in rows:
|
||||||
|
e = by_id[n.id]
|
||||||
|
n.given = (e.get("given") or "").strip() or None
|
||||||
|
n.surname = (e.get("surname") or "").strip() or None
|
||||||
|
n.display_name = None # rebuild from parts
|
||||||
|
record_audit(
|
||||||
|
session,
|
||||||
|
action="cleanup_names",
|
||||||
|
entity_type="Tree",
|
||||||
|
entity_id=tree.id,
|
||||||
|
tree_id=tree.id,
|
||||||
|
actor_user_id=actor.id,
|
||||||
|
after={"count": len(rows)},
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
return len(rows)
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Tree cleanup: preview/apply for deceased-by-year, gender-from-source, names."""
|
||||||
|
|
||||||
|
from tests.conftest import auth, register
|
||||||
|
|
||||||
|
|
||||||
|
async def _tree(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, surname=None):
|
||||||
|
return (
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/persons", json={"given": given, "surname": surname}, headers=h
|
||||||
|
)
|
||||||
|
).json()["id"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _birth(client, h, tid, pid, year):
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/events",
|
||||||
|
json={"event_type": "birth", "person_id": pid, "date_value": str(year)},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_deceased_preview_and_apply(client):
|
||||||
|
h, tid = await _tree(client, "cl-dec@example.com")
|
||||||
|
old = await _person(client, h, tid, "Josias", "Moody")
|
||||||
|
young = await _person(client, h, tid, "Kid", "Moody")
|
||||||
|
await _birth(client, h, tid, old, 1900)
|
||||||
|
await _birth(client, h, tid, young, 1990)
|
||||||
|
|
||||||
|
prev = (
|
||||||
|
await client.get(f"/api/v1/trees/{tid}/cleanup/deceased?born_on_or_before=1930", headers=h)
|
||||||
|
).json()
|
||||||
|
assert [r["person_id"] for r in prev] == [old]
|
||||||
|
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/deceased", json={"person_ids": [old]}, headers=h
|
||||||
|
)
|
||||||
|
assert r.status_code == 200 and r.json()["updated"] == 1
|
||||||
|
assert (
|
||||||
|
await client.get(f"/api/v1/trees/{tid}/persons/{old}", headers=h)
|
||||||
|
).json()["is_living"] is False
|
||||||
|
# Re-preview no longer lists the now-deceased person.
|
||||||
|
prev2 = (
|
||||||
|
await client.get(f"/api/v1/trees/{tid}/cleanup/deceased?born_on_or_before=1930", headers=h)
|
||||||
|
).json()
|
||||||
|
assert old not in [r["person_id"] for r in prev2]
|
||||||
|
|
||||||
|
|
||||||
|
GED = b"""0 HEAD
|
||||||
|
0 @I1@ INDI
|
||||||
|
1 NAME Josias /Moody/
|
||||||
|
1 SEX M
|
||||||
|
0 @I2@ INDI
|
||||||
|
1 NAME Flora /Paul/
|
||||||
|
1 SEX F
|
||||||
|
0 TRLR
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def test_gender_from_source(client):
|
||||||
|
h, tid = await _tree(client, "cl-gen@example.com")
|
||||||
|
await _person(client, h, tid, "Josias", "Moody")
|
||||||
|
await _person(client, h, tid, "Flora", "Paul")
|
||||||
|
await _person(client, h, tid, "Nobody", "Else") # not in source
|
||||||
|
|
||||||
|
prev = await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/gender/preview",
|
||||||
|
files={"file": ("src.ged", GED, "text/plain")},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
props = prev.json()
|
||||||
|
by_name = {p["name"]: p["proposed_gender"] for p in props}
|
||||||
|
assert by_name == {"Josias Moody": "male", "Flora Paul": "female"}
|
||||||
|
|
||||||
|
updates = [{"person_id": p["person_id"], "gender": p["proposed_gender"]} for p in props]
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/gender", json={"updates": updates}, headers=h
|
||||||
|
)
|
||||||
|
assert r.status_code == 200 and r.json()["updated"] == 2
|
||||||
|
people = (await client.get(f"/api/v1/trees/{tid}/persons", headers=h)).json()
|
||||||
|
genders = {p["primary_name"]: p["gender"] for p in people}
|
||||||
|
assert genders["Josias Moody"] == "male" and genders["Flora Paul"] == "female"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_name_issues_preview_and_fix(client):
|
||||||
|
h, tid = await _tree(client, "cl-name@example.com")
|
||||||
|
# surname got a date; real surname landed in the given name.
|
||||||
|
bad = await _person(client, h, tid, "Henry Paul", "1859")
|
||||||
|
await _person(client, h, tid, "Normal", "Person") # should not be flagged
|
||||||
|
|
||||||
|
issues = (await client.get(f"/api/v1/trees/{tid}/cleanup/names", headers=h)).json()
|
||||||
|
assert len(issues) == 1 and issues[0]["issue"] == "date_in_surname"
|
||||||
|
name_id = issues[0]["name_id"]
|
||||||
|
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/names",
|
||||||
|
json={"edits": [{"name_id": name_id, "given": "Henry", "surname": "Paul"}]},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200 and r.json()["updated"] == 1
|
||||||
|
person = (await client.get(f"/api/v1/trees/{tid}/persons/{bad}", headers=h)).json()
|
||||||
|
assert person["primary_name"] == "Henry Paul"
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
|
import { api } from "@/lib/api/client";
|
||||||
|
import type { components } from "@/lib/api/schema";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
|
type Deceased = components["schemas"]["DeceasedCandidate"];
|
||||||
|
type GenderProp = components["schemas"]["GenderProposal"];
|
||||||
|
type NameIssue = components["schemas"]["NameIssue"];
|
||||||
|
|
||||||
|
const ISSUE_LABEL: Record<string, string> = {
|
||||||
|
date_in_surname: "date in surname",
|
||||||
|
date_in_given: "date in given name",
|
||||||
|
no_surname: "no surname",
|
||||||
|
packed_given: "long given name",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CleanupPage() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const treeId = params.id;
|
||||||
|
|
||||||
|
// 1) Deceased by birth year
|
||||||
|
const [year, setYear] = useState(1930);
|
||||||
|
const [deceased, setDeceased] = useState<Deceased[] | null>(null);
|
||||||
|
const [decSel, setDecSel] = useState<Set<string>>(new Set());
|
||||||
|
const [decMsg, setDecMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 2) Gender from source GEDCOM
|
||||||
|
const [gender, setGender] = useState<GenderProp[] | null>(null);
|
||||||
|
const [genSel, setGenSel] = useState<Set<string>>(new Set());
|
||||||
|
const [genMsg, setGenMsg] = useState<string | null>(null);
|
||||||
|
const genFile = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// 3) Name issues
|
||||||
|
const [issues, setIssues] = useState<NameIssue[] | null>(null);
|
||||||
|
const [edits, setEdits] = useState<Record<string, { given: string; surname: string; on: boolean }>>({});
|
||||||
|
const [nameMsg, setNameMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function previewDeceased() {
|
||||||
|
setDecMsg(null);
|
||||||
|
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/deceased", {
|
||||||
|
params: { path: { tree_id: treeId }, query: { born_on_or_before: year } },
|
||||||
|
});
|
||||||
|
setDeceased(data ?? []);
|
||||||
|
setDecSel(new Set((data ?? []).map((d) => d.person_id)));
|
||||||
|
}
|
||||||
|
async function applyDeceased() {
|
||||||
|
const ids = [...decSel];
|
||||||
|
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/deceased", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: { person_ids: ids },
|
||||||
|
});
|
||||||
|
setDecMsg(`Marked ${data?.updated ?? 0} people deceased.`);
|
||||||
|
setDeceased(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function previewGender(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (genFile.current) genFile.current.value = "";
|
||||||
|
if (!file) return;
|
||||||
|
setGenMsg(null);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
const resp = await fetch(`/api/v1/trees/${treeId}/cleanup/gender/preview`, {
|
||||||
|
method: "POST",
|
||||||
|
body: fd,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (resp.ok) {
|
||||||
|
const data: GenderProp[] = await resp.json();
|
||||||
|
setGender(data);
|
||||||
|
setGenSel(new Set(data.map((g) => g.person_id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function applyGender() {
|
||||||
|
const updates = (gender ?? [])
|
||||||
|
.filter((g) => genSel.has(g.person_id))
|
||||||
|
.map((g) => ({ person_id: g.person_id, gender: g.proposed_gender }));
|
||||||
|
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/gender", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: { updates },
|
||||||
|
});
|
||||||
|
setGenMsg(`Set gender on ${data?.updated ?? 0} people.`);
|
||||||
|
setGender(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadNames = useCallback(async () => {
|
||||||
|
setNameMsg(null);
|
||||||
|
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/names", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
});
|
||||||
|
setIssues(data ?? []);
|
||||||
|
const init: Record<string, { given: string; surname: string; on: boolean }> = {};
|
||||||
|
for (const i of data ?? []) {
|
||||||
|
init[i.name_id] = { given: i.given ?? "", surname: i.surname ?? "", on: false };
|
||||||
|
}
|
||||||
|
setEdits(init);
|
||||||
|
}, [treeId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNames();
|
||||||
|
}, [loadNames]);
|
||||||
|
|
||||||
|
async function applyNames() {
|
||||||
|
const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on);
|
||||||
|
const body = {
|
||||||
|
edits: chosen.map((i) => ({
|
||||||
|
name_id: i.name_id,
|
||||||
|
given: edits[i.name_id].given,
|
||||||
|
surname: edits[i.name_id].surname,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
if (!body.edits.length) return;
|
||||||
|
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/names", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
setNameMsg(`Fixed ${data?.updated ?? 0} names.`);
|
||||||
|
loadNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = (set: Set<string>, id: string, setter: (s: Set<string>) => void) => {
|
||||||
|
const n = new Set(set);
|
||||||
|
if (n.has(id)) n.delete(id);
|
||||||
|
else n.add(id);
|
||||||
|
setter(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold">Cleanup</h1>
|
||||||
|
<p className="mt-1 text-sm text-[var(--muted)]">
|
||||||
|
Fix common import messes in bulk. Each tool previews its changes — nothing is saved
|
||||||
|
until you apply.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 1) Deceased by year */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Mark deceased by birth year</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<div className="flex flex-wrap items-end gap-2">
|
||||||
|
<label className="flex flex-col gap-1 text-sm">
|
||||||
|
<span className="text-xs text-[var(--muted)]">Born on or before</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="w-28"
|
||||||
|
value={year}
|
||||||
|
onChange={(e) => setYear(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button variant="outline" onClick={previewDeceased}>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{decMsg && <p className="text-sm text-bronze">{decMsg}</p>}
|
||||||
|
{deceased && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-[var(--muted)]">
|
||||||
|
{deceased.length} people born ≤ {year} (not already marked deceased).
|
||||||
|
</p>
|
||||||
|
<ul className="max-h-64 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||||
|
{deceased.map((d) => (
|
||||||
|
<li key={d.person_id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={decSel.has(d.person_id)}
|
||||||
|
onChange={() => toggle(decSel, d.person_id, setDecSel)}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{d.name}</span>
|
||||||
|
<span className="text-xs text-[var(--muted)]">b. {d.birth_year}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{deceased.length > 0 && (
|
||||||
|
<Button onClick={applyDeceased}>Mark {decSel.size} deceased</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 2) Gender from source */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Set sex from a source GEDCOM</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-sm text-[var(--muted)]">
|
||||||
|
Upload your source <code>.ged</code> (it carries each person’s sex). We match by
|
||||||
|
name and propose sex only for people who don’t have it set.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
ref={genFile}
|
||||||
|
type="file"
|
||||||
|
accept=".ged,.gedcom,text/plain"
|
||||||
|
onChange={previewGender}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<Button variant="outline" onClick={() => genFile.current?.click()}>
|
||||||
|
Choose source GEDCOM
|
||||||
|
</Button>
|
||||||
|
{genMsg && <p className="text-sm text-bronze">{genMsg}</p>}
|
||||||
|
{gender && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-[var(--muted)]">{gender.length} matches with a sex to set.</p>
|
||||||
|
<ul className="max-h-64 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||||
|
{gender.map((g) => (
|
||||||
|
<li key={g.person_id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={genSel.has(g.person_id)}
|
||||||
|
onChange={() => toggle(genSel, g.person_id, setGenSel)}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{g.name}</span>
|
||||||
|
<span
|
||||||
|
className="text-xs"
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
g.proposed_gender === "male"
|
||||||
|
? "rgb(120,159,172)"
|
||||||
|
: "rgb(196,138,146)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{g.proposed_gender}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{gender.length > 0 && (
|
||||||
|
<Button onClick={applyGender}>Set sex on {genSel.size} people</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 3) Name issues */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Names that look broken</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{nameMsg && <p className="text-sm text-bronze">{nameMsg}</p>}
|
||||||
|
{issues === null ? (
|
||||||
|
<p className="text-sm text-[var(--muted)]">Scanning…</p>
|
||||||
|
) : issues.length === 0 ? (
|
||||||
|
<p className="text-sm text-[var(--muted)]">No obvious name problems found.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-[var(--muted)]">
|
||||||
|
{issues.length} flagged. Edit given/surname, tick the ones to fix, then apply.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{issues.map((i) => {
|
||||||
|
const e = edits[i.name_id] ?? { given: "", surname: "", on: false };
|
||||||
|
return (
|
||||||
|
<li key={i.name_id} className="flex flex-wrap items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={e.on}
|
||||||
|
onChange={() =>
|
||||||
|
setEdits((p) => ({ ...p, [i.name_id]: { ...e, on: !e.on } }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="h-9 w-40"
|
||||||
|
placeholder="Given"
|
||||||
|
value={e.given}
|
||||||
|
onChange={(ev) =>
|
||||||
|
setEdits((p) => ({ ...p, [i.name_id]: { ...e, given: ev.target.value } }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="h-9 w-40"
|
||||||
|
placeholder="Surname"
|
||||||
|
value={e.surname}
|
||||||
|
onChange={(ev) =>
|
||||||
|
setEdits((p) => ({
|
||||||
|
...p,
|
||||||
|
[i.name_id]: { ...e, surname: ev.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="rounded bg-[var(--border)]/50 px-1.5 py-0.5 text-xs text-[var(--muted)]">
|
||||||
|
{ISSUE_LABEL[i.issue] ?? i.issue}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<Button onClick={applyNames}>Fix selected</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
LogOut,
|
LogOut,
|
||||||
Network,
|
Network,
|
||||||
Settings,
|
Settings,
|
||||||
|
Sparkles,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -128,6 +129,12 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
|||||||
icon={ArrowDownUp}
|
icon={ArrowDownUp}
|
||||||
active={pathname.startsWith(`/trees/${treeId}/gedcom`)}
|
active={pathname.startsWith(`/trees/${treeId}/gedcom`)}
|
||||||
/>
|
/>
|
||||||
|
<Item
|
||||||
|
href={`/trees/${treeId}/cleanup`}
|
||||||
|
label="Cleanup"
|
||||||
|
icon={Sparkles}
|
||||||
|
active={pathname.startsWith(`/trees/${treeId}/cleanup`)}
|
||||||
|
/>
|
||||||
<Item
|
<Item
|
||||||
href={`/trees/${treeId}/recovery`}
|
href={`/trees/${treeId}/recovery`}
|
||||||
label="Recovery"
|
label="Recovery"
|
||||||
|
|||||||
Vendored
+364
@@ -679,6 +679,76 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/deceased": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** Preview Deceased */
|
||||||
|
get: operations["preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get"];
|
||||||
|
put?: never;
|
||||||
|
/** Apply Deceased */
|
||||||
|
post: operations["apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** Preview Gender */
|
||||||
|
post: operations["preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/gender": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** Apply Gender */
|
||||||
|
post: operations["apply_gender_api_v1_trees__tree_id__cleanup_gender_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/names": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/** Preview Names */
|
||||||
|
get: operations["preview_names_api_v1_trees__tree_id__cleanup_names_get"];
|
||||||
|
put?: never;
|
||||||
|
/** Apply Names */
|
||||||
|
post: operations["apply_names_api_v1_trees__tree_id__cleanup_names_post"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
@@ -713,6 +783,11 @@ export interface components {
|
|||||||
/** File */
|
/** File */
|
||||||
file: string;
|
file: string;
|
||||||
};
|
};
|
||||||
|
/** Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post */
|
||||||
|
Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post: {
|
||||||
|
/** File */
|
||||||
|
file: string;
|
||||||
|
};
|
||||||
/** Body_upload_media_api_v1_trees__tree_id__media_post */
|
/** Body_upload_media_api_v1_trees__tree_id__media_post */
|
||||||
Body_upload_media_api_v1_trees__tree_id__media_post: {
|
Body_upload_media_api_v1_trees__tree_id__media_post: {
|
||||||
/** File */
|
/** File */
|
||||||
@@ -796,6 +871,28 @@ export interface components {
|
|||||||
detail?: string | null;
|
detail?: string | null;
|
||||||
confidence?: components["schemas"]["CitationConfidence"] | null;
|
confidence?: components["schemas"]["CitationConfidence"] | null;
|
||||||
};
|
};
|
||||||
|
/** CleanupResult */
|
||||||
|
CleanupResult: {
|
||||||
|
/** Updated */
|
||||||
|
updated: number;
|
||||||
|
};
|
||||||
|
/** DeceasedApply */
|
||||||
|
DeceasedApply: {
|
||||||
|
/** Person Ids */
|
||||||
|
person_ids: string[];
|
||||||
|
};
|
||||||
|
/** DeceasedCandidate */
|
||||||
|
DeceasedCandidate: {
|
||||||
|
/**
|
||||||
|
* Person Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
person_id: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Birth Year */
|
||||||
|
birth_year: number;
|
||||||
|
};
|
||||||
/** DuplicateMatch */
|
/** DuplicateMatch */
|
||||||
DuplicateMatch: {
|
DuplicateMatch: {
|
||||||
/** Xref */
|
/** Xref */
|
||||||
@@ -905,6 +1002,33 @@ export interface components {
|
|||||||
/** Notes */
|
/** Notes */
|
||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
};
|
};
|
||||||
|
/** GenderApply */
|
||||||
|
GenderApply: {
|
||||||
|
/** Updates */
|
||||||
|
updates: components["schemas"]["GenderUpdate"][];
|
||||||
|
};
|
||||||
|
/** GenderProposal */
|
||||||
|
GenderProposal: {
|
||||||
|
/**
|
||||||
|
* Person Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
person_id: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Proposed Gender */
|
||||||
|
proposed_gender: string;
|
||||||
|
};
|
||||||
|
/** GenderUpdate */
|
||||||
|
GenderUpdate: {
|
||||||
|
/**
|
||||||
|
* Person Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
person_id: string;
|
||||||
|
/** Gender */
|
||||||
|
gender: string;
|
||||||
|
};
|
||||||
/** HTTPValidationError */
|
/** HTTPValidationError */
|
||||||
HTTPValidationError: {
|
HTTPValidationError: {
|
||||||
/** Detail */
|
/** Detail */
|
||||||
@@ -984,6 +1108,11 @@ export interface components {
|
|||||||
/** Source Id */
|
/** Source Id */
|
||||||
source_id?: string | null;
|
source_id?: string | null;
|
||||||
};
|
};
|
||||||
|
/** NameApply */
|
||||||
|
NameApply: {
|
||||||
|
/** Edits */
|
||||||
|
edits: components["schemas"]["NameEdit"][];
|
||||||
|
};
|
||||||
/** NameCreate */
|
/** NameCreate */
|
||||||
NameCreate: {
|
NameCreate: {
|
||||||
/**
|
/**
|
||||||
@@ -1007,6 +1136,37 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
is_primary?: boolean;
|
is_primary?: boolean;
|
||||||
};
|
};
|
||||||
|
/** NameEdit */
|
||||||
|
NameEdit: {
|
||||||
|
/**
|
||||||
|
* Name Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
name_id: string;
|
||||||
|
/** Given */
|
||||||
|
given?: string | null;
|
||||||
|
/** Surname */
|
||||||
|
surname?: string | null;
|
||||||
|
};
|
||||||
|
/** NameIssue */
|
||||||
|
NameIssue: {
|
||||||
|
/**
|
||||||
|
* Name Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
name_id: string;
|
||||||
|
/**
|
||||||
|
* Person Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
person_id: string;
|
||||||
|
/** Given */
|
||||||
|
given?: string | null;
|
||||||
|
/** Surname */
|
||||||
|
surname?: string | null;
|
||||||
|
/** Issue */
|
||||||
|
issue: string;
|
||||||
|
};
|
||||||
/** NameRead */
|
/** NameRead */
|
||||||
NameRead: {
|
NameRead: {
|
||||||
/**
|
/**
|
||||||
@@ -3218,4 +3378,208 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: {
|
||||||
|
born_on_or_before?: number;
|
||||||
|
};
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["DeceasedCandidate"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["DeceasedApply"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CleanupResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"multipart/form-data": components["schemas"]["Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["GenderProposal"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
apply_gender_api_v1_trees__tree_id__cleanup_gender_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["GenderApply"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CleanupResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
preview_names_api_v1_trees__tree_id__cleanup_names_get: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["NameIssue"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
apply_names_api_v1_trees__tree_id__cleanup_names_post: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
tree_id: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["NameApply"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description Successful Response */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["CleanupResult"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Validation Error */
|
||||||
|
422: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["HTTPValidationError"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2701,6 +2701,322 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/deceased": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Preview Deceased",
|
||||||
|
"operationId": "preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "born_on_or_before",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"default": 1930,
|
||||||
|
"title": "Born On Or Before"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DeceasedCandidate"
|
||||||
|
},
|
||||||
|
"title": "Response Preview Deceased Api V1 Trees Tree Id Cleanup Deceased Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Apply Deceased",
|
||||||
|
"operationId": "apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeceasedApply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CleanupResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Preview Gender",
|
||||||
|
"operationId": "preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"multipart/form-data": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GenderProposal"
|
||||||
|
},
|
||||||
|
"title": "Response Preview Gender Api V1 Trees Tree Id Cleanup Gender Preview Post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/gender": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Apply Gender",
|
||||||
|
"operationId": "apply_gender_api_v1_trees__tree_id__cleanup_gender_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/GenderApply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CleanupResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/names": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Preview Names",
|
||||||
|
"operationId": "preview_names_api_v1_trees__tree_id__cleanup_names_get",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/NameIssue"
|
||||||
|
},
|
||||||
|
"title": "Response Preview Names Api V1 Trees Tree Id Cleanup Names Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Apply Names",
|
||||||
|
"operationId": "apply_names_api_v1_trees__tree_id__cleanup_names_post",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tree_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Tree Id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NameApply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CleanupResult"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@@ -2770,6 +3086,20 @@
|
|||||||
],
|
],
|
||||||
"title": "Body_preview_gedcom_api_v1_trees__tree_id__gedcom_preview_post"
|
"title": "Body_preview_gedcom_api_v1_trees__tree_id__gedcom_preview_post"
|
||||||
},
|
},
|
||||||
|
"Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post": {
|
||||||
|
"properties": {
|
||||||
|
"file": {
|
||||||
|
"type": "string",
|
||||||
|
"contentMediaType": "application/octet-stream",
|
||||||
|
"title": "File"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"file"
|
||||||
|
],
|
||||||
|
"title": "Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"
|
||||||
|
},
|
||||||
"Body_upload_media_api_v1_trees__tree_id__media_post": {
|
"Body_upload_media_api_v1_trees__tree_id__media_post": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"file": {
|
"file": {
|
||||||
@@ -3091,6 +3421,60 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "CitationUpdate"
|
"title": "CitationUpdate"
|
||||||
},
|
},
|
||||||
|
"CleanupResult": {
|
||||||
|
"properties": {
|
||||||
|
"updated": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Updated"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"updated"
|
||||||
|
],
|
||||||
|
"title": "CleanupResult"
|
||||||
|
},
|
||||||
|
"DeceasedApply": {
|
||||||
|
"properties": {
|
||||||
|
"person_ids": {
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Person Ids"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"person_ids"
|
||||||
|
],
|
||||||
|
"title": "DeceasedApply"
|
||||||
|
},
|
||||||
|
"DeceasedCandidate": {
|
||||||
|
"properties": {
|
||||||
|
"person_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"birth_year": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Birth Year"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"person_id",
|
||||||
|
"name",
|
||||||
|
"birth_year"
|
||||||
|
],
|
||||||
|
"title": "DeceasedCandidate"
|
||||||
|
},
|
||||||
"DuplicateMatch": {
|
"DuplicateMatch": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"xref": {
|
"xref": {
|
||||||
@@ -3526,6 +3910,65 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "EventUpdate"
|
"title": "EventUpdate"
|
||||||
},
|
},
|
||||||
|
"GenderApply": {
|
||||||
|
"properties": {
|
||||||
|
"updates": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/GenderUpdate"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Updates"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"updates"
|
||||||
|
],
|
||||||
|
"title": "GenderApply"
|
||||||
|
},
|
||||||
|
"GenderProposal": {
|
||||||
|
"properties": {
|
||||||
|
"person_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"proposed_gender": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Proposed Gender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"person_id",
|
||||||
|
"name",
|
||||||
|
"proposed_gender"
|
||||||
|
],
|
||||||
|
"title": "GenderProposal"
|
||||||
|
},
|
||||||
|
"GenderUpdate": {
|
||||||
|
"properties": {
|
||||||
|
"person_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"gender": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Gender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"person_id",
|
||||||
|
"gender"
|
||||||
|
],
|
||||||
|
"title": "GenderUpdate"
|
||||||
|
},
|
||||||
"HTTPValidationError": {
|
"HTTPValidationError": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"detail": {
|
"detail": {
|
||||||
@@ -3774,6 +4217,22 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "MediaUpdate"
|
"title": "MediaUpdate"
|
||||||
},
|
},
|
||||||
|
"NameApply": {
|
||||||
|
"properties": {
|
||||||
|
"edits": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/NameEdit"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Edits"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"edits"
|
||||||
|
],
|
||||||
|
"title": "NameApply"
|
||||||
|
},
|
||||||
"NameCreate": {
|
"NameCreate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"name_type": {
|
"name_type": {
|
||||||
@@ -3845,6 +4304,89 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"title": "NameCreate"
|
"title": "NameCreate"
|
||||||
},
|
},
|
||||||
|
"NameEdit": {
|
||||||
|
"properties": {
|
||||||
|
"name_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Name Id"
|
||||||
|
},
|
||||||
|
"given": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Given"
|
||||||
|
},
|
||||||
|
"surname": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Surname"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name_id"
|
||||||
|
],
|
||||||
|
"title": "NameEdit"
|
||||||
|
},
|
||||||
|
"NameIssue": {
|
||||||
|
"properties": {
|
||||||
|
"name_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Name Id"
|
||||||
|
},
|
||||||
|
"person_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"given": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Given"
|
||||||
|
},
|
||||||
|
"surname": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Surname"
|
||||||
|
},
|
||||||
|
"issue": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Issue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"name_id",
|
||||||
|
"person_id",
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"title": "NameIssue"
|
||||||
|
},
|
||||||
"NameRead": {
|
"NameRead": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
|
|||||||
Reference in New Issue
Block a user