Cleanup tool: "mark deceased by a child's birth year" rule
Adds a preview/apply rule to the Cleanup tool for parents who have NO birth date
of their own (so the existing born-on-or-before rule can't reach them) but who
have a child born long ago — they're necessarily deceased. This is the gap that
left ~56 parents in the Paul tree as "unknown".
- cleanup_service.preview_deceased_by_child(year): parents of any child born
on/before the cutoff, excluding already-deceased; returns child_birth_year.
- GET /trees/{id}/cleanup/deceased-by-child?born_on_or_before=1900. Apply reuses
the existing POST .../cleanup/deceased (same audited mark-deceased path).
- Frontend: a new card in the Cleanup tool (year input → preview → select →
apply), preview-first like the rest of the tool.
Test covers preview (finds the no-birthdate parent of a pre-cutoff child,
excludes modern-child parents), child_birth_year, apply, and re-preview drop.
Suite 106 passing.
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
@@ -6,6 +6,7 @@ from app.api.deps import CurrentUser, SessionDep
|
|||||||
from app.schemas.cleanup import (
|
from app.schemas.cleanup import (
|
||||||
CleanupResult,
|
CleanupResult,
|
||||||
DeceasedApply,
|
DeceasedApply,
|
||||||
|
DeceasedByChildCandidate,
|
||||||
DeceasedCandidate,
|
DeceasedCandidate,
|
||||||
GenderApply,
|
GenderApply,
|
||||||
GenderProposal,
|
GenderProposal,
|
||||||
@@ -31,6 +32,24 @@ async def preview_deceased(
|
|||||||
return [DeceasedCandidate(**r) for r in rows]
|
return [DeceasedCandidate(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/{tree_id}/cleanup/deceased-by-child", response_model=list[DeceasedByChildCandidate]
|
||||||
|
)
|
||||||
|
async def preview_deceased_by_child(
|
||||||
|
tree_id: uuid.UUID,
|
||||||
|
session: SessionDep,
|
||||||
|
current: CurrentUser,
|
||||||
|
born_on_or_before: int = 1900,
|
||||||
|
) -> list[DeceasedByChildCandidate]:
|
||||||
|
"""People with a child born on/before the cutoff — necessarily deceased even
|
||||||
|
when their own birth date is missing. Apply via POST .../cleanup/deceased."""
|
||||||
|
tree = await tree_service.get_tree(session, viewer_id=current.id, tree_id=tree_id)
|
||||||
|
rows = await cleanup_service.preview_deceased_by_child(
|
||||||
|
session, actor=current, tree=tree, year=born_on_or_before
|
||||||
|
)
|
||||||
|
return [DeceasedByChildCandidate(**r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{tree_id}/cleanup/deceased", response_model=CleanupResult)
|
@router.post("/{tree_id}/cleanup/deceased", response_model=CleanupResult)
|
||||||
async def apply_deceased(
|
async def apply_deceased(
|
||||||
tree_id: uuid.UUID, data: DeceasedApply, session: SessionDep, current: CurrentUser
|
tree_id: uuid.UUID, data: DeceasedApply, session: SessionDep, current: CurrentUser
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ class DeceasedCandidate(BaseModel):
|
|||||||
birth_year: int
|
birth_year: int
|
||||||
|
|
||||||
|
|
||||||
|
class DeceasedByChildCandidate(BaseModel):
|
||||||
|
person_id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
child_birth_year: int
|
||||||
|
|
||||||
|
|
||||||
class DeceasedApply(BaseModel):
|
class DeceasedApply(BaseModel):
|
||||||
person_ids: list[uuid.UUID]
|
person_ids: list[uuid.UUID]
|
||||||
|
|
||||||
|
|||||||
@@ -133,6 +133,51 @@ async def apply_deceased(
|
|||||||
return len(persons)
|
return len(persons)
|
||||||
|
|
||||||
|
|
||||||
|
# ---- 1b. Mark deceased by a CHILD's birth year -------------------------------------
|
||||||
|
# For parents whose own birth date is missing (so the birth-year rule can't reach
|
||||||
|
# them) but who have a child born long ago — they're necessarily deceased. Applies
|
||||||
|
# through the same apply_deceased() path.
|
||||||
|
|
||||||
|
async def preview_deceased_by_child(
|
||||||
|
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)
|
||||||
|
rels = (
|
||||||
|
await session.execute(
|
||||||
|
select(Relationship).where(
|
||||||
|
Relationship.tree_id == tree.id,
|
||||||
|
Relationship.deleted_at.is_(None),
|
||||||
|
Relationship.type == RelationshipType.parent_child,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
# parent id -> earliest child birth year, among children born on/before `year`.
|
||||||
|
earliest_child: dict[uuid.UUID, int] = {}
|
||||||
|
for r in rels:
|
||||||
|
cy = years.get(r.person_to_id) # the child's birth year
|
||||||
|
if cy is None or cy > year:
|
||||||
|
continue
|
||||||
|
if r.person_from_id not in earliest_child or cy < earliest_child[r.person_from_id]:
|
||||||
|
earliest_child[r.person_from_id] = cy
|
||||||
|
persons = {p.id: p for p in await _persons(session, tree.id)}
|
||||||
|
out: list[dict] = []
|
||||||
|
for parent_id, cy in earliest_child.items():
|
||||||
|
p = persons.get(parent_id)
|
||||||
|
if p is None or p.is_living is False: # gone or already deceased
|
||||||
|
continue
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"person_id": str(parent_id),
|
||||||
|
"name": _display(names.get(parent_id)),
|
||||||
|
"child_birth_year": cy,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
out.sort(key=lambda r: r["child_birth_year"])
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
# ---- 2. Re-derive gender from a source GEDCOM (matches by name) ----------------------
|
# ---- 2. Re-derive gender from a source GEDCOM (matches by name) ----------------------
|
||||||
|
|
||||||
async def preview_gender(
|
async def preview_gender(
|
||||||
|
|||||||
@@ -51,6 +51,53 @@ async def test_deceased_preview_and_apply(client):
|
|||||||
assert old not in [r["person_id"] for r in prev2]
|
assert old not in [r["person_id"] for r in prev2]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_deceased_by_child_preview_and_apply(client):
|
||||||
|
h, tid = await _tree(client, "cl-decchild@example.com")
|
||||||
|
# Parent with NO birth date (the gap the birth-year rule can't reach).
|
||||||
|
parent = await _person(client, h, tid, "Gesche", "Frerking")
|
||||||
|
child = await _person(client, h, tid, "Kindt", "Frerking")
|
||||||
|
await _birth(client, h, tid, child, 1880) # child born before the cutoff
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/relationships",
|
||||||
|
json={"type": "parent_child", "person_from_id": parent, "person_to_id": child},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
# A parent of a modern child must NOT be flagged.
|
||||||
|
p_modern = await _person(client, h, tid, "Modern", "Parent")
|
||||||
|
c_modern = await _person(client, h, tid, "Kid", "Parent")
|
||||||
|
await _birth(client, h, tid, c_modern, 1990)
|
||||||
|
await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/relationships",
|
||||||
|
json={"type": "parent_child", "person_from_id": p_modern, "person_to_id": c_modern},
|
||||||
|
headers=h,
|
||||||
|
)
|
||||||
|
|
||||||
|
prev = (
|
||||||
|
await client.get(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/deceased-by-child?born_on_or_before=1900", headers=h
|
||||||
|
)
|
||||||
|
).json()
|
||||||
|
ids = [r["person_id"] for r in prev]
|
||||||
|
assert parent in ids and p_modern not in ids
|
||||||
|
assert next(r for r in prev if r["person_id"] == parent)["child_birth_year"] == 1880
|
||||||
|
|
||||||
|
# Apply through the shared deceased endpoint.
|
||||||
|
r = await client.post(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/deceased", json={"person_ids": [parent]}, headers=h
|
||||||
|
)
|
||||||
|
assert r.status_code == 200 and r.json()["updated"] == 1
|
||||||
|
assert (
|
||||||
|
await client.get(f"/api/v1/trees/{tid}/persons/{parent}", headers=h)
|
||||||
|
).json()["is_living"] is False
|
||||||
|
# Re-preview drops the now-deceased parent.
|
||||||
|
prev2 = (
|
||||||
|
await client.get(
|
||||||
|
f"/api/v1/trees/{tid}/cleanup/deceased-by-child?born_on_or_before=1900", headers=h
|
||||||
|
)
|
||||||
|
).json()
|
||||||
|
assert parent not in [r["person_id"] for r in prev2]
|
||||||
|
|
||||||
|
|
||||||
async def test_gender_from_spouse_preview_and_apply(client):
|
async def test_gender_from_spouse_preview_and_apply(client):
|
||||||
h, tid = await _tree(client, "cl-spouse@example.com")
|
h, tid = await _tree(client, "cl-spouse@example.com")
|
||||||
husband = (
|
husband = (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
|
||||||
type Deceased = components["schemas"]["DeceasedCandidate"];
|
type Deceased = components["schemas"]["DeceasedCandidate"];
|
||||||
|
type DeceasedByChild = components["schemas"]["DeceasedByChildCandidate"];
|
||||||
type GenderProp = components["schemas"]["GenderProposal"];
|
type GenderProp = components["schemas"]["GenderProposal"];
|
||||||
type NameIssue = components["schemas"]["NameIssue"];
|
type NameIssue = components["schemas"]["NameIssue"];
|
||||||
type Person = components["schemas"]["PersonRead"];
|
type Person = components["schemas"]["PersonRead"];
|
||||||
@@ -31,6 +32,12 @@ export default function CleanupPage() {
|
|||||||
const [decSel, setDecSel] = useState<Set<string>>(new Set());
|
const [decSel, setDecSel] = useState<Set<string>>(new Set());
|
||||||
const [decMsg, setDecMsg] = useState<string | null>(null);
|
const [decMsg, setDecMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 1b) Deceased by a child's birth year (for parents with no birth date)
|
||||||
|
const [childYear, setChildYear] = useState(1900);
|
||||||
|
const [decByChild, setDecByChild] = useState<DeceasedByChild[] | null>(null);
|
||||||
|
const [dbcSel, setDbcSel] = useState<Set<string>>(new Set());
|
||||||
|
const [dbcMsg, setDbcMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
// 2) Gender from source GEDCOM
|
// 2) Gender from source GEDCOM
|
||||||
const [gender, setGender] = useState<GenderProp[] | null>(null);
|
const [gender, setGender] = useState<GenderProp[] | null>(null);
|
||||||
const [genSel, setGenSel] = useState<Set<string>>(new Set());
|
const [genSel, setGenSel] = useState<Set<string>>(new Set());
|
||||||
@@ -63,6 +70,23 @@ export default function CleanupPage() {
|
|||||||
setDeceased(null);
|
setDeceased(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function previewDeceasedByChild() {
|
||||||
|
setDbcMsg(null);
|
||||||
|
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/deceased-by-child", {
|
||||||
|
params: { path: { tree_id: treeId }, query: { born_on_or_before: childYear } },
|
||||||
|
});
|
||||||
|
setDecByChild(data ?? []);
|
||||||
|
setDbcSel(new Set((data ?? []).map((d) => d.person_id)));
|
||||||
|
}
|
||||||
|
async function applyDeceasedByChild() {
|
||||||
|
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/deceased", {
|
||||||
|
params: { path: { tree_id: treeId } },
|
||||||
|
body: { person_ids: [...dbcSel] },
|
||||||
|
});
|
||||||
|
setDbcMsg(`Marked ${data?.updated ?? 0} people deceased.`);
|
||||||
|
setDecByChild(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function previewGender(e: React.ChangeEvent<HTMLInputElement>) {
|
async function previewGender(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (genFile.current) genFile.current.value = "";
|
if (genFile.current) genFile.current.value = "";
|
||||||
@@ -231,6 +255,59 @@ export default function CleanupPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* 1b) Deceased by a child's birth year */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Mark deceased by a child’s birth year</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
<p className="text-sm text-[var(--muted)]">
|
||||||
|
Catches parents who have <strong>no birth date of their own</strong> (so the rule
|
||||||
|
above can’t reach them) but who have a child born long ago — they’re necessarily
|
||||||
|
deceased.
|
||||||
|
</p>
|
||||||
|
<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)]">Has a child born on or before</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="w-28"
|
||||||
|
value={childYear}
|
||||||
|
onChange={(e) => setChildYear(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Button variant="outline" onClick={previewDeceasedByChild}>
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{dbcMsg && <p className="text-sm text-bronze">{dbcMsg}</p>}
|
||||||
|
{decByChild && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-[var(--muted)]">
|
||||||
|
{decByChild.length} people with a child born ≤ {childYear} (not already marked
|
||||||
|
deceased).
|
||||||
|
</p>
|
||||||
|
<ul className="max-h-64 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||||
|
{decByChild.map((d) => (
|
||||||
|
<li key={d.person_id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={dbcSel.has(d.person_id)}
|
||||||
|
onChange={() => toggle(dbcSel, d.person_id, setDbcSel)}
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{d.name}</span>
|
||||||
|
<span className="text-xs text-[var(--muted)]">child b. {d.child_birth_year}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{decByChild.length > 0 && (
|
||||||
|
<Button onClick={applyDeceasedByChild}>Mark {dbcSel.size} deceased</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 2) Gender from source */}
|
{/* 2) Gender from source */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
Vendored
+66
@@ -718,6 +718,27 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/deceased-by-child": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Preview Deceased By Child
|
||||||
|
* @description People with a child born on/before the cutoff — necessarily deceased even
|
||||||
|
* when their own birth date is missing. Apply via POST .../cleanup/deceased.
|
||||||
|
*/
|
||||||
|
get: operations["preview_deceased_by_child_api_v1_trees__tree_id__cleanup_deceased_by_child_get"];
|
||||||
|
put?: never;
|
||||||
|
post?: never;
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1286,6 +1307,18 @@ export interface components {
|
|||||||
/** Person Ids */
|
/** Person Ids */
|
||||||
person_ids: string[];
|
person_ids: string[];
|
||||||
};
|
};
|
||||||
|
/** DeceasedByChildCandidate */
|
||||||
|
DeceasedByChildCandidate: {
|
||||||
|
/**
|
||||||
|
* Person Id
|
||||||
|
* Format: uuid
|
||||||
|
*/
|
||||||
|
person_id: string;
|
||||||
|
/** Name */
|
||||||
|
name: string;
|
||||||
|
/** Child Birth Year */
|
||||||
|
child_birth_year: number;
|
||||||
|
};
|
||||||
/** DeceasedCandidate */
|
/** DeceasedCandidate */
|
||||||
DeceasedCandidate: {
|
DeceasedCandidate: {
|
||||||
/**
|
/**
|
||||||
@@ -4013,6 +4046,39 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
preview_deceased_by_child_api_v1_trees__tree_id__cleanup_deceased_by_child_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"]["DeceasedByChildCandidate"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @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: {
|
preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@@ -2873,6 +2873,64 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/trees/{tree_id}/cleanup/deceased-by-child": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"summary": "Preview Deceased By Child",
|
||||||
|
"description": "People with a child born on/before the cutoff \u2014 necessarily deceased even\nwhen their own birth date is missing. Apply via POST .../cleanup/deceased.",
|
||||||
|
"operationId": "preview_deceased_by_child_api_v1_trees__tree_id__cleanup_deceased_by_child_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": 1900,
|
||||||
|
"title": "Born On Or Before"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/DeceasedByChildCandidate"
|
||||||
|
},
|
||||||
|
"title": "Response Preview Deceased By Child Api V1 Trees Tree Id Cleanup Deceased By Child Get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -4897,6 +4955,30 @@
|
|||||||
],
|
],
|
||||||
"title": "DeceasedApply"
|
"title": "DeceasedApply"
|
||||||
},
|
},
|
||||||
|
"DeceasedByChildCandidate": {
|
||||||
|
"properties": {
|
||||||
|
"person_id": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Person Id"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Name"
|
||||||
|
},
|
||||||
|
"child_birth_year": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Child Birth Year"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"person_id",
|
||||||
|
"name",
|
||||||
|
"child_birth_year"
|
||||||
|
],
|
||||||
|
"title": "DeceasedByChildCandidate"
|
||||||
|
},
|
||||||
"DeceasedCandidate": {
|
"DeceasedCandidate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"person_id": {
|
"person_id": {
|
||||||
|
|||||||
Reference in New Issue
Block a user