#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 Donald N. Allingham
# Copyright (C) 2009 Gary Burton
# Copyright (C) 2011 Tim G L Lyons
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
"""
A utility to make a best guess if a person is alive. This is used to provide
privacy in reports and exports.
"""
#-------------------------------------------------------------------------
#
# Standard python modules
#
#-------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".gen.utils.alive")
#-------------------------------------------------------------------------
#
# Gramps modules
#
#-------------------------------------------------------------------------
from ..display.name import displayer as name_displayer
from ..lib.date import Date, Today
from ..errors import DatabaseError
from ..const import GRAMPS_LOCALE as glocale
_ = glocale.translation.sgettext
#-------------------------------------------------------------------------
#
# Constants from config .ini keys
#
#-------------------------------------------------------------------------
# cache values; use refresh_constants() if they change
try:
from ..config import config
_MAX_AGE_PROB_ALIVE = config.get('behavior.max-age-prob-alive')
_MAX_SIB_AGE_DIFF = config.get('behavior.max-sib-age-diff')
_AVG_GENERATION_GAP = config.get('behavior.avg-generation-gap')
except ImportError:
# Utils used as module not part of GRAMPS
_MAX_AGE_PROB_ALIVE = 110
_MAX_SIB_AGE_DIFF = 20
_AVG_GENERATION_GAP = 20
#-------------------------------------------------------------------------
#
# ProbablyAlive class
#
#-------------------------------------------------------------------------
[docs]class ProbablyAlive(object):
"""
An object to hold the parameters for considering someone alive.
"""
def __init__(self,
db,
max_sib_age_diff=None,
max_age_prob_alive=None,
avg_generation_gap=None):
self.db = db
if max_sib_age_diff is None:
max_sib_age_diff = _MAX_SIB_AGE_DIFF
if max_age_prob_alive is None:
max_age_prob_alive = _MAX_AGE_PROB_ALIVE
if avg_generation_gap is None:
avg_generation_gap = _AVG_GENERATION_GAP
self.MAX_SIB_AGE_DIFF = max_sib_age_diff
self.MAX_AGE_PROB_ALIVE = max_age_prob_alive
self.AVG_GENERATION_GAP = avg_generation_gap
[docs] def probably_alive_range(self, person, is_spouse=False):
# FIXME: some of these computed dates need to be a span. For
# example, if a person could be born +/- 20 yrs around
# a date then it should be a span, and yr_offset should
# deal with it as well ("between 1920 and 1930" + 10 =
# "between 1930 and 1940")
if person is None:
return (None, None, "", None)
birth_ref = person.get_birth_ref()
death_ref = person.get_death_ref()
death_date = None
birth_date = None
explain = ""
# If the recorded death year is before current year then
# things are simple.
if death_ref and death_ref.get_role().is_primary():
if death_ref:
death = self.db.get_event_from_handle(death_ref.ref)
if death:
if death.get_date_object().is_valid():
death_date = death.get_date_object()
else: # has a death event, but it is not valid:
death_date = Today() # before today
death_date.set_modifier(Date.MOD_BEFORE)
# Look for Cause Of Death, Burial or Cremation events.
# These are fairly good indications that someone's not alive.
if not death_date:
for ev_ref in person.get_primary_event_ref_list():
if ev_ref:
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_death_fallback():
death_date = ev.get_date_object()
if not death_date.is_valid():
death_date = Today() # before today
death_date.set_modifier(Date.MOD_BEFORE)
# If they were born within X years before current year then
# assume they are alive (we already know they are not dead).
if not birth_date:
if birth_ref and birth_ref.get_role().is_primary():
birth = self.db.get_event_from_handle(birth_ref.ref)
if birth and birth.get_date_object().get_start_date() != Date.EMPTY:
birth_date = birth.get_date_object()
# Look for Baptism, etc events.
# These are fairly good indications that someone's birth.
if not birth_date:
for ev_ref in person.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth_fallback():
birth_date = ev.get_date_object()
if not birth_date and death_date:
# person died more than MAX after current year
birth_date = death_date.copy_offset_ymd(year=-self.MAX_AGE_PROB_ALIVE)
explain = _("death date")
if not death_date and birth_date:
# person died more than MAX after current year
death_date = birth_date.copy_offset_ymd(year=self.MAX_AGE_PROB_ALIVE)
explain = _("birth date")
if death_date and birth_date:
return (birth_date, death_date, explain, person) # direct self evidence
# Neither birth nor death events are available. Try looking
# at siblings. If a sibling was born more than X years past,
# or more than Z future, then probably this person is
# not alive. If the sibling died more than X years
# past, or more than X years future, then probably not alive.
family_list = person.get_parent_family_handle_list()
for family_handle in family_list:
family = self.db.get_family_from_handle(family_handle)
if family is None:
continue
for child_ref in family.get_child_ref_list():
child_handle = child_ref.ref
child = self.db.get_person_from_handle(child_handle)
if child is None:
continue
# Go through once looking for direct evidence:
for ev_ref in child.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
# if sibling birth date too far away, then not alive:
year = dobj.get_year()
if year != 0:
# sibling birth date
return (Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF),
Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF + self.MAX_AGE_PROB_ALIVE),
_("sibling birth date"),
child)
elif ev and ev.type.is_death():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
# if sibling death date too far away, then not alive:
year = dobj.get_year()
if year != 0:
# sibling death date
return (Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF - self.MAX_AGE_PROB_ALIVE),
Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF - self.MAX_AGE_PROB_ALIVE
+ self.MAX_AGE_PROB_ALIVE),
_("sibling death date"),
child)
# Go through again looking for fallback:
for ev_ref in child.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
# if sibling birth date too far away, then not alive:
year = dobj.get_year()
if year != 0:
# sibling birth date
return (Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF),
Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF + self.MAX_AGE_PROB_ALIVE),
_("sibling birth-related date"),
child)
elif ev and ev.type.is_death_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
# if sibling death date too far away, then not alive:
year = dobj.get_year()
if year != 0:
# sibling death date
return (Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF - self.MAX_AGE_PROB_ALIVE),
Date().copy_ymd(year - self.MAX_SIB_AGE_DIFF - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE),
_("sibling death-related date"),
child)
if not is_spouse: # if you are not in recursion, let's recurse:
for family_handle in person.get_family_handle_list():
family = self.db.get_family_from_handle(family_handle)
if family:
mother_handle = family.get_mother_handle()
father_handle = family.get_father_handle()
if mother_handle == person.handle and father_handle:
father = self.db.get_person_from_handle(father_handle)
date1, date2, explain, other = self.probably_alive_range(father, is_spouse=True)
if date1 and date1.get_year() != 0:
return (Date().copy_ymd(date1.get_year() - self.AVG_GENERATION_GAP),
Date().copy_ymd(date1.get_year() - self.AVG_GENERATION_GAP + self.MAX_AGE_PROB_ALIVE),
_("a spouse's birth-related date, ") + explain, other)
elif date2 and date2.get_year() != 0:
return (Date().copy_ymd(date2.get_year() + self.AVG_GENERATION_GAP - self.MAX_AGE_PROB_ALIVE),
Date().copy_ymd(date2.get_year() + self.AVG_GENERATION_GAP),
_("a spouse's death-related date, ") + explain, other)
elif father_handle == person.handle and mother_handle:
mother = self.db.get_person_from_handle(mother_handle)
date1, date2, explain, other = self.probably_alive_range(mother, is_spouse=True)
if date1 and date1.get_year() != 0:
return (Date().copy_ymd(date1.get_year() - self.AVG_GENERATION_GAP),
Date().copy_ymd(date1.get_year() - self.AVG_GENERATION_GAP + self.MAX_AGE_PROB_ALIVE),
_("a spouse's birth-related date, ") + explain, other)
elif date2 and date2.get_year() != 0:
return (Date().copy_ymd(date2.get_year() + self.AVG_GENERATION_GAP - self.MAX_AGE_PROB_ALIVE),
Date().copy_ymd(date2.get_year() + self.AVG_GENERATION_GAP),
_("a spouse's death-related date, ") + explain, other)
# Let's check the family events and see if we find something
for ref in family.get_event_ref_list():
if ref:
event = self.db.get_event_from_handle(ref.ref)
if event:
date = event.get_date_object()
year = date.get_year()
if year != 0:
other = None
if person.handle == mother_handle and father_handle:
other = self.db.get_person_from_handle(father_handle)
elif person.handle == father_handle and mother_handle:
other = self.db.get_person_from_handle(mother_handle)
return (Date().copy_ymd(year - self.AVG_GENERATION_GAP),
Date().copy_ymd(year - self.AVG_GENERATION_GAP +
self.MAX_AGE_PROB_ALIVE),
_("event with spouse"), other)
# Try looking for descendants that were born more than a lifespan
# ago.
def descendants_too_old (person, years):
for family_handle in person.get_family_handle_list():
family = self.db.get_family_from_handle(family_handle)
if not family:
# can happen with LivingProxyDb(PrivateProxyDb(db))
continue
for child_ref in family.get_child_ref_list():
child_handle = child_ref.ref
child = self.db.get_person_from_handle(child_handle)
child_birth_ref = child.get_birth_ref()
if child_birth_ref:
child_birth = self.db.get_event_from_handle(child_birth_ref.ref)
dobj = child_birth.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
d = Date(dobj)
val = d.get_start_date()
val = d.get_year() - years
d.set_year(val)
return (d, d.copy_offset_ymd(self.MAX_AGE_PROB_ALIVE),
_("descendant birth date"),
child)
child_death_ref = child.get_death_ref()
if child_death_ref:
child_death = self.db.get_event_from_handle(child_death_ref.ref)
dobj = child_death.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- self.AVG_GENERATION_GAP),
dobj.copy_offset_ymd(- self.AVG_GENERATION_GAP + self.MAX_AGE_PROB_ALIVE),
_("descendant death date"),
child)
date1, date2, explain, other = descendants_too_old (child, years + self.AVG_GENERATION_GAP)
if date1 and date2:
return date1, date2, explain, other
# Check fallback data:
for ev_ref in child.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
d = Date(dobj)
val = d.get_start_date()
val = d.get_year() - years
d.set_year(val)
return (d, d.copy_offset_ymd(self.MAX_AGE_PROB_ALIVE),
_("descendant birth-related date"),
child)
elif ev and ev.type.is_death_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- self.AVG_GENERATION_GAP),
dobj.copy_offset_ymd(- self.AVG_GENERATION_GAP + self.MAX_AGE_PROB_ALIVE),
_("descendant death-related date"),
child)
return (None, None, "", None)
# If there are descendants that are too old for the person to have
# been alive in the current year then they must be dead.
date1, date2, explain, other = None, None, "", None
try:
date1, date2, explain, other = descendants_too_old(person, self.AVG_GENERATION_GAP)
except RuntimeError:
raise DatabaseError(
_("Database error: loop in %s's descendants") %
name_displayer.display(person))
if date1 and date2:
return (date1, date2, explain, other)
def ancestors_too_old(person, year):
LOG.debug("ancestors_too_old('%s', %s)".format(
name_displayer.display(person), year) )
family_handle = person.get_main_parents_family_handle()
if family_handle:
family = self.db.get_family_from_handle(family_handle)
if not family:
# can happen with LivingProxyDb(PrivateProxyDb(db))
return (None, None, "", None)
father_handle = family.get_father_handle()
if father_handle:
father = self.db.get_person_from_handle(father_handle)
father_birth_ref = father.get_birth_ref()
if father_birth_ref and father_birth_ref.get_role().is_primary():
father_birth = self.db.get_event_from_handle(
father_birth_ref.ref)
dobj = father_birth.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year),
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE),
_("ancestor birth date"),
father)
father_death_ref = father.get_death_ref()
if father_death_ref and father_death_ref.get_role().is_primary():
father_death = self.db.get_event_from_handle(
father_death_ref.ref)
dobj = father_death.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE),
_("ancestor death date"),
father)
# Check fallback data:
for ev_ref in father.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year),
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE),
_("ancestor birth-related date"),
father)
elif ev and ev.type.is_death_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE),
_("ancestor death-related date"),
father)
date1, date2, explain, other = ancestors_too_old (father, year - self.AVG_GENERATION_GAP)
if date1 and date2:
return date1, date2, explain, other
mother_handle = family.get_mother_handle()
if mother_handle:
mother = self.db.get_person_from_handle(mother_handle)
mother_birth_ref = mother.get_birth_ref()
if mother_birth_ref and mother_birth_ref.get_role().is_primary():
mother_birth = self.db.get_event_from_handle(mother_birth_ref.ref)
dobj = mother_birth.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year),
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE),
_("ancestor birth date"),
mother)
mother_death_ref = mother.get_death_ref()
if mother_death_ref and mother_death_ref.get_role().is_primary():
mother_death = self.db.get_event_from_handle(
mother_death_ref.ref)
dobj = mother_death.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE),
_("ancestor death date"),
mother)
# Check fallback data:
for ev_ref in mother.get_primary_event_ref_list():
ev = self.db.get_event_from_handle(ev_ref.ref)
if ev and ev.type.is_birth_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year),
dobj.copy_offset_ymd(- year + self.MAX_AGE_PROB_ALIVE),
_("ancestor birth-related date"),
mother)
elif ev and ev.type.is_death_fallback():
dobj = ev.get_date_object()
if dobj.get_start_date() != Date.EMPTY:
return (dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE),
dobj.copy_offset_ymd(- year - self.MAX_AGE_PROB_ALIVE + self.MAX_AGE_PROB_ALIVE),
_("ancestor death-related date"),
mother)
date1, date2, explain, other = ancestors_too_old (mother, year - self.AVG_GENERATION_GAP)
if date1 and date2:
return (date1, date2, explain, other)
return (None, None, "", None)
try:
# If there are ancestors that would be too old in the current year
# then assume our person must be dead too.
date1, date2, explain, other = ancestors_too_old (person, - self.AVG_GENERATION_GAP)
except RuntimeError:
raise Errors.DatabaseError(
_("Database error: loop in %s's ancestors") %
name_displayer.display(person))
if date1 and date2:
return (date1, date2, explain, other)
# If we can't find any reason to believe that they are dead we
# must assume they are alive.
return (None, None, "", None)
#-------------------------------------------------------------------------
#
# probably_alive
#
#-------------------------------------------------------------------------
[docs]def probably_alive(person, db,
current_date=None,
limit=0,
max_sib_age_diff=None,
max_age_prob_alive=None,
avg_generation_gap=None,
return_range=False):
"""
Return true if the person may be alive on current_date.
This works by a process of elimination. If we can't find a good
reason to believe that someone is dead then we assume they must
be alive.
:param current_date: a date object that is not estimated or modified
(defaults to today)
:param limit: number of years to check beyond death_date
:param max_sib_age_diff: maximum sibling age difference, in years
:param max_age_prob_alive: maximum age of a person, in years
:param avg_generation_gap: average generation gap, in years
"""
# First, get the real database to use all people
# for determining alive status:
birth, death, explain, relative = probably_alive_range(person, db,
max_sib_age_diff, max_age_prob_alive, avg_generation_gap)
if current_date is None:
current_date = Today()
LOG.debug("%s: b.%s, d.%s - %s".format(
" ".join(person.get_primary_name().get_text_data_list()),
birth, death, explain))
if not birth or not death:
# no evidence, must consider alive
return ((True, None, None, _("no evidence"), None) if return_range
else True)
# must have dates from here:
if limit:
death += limit # add these years to death
# Finally, check to see if current_date is between dates
result = (current_date.match(birth, ">=") and
current_date.match(death, "<="))
if return_range:
return (result, birth, death, explain, relative)
else:
return result
[docs]def probably_alive_range(person, db,
max_sib_age_diff=None,
max_age_prob_alive=None,
avg_generation_gap=None):
"""
Computes estimated birth and death dates.
Returns: (birth_date, death_date, explain_text, related_person)
"""
# First, find the real database to use all people
# for determining alive status:
from ..proxy.proxybase import ProxyDbBase
basedb = db
while isinstance(basedb, ProxyDbBase):
basedb = basedb.db
# Now, we create a wrapper for doing work:
pb = ProbablyAlive(basedb, max_sib_age_diff,
max_age_prob_alive, avg_generation_gap)
return pb.probably_alive_range(person)
[docs]def update_constants():
"""
Used to update the constants that are cached in this module.
"""
from ..config import config
global _MAX_AGE_PROB_ALIVE, _MAX_SIB_AGE_DIFF, _AVG_GENERATION_GAP
_MAX_AGE_PROB_ALIVE = config.get('behavior.max-age-prob-alive')
_MAX_SIB_AGE_DIFF = config.get('behavior.max-sib-age-diff')
_AVG_GENERATION_GAP = config.get('behavior.avg-generation-gap')