471 lines
19 KiB
Python
471 lines
19 KiB
Python
# encoding:utf-8
|
|
#
|
|
# Gramps - a GTK+/GNOME based genealogy program - Records plugin
|
|
#
|
|
# Copyright (C) 2008-2011 Reinhard Müller
|
|
# Copyright (C) 2010 Jakim Friant
|
|
# Copyright (C) 2013-2015 Paul Franklin
|
|
#
|
|
# 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.
|
|
#
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Standard Python modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
import datetime
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Gramps modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
|
_ = glocale.translation.sgettext
|
|
from gramps.gen.lib import (ChildRefType, Date, Span, Name, StyledText,
|
|
StyledTextTag, StyledTextTagType)
|
|
from gramps.gen.display.name import displayer as name_displayer
|
|
from gramps.gen.utils.alive import probably_alive
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# List of records
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
def _T_(value): # enable deferred translations (see Python docs 22.1.3.4)
|
|
return value
|
|
# _T_ is a gramps-defined keyword -- see po/update_po.py and po/genpot.sh
|
|
|
|
RECORDS = [
|
|
(_T_("Youngest living person"), 'person_youngestliving', True),
|
|
(_T_("Oldest living person"), 'person_oldestliving', True),
|
|
(_T_("Person died at youngest age"), 'person_youngestdied', False),
|
|
(_T_("Person died at oldest age"), 'person_oldestdied', True),
|
|
(_T_("Person married at youngest age"), 'person_youngestmarried', True),
|
|
(_T_("Person married at oldest age"), 'person_oldestmarried', True),
|
|
(_T_("Person divorced at youngest age"), 'person_youngestdivorced', False),
|
|
(_T_("Person divorced at oldest age"), 'person_oldestdivorced', False),
|
|
(_T_("Youngest father"), 'person_youngestfather', True),
|
|
(_T_("Youngest mother"), 'person_youngestmother', True),
|
|
(_T_("Oldest father"), 'person_oldestfather', True),
|
|
(_T_("Oldest mother"), 'person_oldestmother', True),
|
|
(_T_("Couple with most children"), 'family_mostchildren', True),
|
|
(_T_("Living couple married most recently"), 'family_youngestmarried',True),
|
|
(_T_("Living couple married most long ago"), 'family_oldestmarried', True),
|
|
(_T_("Shortest past marriage"), 'family_shortest', False),
|
|
(_T_("Longest past marriage"), 'family_longest', True),
|
|
(_T_("Couple with smallest age difference"), 'family_smallestagediff', True),
|
|
(_T_("Couple with biggest age difference"), 'family_biggestagediff', True)]
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Global functions
|
|
#
|
|
#------------------------------------------------------------------------
|
|
def _good_date(date):
|
|
return (date is not None and date.is_valid())
|
|
|
|
def _find_death_date(db, person):
|
|
death_ref = person.get_death_ref()
|
|
if death_ref:
|
|
death = db.get_event_from_handle(death_ref.ref)
|
|
return death.get_date_object()
|
|
else:
|
|
event_list = person.get_primary_event_ref_list()
|
|
for event_ref in event_list:
|
|
event = db.get_event_from_handle(event_ref.ref)
|
|
if event.get_type().is_death_fallback():
|
|
return event.get_date_object()
|
|
return None
|
|
|
|
def find_records(db, filter, top_size, callname,
|
|
trans_text=glocale.translation.sgettext, name_format=None):
|
|
"""
|
|
@param trans_text: allow deferred translation of strings
|
|
@type trans_text: a GrampsLocale sgettext instance
|
|
trans_text is a defined keyword (see po/update_po.py, po/genpot.sh)
|
|
:param name_format: optional format to control display of person's name
|
|
:type name_format: None or int
|
|
"""
|
|
|
|
today = datetime.date.today()
|
|
today_date = Date(today.year, today.month, today.day)
|
|
|
|
# Person records
|
|
person_youngestliving = []
|
|
person_oldestliving = []
|
|
person_youngestdied = []
|
|
person_oldestdied = []
|
|
person_youngestmarried = []
|
|
person_oldestmarried = []
|
|
person_youngestdivorced = []
|
|
person_oldestdivorced = []
|
|
person_youngestfather = []
|
|
person_youngestmother = []
|
|
person_oldestfather = []
|
|
person_oldestmother = []
|
|
|
|
person_handle_list = db.iter_person_handles()
|
|
|
|
if filter:
|
|
person_handle_list = filter.apply(db, person_handle_list)
|
|
|
|
for person_handle in person_handle_list:
|
|
person = db.get_person_from_handle(person_handle)
|
|
|
|
# FIXME this should check for a "fallback" birth also/instead
|
|
birth_ref = person.get_birth_ref()
|
|
|
|
if not birth_ref:
|
|
# No birth event, so we can't calculate any age.
|
|
continue
|
|
|
|
birth = db.get_event_from_handle(birth_ref.ref)
|
|
birth_date = birth.get_date_object()
|
|
|
|
death_date = _find_death_date(db, person)
|
|
|
|
if not _good_date(birth_date):
|
|
# Birth date unknown or incomplete, so we can't calculate any age.
|
|
continue
|
|
|
|
name = _get_styled_primary_name(person, callname,
|
|
trans_text=trans_text,
|
|
name_format=name_format)
|
|
|
|
if death_date is None:
|
|
if probably_alive(person, db):
|
|
# Still living, look for age records
|
|
_record(person_youngestliving, person_oldestliving,
|
|
today_date - birth_date, name, 'Person', person_handle,
|
|
top_size)
|
|
elif _good_date(death_date):
|
|
# Already died, look for age records
|
|
_record(person_youngestdied, person_oldestdied,
|
|
death_date - birth_date, name, 'Person', person_handle,
|
|
top_size)
|
|
|
|
for family_handle in person.get_family_handle_list():
|
|
family = db.get_family_from_handle(family_handle)
|
|
|
|
marriage_date = None
|
|
divorce_date = None
|
|
for event_ref in family.get_event_ref_list():
|
|
event = db.get_event_from_handle(event_ref.ref)
|
|
if (event.get_type().is_marriage() and
|
|
(event_ref.get_role().is_family() or
|
|
event_ref.get_role().is_primary())):
|
|
marriage_date = event.get_date_object()
|
|
elif (event.get_type().is_divorce() and
|
|
(event_ref.get_role().is_family() or
|
|
event_ref.get_role().is_primary())):
|
|
divorce_date = event.get_date_object()
|
|
|
|
if _good_date(marriage_date):
|
|
_record(person_youngestmarried, person_oldestmarried,
|
|
marriage_date - birth_date,
|
|
name, 'Person', person_handle, top_size)
|
|
|
|
if _good_date(divorce_date):
|
|
_record(person_youngestdivorced, person_oldestdivorced,
|
|
divorce_date - birth_date,
|
|
name, 'Person', person_handle, top_size)
|
|
|
|
for child_ref in family.get_child_ref_list():
|
|
if person.get_gender() == person.MALE:
|
|
relation = child_ref.get_father_relation()
|
|
elif person.get_gender() == person.FEMALE:
|
|
relation = child_ref.get_mother_relation()
|
|
else:
|
|
continue
|
|
if relation != ChildRefType.BIRTH:
|
|
continue
|
|
|
|
child = db.get_person_from_handle(child_ref.ref)
|
|
|
|
# FIXME this should check for a "fallback" birth also/instead
|
|
child_birth_ref = child.get_birth_ref()
|
|
if not child_birth_ref:
|
|
continue
|
|
|
|
child_birth = db.get_event_from_handle(child_birth_ref.ref)
|
|
child_birth_date = child_birth.get_date_object()
|
|
|
|
if not _good_date(child_birth_date):
|
|
continue
|
|
|
|
if person.get_gender() == person.MALE:
|
|
_record(person_youngestfather, person_oldestfather,
|
|
child_birth_date - birth_date,
|
|
name, 'Person', person_handle, top_size)
|
|
elif person.get_gender() == person.FEMALE:
|
|
_record(person_youngestmother, person_oldestmother,
|
|
child_birth_date - birth_date,
|
|
name, 'Person', person_handle, top_size)
|
|
|
|
|
|
# Family records
|
|
family_mostchildren = []
|
|
family_youngestmarried = []
|
|
family_oldestmarried = []
|
|
family_shortest = []
|
|
family_longest = []
|
|
family_smallestagediff = []
|
|
family_biggestagediff = []
|
|
|
|
for family in db.iter_families():
|
|
#family = db.get_family_from_handle(family_handle)
|
|
|
|
father_handle = family.get_father_handle()
|
|
if not father_handle:
|
|
continue
|
|
mother_handle = family.get_mother_handle()
|
|
if not mother_handle:
|
|
continue
|
|
|
|
# Test if either father or mother are in filter
|
|
if filter:
|
|
if not filter.apply(db, [father_handle, mother_handle]):
|
|
continue
|
|
|
|
father = db.get_person_from_handle(father_handle)
|
|
if father is None:
|
|
continue
|
|
mother = db.get_person_from_handle(mother_handle)
|
|
if mother is None:
|
|
continue
|
|
|
|
name = StyledText(trans_text("%(father)s and %(mother)s")) % {
|
|
'father': _get_styled_primary_name(father, callname,
|
|
trans_text=trans_text,
|
|
name_format=name_format),
|
|
'mother': _get_styled_primary_name(mother, callname,
|
|
trans_text=trans_text,
|
|
name_format=name_format)}
|
|
|
|
_record(None, family_mostchildren,
|
|
len(family.get_child_ref_list()),
|
|
name, 'Family', family.handle, top_size)
|
|
|
|
father_birth_ref = father.get_birth_ref()
|
|
if father_birth_ref:
|
|
father_birth_date = db.get_event_from_handle(father_birth_ref.ref).get_date_object()
|
|
else:
|
|
father_birth_date = None
|
|
|
|
mother_birth_ref = mother.get_birth_ref()
|
|
if mother_birth_ref:
|
|
mother_birth_date = db.get_event_from_handle(mother_birth_ref.ref).get_date_object()
|
|
else:
|
|
mother_birth_date = None
|
|
|
|
if _good_date(father_birth_date) and _good_date(mother_birth_date):
|
|
if father_birth_date >> mother_birth_date:
|
|
_record(family_smallestagediff, family_biggestagediff,
|
|
father_birth_date - mother_birth_date,
|
|
name, 'Family', family.handle, top_size)
|
|
elif mother_birth_date >> father_birth_date:
|
|
_record(family_smallestagediff, family_biggestagediff,
|
|
mother_birth_date - father_birth_date,
|
|
name, 'Family', family.handle, top_size)
|
|
|
|
marriage_date = None
|
|
divorce = None
|
|
divorce_date = None
|
|
for event_ref in family.get_event_ref_list():
|
|
event = db.get_event_from_handle(event_ref.ref)
|
|
if (event.get_type().is_marriage() and
|
|
(event_ref.get_role().is_family() or
|
|
event_ref.get_role().is_primary())):
|
|
marriage_date = event.get_date_object()
|
|
if (event and event.get_type().is_divorce() and
|
|
(event_ref.get_role().is_family() or
|
|
event_ref.get_role().is_primary())):
|
|
divorce = event
|
|
divorce_date = event.get_date_object()
|
|
|
|
father_death_date = _find_death_date(db, father)
|
|
mother_death_date = _find_death_date(db, mother)
|
|
|
|
if not _good_date(marriage_date):
|
|
# Not married or marriage date unknown
|
|
continue
|
|
|
|
if divorce is not None and not _good_date(divorce_date):
|
|
# Divorced but date unknown or inexact
|
|
continue
|
|
|
|
if not probably_alive(father, db) and not _good_date(father_death_date):
|
|
# Father died but death date unknown or inexact
|
|
continue
|
|
|
|
if not probably_alive(mother, db) and not _good_date(mother_death_date):
|
|
# Mother died but death date unknown or inexact
|
|
continue
|
|
|
|
if (divorce_date is None
|
|
and father_death_date is None
|
|
and mother_death_date is None):
|
|
# Still married and alive
|
|
if probably_alive(father, db) and probably_alive(mother, db):
|
|
_record(family_youngestmarried, family_oldestmarried,
|
|
today_date - marriage_date,
|
|
name, 'Family', family.handle, top_size)
|
|
elif (_good_date(divorce_date) or
|
|
_good_date(father_death_date) or
|
|
_good_date(mother_death_date)):
|
|
end = None
|
|
if _good_date(father_death_date) and _good_date(mother_death_date):
|
|
end = min(father_death_date, mother_death_date)
|
|
elif _good_date(father_death_date):
|
|
end = father_death_date
|
|
elif _good_date(mother_death_date):
|
|
end = mother_death_date
|
|
if _good_date(divorce_date):
|
|
if end:
|
|
end = min(end, divorce_date)
|
|
else:
|
|
end = divorce_date
|
|
duration = end - marriage_date
|
|
|
|
_record(family_shortest, family_longest,
|
|
duration, name, 'Family', family.handle, top_size)
|
|
#python 3 workaround: assign locals to tmp so we work with runtime version
|
|
tmp = locals()
|
|
return [(trans_text(text), varname, tmp[varname])
|
|
for (text, varname, default) in RECORDS]
|
|
|
|
def _record(lowest, highest, value, text, handle_type, handle, top_size):
|
|
|
|
if value < 0: # ignore erroneous data
|
|
return # (since the data-verification tool already finds it)
|
|
|
|
if isinstance(value, Span):
|
|
low_value = value.minmax[0]
|
|
high_value = value.minmax[1]
|
|
else:
|
|
low_value = value
|
|
high_value = value
|
|
|
|
if lowest is not None:
|
|
lowest.append((high_value, value, text, handle_type, handle))
|
|
lowest.sort(key=lambda a: a[0]) # FIXME: Ist das lambda notwendig?
|
|
for i in range(top_size, len(lowest)):
|
|
if lowest[i-1][0] < lowest[i][0]:
|
|
del lowest[i:]
|
|
break
|
|
|
|
if highest is not None:
|
|
highest.append((low_value, value, text, handle_type, handle))
|
|
highest.sort(reverse=True)
|
|
for i in range(top_size, len(highest)):
|
|
if highest[i-1][0] > highest[i][0]:
|
|
del highest[i:]
|
|
break
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Reusable functions (could be methods of gen.lib.*)
|
|
#
|
|
#------------------------------------------------------------------------
|
|
|
|
CALLNAME_DONTUSE = 0
|
|
CALLNAME_REPLACE = 1
|
|
CALLNAME_UNDERLINE_ADD = 2
|
|
|
|
def _get_styled(name, callname, placeholder=False,
|
|
trans_text=glocale.translation.sgettext, name_format=None):
|
|
"""
|
|
Return a StyledText object with the name formatted according to the
|
|
parameters:
|
|
|
|
@param callname: whether the callname should be used instead of the first
|
|
name (CALLNAME_REPLACE), underlined within the first name
|
|
(CALLNAME_UNDERLINE_ADD) or not used at all (CALLNAME_DONTUSE).
|
|
@param placeholder: whether a series of underscores should be inserted as a
|
|
placeholder if first name or surname are missing.
|
|
@param trans_text: allow deferred translation of strings
|
|
@type trans_text: a GrampsLocale sgettext instance
|
|
trans_text is a defined keyword (see po/update_po.py, po/genpot.sh)
|
|
:param name_format: optional format to control display of person's name
|
|
:type name_format: None or int
|
|
"""
|
|
|
|
# Make a copy of the name object so we don't mess around with the real
|
|
# data.
|
|
n = Name(source=name)
|
|
|
|
# Insert placeholders.
|
|
if placeholder:
|
|
if not n.first_name:
|
|
n.first_name = "____________"
|
|
if not n.surname:
|
|
n.surname = "____________"
|
|
|
|
if n.call:
|
|
if callname == CALLNAME_REPLACE:
|
|
# Replace first name with call name.
|
|
n.first_name = n.call
|
|
elif callname == CALLNAME_UNDERLINE_ADD:
|
|
if n.call not in n.first_name:
|
|
# Add call name to first name.
|
|
# translators: used in French+Russian, ignore otherwise
|
|
n.first_name = trans_text('"%(callname)s" (%(firstname)s)') % {
|
|
'callname': n.call,
|
|
'firstname': n.first_name }
|
|
|
|
real_format = name_displayer.get_default_format()
|
|
if name_format is not None:
|
|
name_displayer.set_default_format(name_format)
|
|
text = name_displayer.display_name(n)
|
|
name_displayer.set_default_format(real_format)
|
|
tags = []
|
|
|
|
if n.call:
|
|
if callname == CALLNAME_UNDERLINE_ADD:
|
|
# "name" in next line is on purpose: only underline the call name
|
|
# if it was a part of the *original* first name
|
|
if n.call in name.first_name:
|
|
# Underline call name
|
|
callpos = text.find(n.call)
|
|
tags = [StyledTextTag(StyledTextTagType.UNDERLINE, True,
|
|
[(callpos, callpos + len(n.call))])]
|
|
|
|
return StyledText(text, tags)
|
|
|
|
def _get_styled_primary_name(person, callname, placeholder=False,
|
|
trans_text=glocale.translation.sgettext, name_format=None):
|
|
"""
|
|
Return a StyledText object with the person's name formatted according to
|
|
the parameters:
|
|
|
|
@param callname: whether the callname should be used instead of the first
|
|
name (CALLNAME_REPLACE), underlined within the first name
|
|
(CALLNAME_UNDERLINE_ADD) or not used at all (CALLNAME_DONTUSE).
|
|
@param placeholder: whether a series of underscores should be inserted as a
|
|
placeholder if first name or surname are missing.
|
|
@param trans_text: allow deferred translation of strings
|
|
@type trans_text: a GrampsLocale sgettext instance
|
|
trans_text is a defined keyword (see po/update_po.py, po/genpot.sh)
|
|
:param name_format: optional format to control display of person's name
|
|
:type name_format: None or int
|
|
"""
|
|
|
|
return _get_styled(person.get_primary_name(), callname,
|
|
trans_text=trans_text,
|
|
placeholder=placeholder, name_format=name_format)
|