mirror of
https://github.com/kennethreitz/kjvstudy.org.git
synced 2026-06-05 23:00:16 +00:00
Add more interesting family tree statistics
New statistics added to the family tree overview: Most Siblings: - Tracks which person had the most siblings - Currently shows Nathan with 18 siblings (one of David's 19 children) Close Family Marriages: - Detects marriages between close relatives - Checks for sibling marriages and aunt/uncle-niece/nephew relationships - Shows 0 in current GEDCOM data - Provides historical context: "common in early biblical times" - Important for understanding biblical/ancient Near Eastern culture API Changes: - Add most_siblings field to FamilyTreeStatsResponse - Add close_family_marriages field with description - Calculate sibling counts from GEDCOM relationship data - Detect close family relationships through parent/sibling analysis Template Updates: - Add "Most Siblings" row with clickable link to person page - Add "Close Family Marriages" row with contextual note - Populate values via JavaScript from stats API Test Updates: - Add assertions for most_siblings structure - Add assertions for close_family_marriages value - Verify all new fields are present and correctly typed This helps provide educational context about how family structures differed in ancient biblical times compared to modern norms. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -266,8 +266,10 @@ class FamilyTreeStatsResponse(BaseModel):
|
||||
total_generations: int = Field(..., json_schema_extra={"example": 77})
|
||||
longest_lived: PersonStat
|
||||
most_children: PersonStat
|
||||
most_siblings: PersonStat
|
||||
average_lifespan: Optional[float] = Field(None, json_schema_extra={"example": 256.5})
|
||||
total_with_known_ages: int = Field(..., json_schema_extra={"example": 156})
|
||||
close_family_marriages: int = Field(..., json_schema_extra={"example": 3}, description="Marriages between close relatives (common in early biblical times)")
|
||||
|
||||
|
||||
# Mapping of category names to their data dictionaries
|
||||
@@ -2025,6 +2027,14 @@ def api_family_tree_stats():
|
||||
most_children_person_id = None
|
||||
most_children_count = 0
|
||||
|
||||
# Find person with most siblings
|
||||
most_siblings_person = None
|
||||
most_siblings_person_id = None
|
||||
most_siblings_count = 0
|
||||
|
||||
# Track close family marriages
|
||||
close_family_marriages_count = 0
|
||||
|
||||
# Calculate average lifespan
|
||||
total_age = 0
|
||||
people_with_ages = 0
|
||||
@@ -2092,6 +2102,36 @@ def api_family_tree_stats():
|
||||
most_children_person = person
|
||||
most_children_person_id = person_id
|
||||
|
||||
# Check siblings count
|
||||
siblings_count = len(person.get("siblings", []))
|
||||
if siblings_count > most_siblings_count:
|
||||
most_siblings_count = siblings_count
|
||||
most_siblings_person = person
|
||||
most_siblings_person_id = person_id
|
||||
|
||||
# Check for close family marriages (if person has spouse)
|
||||
if person.get("spouse"):
|
||||
spouse_name = person.get("spouse")
|
||||
# Check if spouse is in the family tree
|
||||
for potential_spouse_id, potential_spouse in family_tree_data.items():
|
||||
if potential_spouse.get("name") == spouse_name:
|
||||
# Check if they share parents (siblings)
|
||||
person_parents = set(person.get("parents", []))
|
||||
spouse_parents = set(potential_spouse.get("parents", []))
|
||||
|
||||
if person_parents and spouse_parents and person_parents & spouse_parents:
|
||||
# They share at least one parent - siblings or half-siblings
|
||||
close_family_marriages_count += 0.5 # Count each marriage once (will be seen from both sides)
|
||||
|
||||
# Check if spouse is parent's sibling (aunt/uncle-niece/nephew)
|
||||
for parent_id in person.get("parents", []):
|
||||
if parent_id in family_tree_data:
|
||||
parent_siblings = family_tree_data[parent_id].get("siblings", [])
|
||||
if potential_spouse_id in parent_siblings:
|
||||
close_family_marriages_count += 0.5
|
||||
|
||||
break
|
||||
|
||||
# Calculate average lifespan
|
||||
average_lifespan = round(total_age / people_with_ages, 1) if people_with_ages > 0 else None
|
||||
|
||||
@@ -2111,8 +2151,15 @@ def api_family_tree_stats():
|
||||
"value": most_children_count,
|
||||
"additional_info": f"Had {most_children_count} children" if most_children_person else None
|
||||
},
|
||||
"most_siblings": {
|
||||
"name": most_siblings_person["name"] if most_siblings_person else "Unknown",
|
||||
"person_id": most_siblings_person_id if most_siblings_person_id else "unknown",
|
||||
"value": most_siblings_count,
|
||||
"additional_info": f"Had {most_siblings_count} siblings" if most_siblings_person else None
|
||||
},
|
||||
"average_lifespan": average_lifespan,
|
||||
"total_with_known_ages": people_with_ages
|
||||
"total_with_known_ages": people_with_ages,
|
||||
"close_family_marriages": int(close_family_marriages_count)
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to load family tree statistics: {str(e)}")
|
||||
|
||||
@@ -181,6 +181,14 @@ section:nth-of-type(3) {
|
||||
<td>Most Children:</td>
|
||||
<td id="stat-most-children">—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Most Siblings:</td>
|
||||
<td id="stat-most-siblings">—</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Close Family Marriages:</td>
|
||||
<td id="stat-close-marriages">—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
@@ -303,6 +311,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_children.value} children)`;
|
||||
}
|
||||
|
||||
// Most siblings
|
||||
if (data.most_siblings && data.most_siblings.value > 0) {
|
||||
const personName = data.most_siblings.name;
|
||||
const personId = data.most_siblings.person_id;
|
||||
document.getElementById('stat-most-siblings').innerHTML =
|
||||
`<a href="/family-tree/person/${personId}">${personName}</a> (${data.most_siblings.value} siblings)`;
|
||||
}
|
||||
|
||||
// Close family marriages
|
||||
if (data.close_family_marriages !== undefined) {
|
||||
document.getElementById('stat-close-marriages').textContent =
|
||||
data.close_family_marriages + ' (common in early biblical times)';
|
||||
}
|
||||
|
||||
// Show stats, hide loading
|
||||
statsLoading.style.display = 'none';
|
||||
statsContainer.style.display = 'table';
|
||||
|
||||
@@ -933,8 +933,10 @@ class TestFamilyTreeEndpoints:
|
||||
assert "total_generations" in data
|
||||
assert "longest_lived" in data
|
||||
assert "most_children" in data
|
||||
assert "most_siblings" in data
|
||||
assert "average_lifespan" in data
|
||||
assert "total_with_known_ages" in data
|
||||
assert "close_family_marriages" in data
|
||||
|
||||
# Verify types
|
||||
assert isinstance(data["total_people"], int)
|
||||
@@ -961,9 +963,23 @@ class TestFamilyTreeEndpoints:
|
||||
assert isinstance(data["most_children"]["value"], int)
|
||||
assert data["most_children"]["value"] >= 0
|
||||
|
||||
# Verify most_siblings structure
|
||||
assert "name" in data["most_siblings"]
|
||||
assert "person_id" in data["most_siblings"]
|
||||
assert "value" in data["most_siblings"]
|
||||
assert "additional_info" in data["most_siblings"]
|
||||
assert isinstance(data["most_siblings"]["name"], str)
|
||||
assert isinstance(data["most_siblings"]["person_id"], str)
|
||||
assert isinstance(data["most_siblings"]["value"], int)
|
||||
assert data["most_siblings"]["value"] >= 0
|
||||
|
||||
# Verify average lifespan is either a number or null
|
||||
assert data["average_lifespan"] is None or isinstance(data["average_lifespan"], (int, float))
|
||||
|
||||
# Verify close family marriages is an integer
|
||||
assert isinstance(data["close_family_marriages"], int)
|
||||
assert data["close_family_marriages"] >= 0
|
||||
|
||||
def test_api_index_includes_new_endpoints(self, client):
|
||||
"""Test that API index includes all new endpoints"""
|
||||
response = client.get("/api/")
|
||||
|
||||
Reference in New Issue
Block a user