Files
gramps/gramps/plugins/lib/librecords.py
2016-04-25 21:31:08 -07:00

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)