gramps/src/plugins/Records.py

648 lines
24 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
#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
#------------------------------------------------------------------------
#
# Standard Python modules
#
#------------------------------------------------------------------------
import datetime
from gen.ggettext import sgettext as _
#------------------------------------------------------------------------
#
# GRAMPS modules
#
#------------------------------------------------------------------------
from gen.lib import ChildRefType, Date, EventType, Name, StyledText, \
StyledTextTag, StyledTextTagType
from gen.plug.docgen import FontStyle, ParagraphStyle, FONT_SANS_SERIF, \
PARA_ALIGN_CENTER
from gen.display.name import displayer as name_displayer
from gen.plug import Gramplet
from gen.plug.menu import (BooleanOption, EnumeratedListOption,
FilterOption, PersonOption, StringOption)
from gen.plug.report import Report
from gen.plug.report import utils as ReportUtils
from gen.plug.report import MenuReportOptions
from Utils import probably_alive
#------------------------------------------------------------------------
#
# Global functions
#
#------------------------------------------------------------------------
def _good_date(date):
if date:
if RecordsReportOptions.REGULAR_DATES_ONLY:
return date.is_regular()
else:
return date.is_valid()
else:
return False
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, callname):
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)
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 = _Person_get_styled_primary_name(person, callname)
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)
elif _good_date(death_date):
# Already died, look for age records
_record(person_youngestdied, person_oldestdied,
death_date - birth_date, name, 'Person', person_handle)
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)
if _good_date(divorce_date):
_record(person_youngestdivorced, person_oldestdivorced,
divorce_date - birth_date,
name, 'Person', person_handle)
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)
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)
elif person.get_gender() == person.FEMALE:
_record(person_youngestmother, person_oldestmother,
child_birth_date - birth_date,
name, 'Person', person_handle)
# Family records
family_mostchildren = []
family_youngestmarried = []
family_oldestmarried = []
family_shortest = []
family_longest = []
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)
mother = db.get_person_from_handle(mother_handle)
name = StyledText(_("%(father)s and %(mother)s")) % {
'father': _Person_get_styled_primary_name(father, callname),
'mother': _Person_get_styled_primary_name(mother, callname)}
_record(None, family_mostchildren,
len(family.get_child_ref_list()),
name, 'Family', family.handle)
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)
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)
return [(text, varname, locals()[varname]) for (text, varname, default) in RECORDS]
def _record(lowest, highest, value, text, handle_type, handle):
if lowest is not None:
lowest.append((value, text, handle_type, handle))
lowest.sort(lambda a,b: cmp(a[0], b[0])) # FIXME: Ist das lambda notwendig?
for i in range(RecordsReportOptions.TOP_SIZE, len(lowest)):
if lowest[i-1][0] < lowest[i][0]:
del lowest[i:]
break
if highest is not None:
highest.append((value, text, handle_type, handle))
highest.sort(reverse=True)
for i in range(RecordsReportOptions.TOP_SIZE, len(highest)):
if highest[i-1][0] > highest[i][0]:
del highest[i:]
break
def _output(value):
return str(value)
#------------------------------------------------------------------------
#
# Reusable functions (could be methods of gen.lib.*)
#
#------------------------------------------------------------------------
_Name_CALLNAME_DONTUSE = 0
_Name_CALLNAME_REPLACE = 1
_Name_CALLNAME_UNDERLINE_ADD = 2
def _Name_get_styled(name, callname, placeholder=False):
"""
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.
"""
# 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 == _Name_CALLNAME_REPLACE:
# Replace first name with call name.
n.first_name = n.call
elif callname == _Name_CALLNAME_UNDERLINE_ADD:
if n.call not in n.first_name:
# Add call name to first name.
n.first_name = "\"%(call)s\" (%(first)s)" % {
'call': n.call,
'first': n.first_name}
text = name_displayer.display_name(n)
tags = []
if n.call:
if callname == _Name_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 _Person_get_styled_primary_name(person, callname, placeholder=False):
"""
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.
"""
return _Name_get_styled(person.get_primary_name(), callname, placeholder)
#------------------------------------------------------------------------
#
# The Gramplet
#
#------------------------------------------------------------------------
class RecordsGramplet(Gramplet):
def init(self):
self.set_use_markup(True)
self.set_tooltip(_("Double-click name for details"))
self.set_text(_("No Family Tree loaded."))
def db_changed(self):
self.dbstate.db.connect('person-rebuild', self.update)
self.dbstate.db.connect('family-rebuild', self.update)
def main(self):
self.set_text(_("Processing...") + "\n")
yield True
records = _find_records(self.dbstate.db, None, _Name_CALLNAME_DONTUSE)
self.set_text("")
for (text, varname, top) in records:
yield True
self.render_text("<b>%s</b>" % text)
last_value = None
rank = 0
for (number, (value, name, handletype, handle)) in enumerate(top):
if value != last_value:
last_value = value
rank = number
self.append_text("\n %s. " % (rank+1))
self.link(unicode(name), handletype, handle)
self.append_text(" (%s)" % _output(value))
self.append_text("\n")
self.append_text("", scroll_to='begin')
yield False
#------------------------------------------------------------------------
#
# The Report
#
#------------------------------------------------------------------------
class RecordsReport(Report):
def __init__(self, database, options, user):
Report.__init__(self, database, options, user)
menu = options.menu
self.filter_option = menu.get_option_by_name('filter')
self.filter = self.filter_option.get_filter()
self.callname = menu.get_option_by_name('callname').get_value()
self.footer = menu.get_option_by_name('footer').get_value()
self.include = {}
for (text, varname, default) in RECORDS:
self.include[varname] = menu.get_option_by_name(varname).get_value()
def write_report(self):
"""
Build the actual report.
"""
records = _find_records(self.database, self.filter, self.callname)
self.doc.start_paragraph('REC-Title')
self.doc.write_text(_("Records"))
self.doc.end_paragraph()
self.doc.start_paragraph('REC-Subtitle')
self.doc.write_text(self.filter.get_name())
self.doc.end_paragraph()
for (text, varname, top) in records:
if not self.include[varname]:
continue
self.doc.start_paragraph('REC-Heading')
self.doc.write_text(text)
self.doc.end_paragraph()
last_value = None
rank = 0
for (number, (value, name, handletype, handle)) in enumerate(top):
if value != last_value:
last_value = value
rank = number
self.doc.start_paragraph('REC-Normal')
self.doc.write_text(_("%(number)s. ") % {'number': rank+1})
self.doc.write_markup(unicode(name), name.get_tags())
self.doc.write_text(_(" (%(value)s)") % {'value': _output(value)})
self.doc.end_paragraph()
self.doc.start_paragraph('REC-Footer')
self.doc.write_text(self.footer)
self.doc.end_paragraph()
#------------------------------------------------------------------------
#
# MenuReportOptions
#
#------------------------------------------------------------------------
class RecordsReportOptions(MenuReportOptions):
"""
Defines options and provides handling interface.
"""
REGULAR_DATES_ONLY = True
TOP_SIZE = 3
def __init__(self, name, dbase):
self.__pid = None
self.__filter = None
self.__db = dbase
MenuReportOptions.__init__(self, name, dbase)
def add_menu_options(self, menu):
category_name = _("Report Options")
self.__filter = FilterOption(_("Filter"), 0)
self.__filter.set_help(
_("Determines what people are included in the report."))
menu.add_option(category_name, "filter", self.__filter)
self.__filter.connect('value-changed', self.__filter_changed)
self.__pid = PersonOption(_("Filter Person"))
self.__pid.set_help(_("The center person for the filter"))
menu.add_option(category_name, "pid", self.__pid)
self.__pid.connect('value-changed', self.__update_filters)
self.__update_filters()
callname = EnumeratedListOption(_("Use call name"), _Name_CALLNAME_DONTUSE)
callname.set_items([
(_Name_CALLNAME_DONTUSE, _("Don't use call name")),
(_Name_CALLNAME_REPLACE, _("Replace first name with call name")),
(_Name_CALLNAME_UNDERLINE_ADD, _("Underline call name in first name / add call name to first name"))])
menu.add_option(category_name, "callname", callname)
footer = StringOption(_("Footer text"), "")
menu.add_option(category_name, "footer", footer)
for (text, varname, default) in RECORDS:
option = BooleanOption(text, default)
if varname.startswith('person'):
category_name = _("Person Records")
elif varname.startswith('family'):
category_name = _("Family Records")
menu.add_option(category_name, varname, option)
def __update_filters(self):
"""
Update the filter list based on the selected person
"""
gid = self.__pid.get_value()
person = self.__db.get_person_from_gramps_id(gid)
filter_list = ReportUtils.get_person_filters(person, False)
self.__filter.set_filters(filter_list)
def __filter_changed(self):
"""
Handle filter change. If the filter is not specific to a person,
disable the person option
"""
filter_value = self.__filter.get_value()
if filter_value in [1, 2, 3, 4]:
# Filters 1, 2, 3 and 4 rely on the center person
self.__pid.set_available(True)
else:
# The rest don't
self.__pid.set_available(False)
def make_default_style(self, default_style):
#Paragraph Styles
font = FontStyle()
font.set_type_face(FONT_SANS_SERIF)
font.set_size(16)
font.set_bold(True)
para = ParagraphStyle()
para.set_font(font)
para.set_alignment(PARA_ALIGN_CENTER)
para.set_description(_("The style used for the report title."))
default_style.add_paragraph_style('REC-Title', para)
font = FontStyle()
font.set_type_face(FONT_SANS_SERIF)
font.set_size(12)
font.set_bold(True)
para = ParagraphStyle()
para.set_font(font)
para.set_alignment(PARA_ALIGN_CENTER)
para.set_bottom_border(True)
para.set_bottom_margin(ReportUtils.pt2cm(8))
para.set_description(_("The style used for the report subtitle."))
default_style.add_paragraph_style('REC-Subtitle', para)
font = FontStyle()
font.set_size(12)
font.set_bold(True)
para = ParagraphStyle()
para.set_font(font)
para.set_top_margin(ReportUtils.pt2cm(6))
para.set_description(_('The style used for headings.'))
default_style.add_paragraph_style('REC-Heading', para)
font = FontStyle()
font.set_size(10)
para = ParagraphStyle()
para.set_font(font)
para.set_left_margin(0.5)
para.set_description(_('The basic style used for the text display.'))
default_style.add_paragraph_style('REC-Normal', para)
font = FontStyle()
font.set_size(8)
para = ParagraphStyle()
para.set_font(font)
para.set_alignment(PARA_ALIGN_CENTER)
para.set_top_border(True)
para.set_top_margin(ReportUtils.pt2cm(8))
para.set_description(_('The style used for the footer.'))
default_style.add_paragraph_style('REC-Footer', para)
#------------------------------------------------------------------------
#
# List of records
#
#------------------------------------------------------------------------
RECORDS = [
(_("Youngest living person"), 'person_youngestliving', True),
(_("Oldest living person"), 'person_oldestliving', True),
(_("Person died at youngest age"), 'person_youngestdied', False),
(_("Person died at oldest age"), 'person_oldestdied', True),
(_("Person married at youngest age"), 'person_youngestmarried', True),
(_("Person married at oldest age"), 'person_oldestmarried', True),
(_("Person divorced at youngest age"), 'person_youngestdivorced', False),
(_("Person divorced at oldest age"), 'person_oldestdivorced', False),
(_("Youngest father"), 'person_youngestfather', True),
(_("Youngest mother"), 'person_youngestmother', True),
(_("Oldest father"), 'person_oldestfather', True),
(_("Oldest mother"), 'person_oldestmother', True),
(_("Couple with most children"), 'family_mostchildren', True),
(_("Living couple married most recently"), 'family_youngestmarried', True),
(_("Living couple married most long ago"), 'family_oldestmarried', True),
(_("Shortest past marriage"), 'family_shortest', False),
(_("Longest past marriage"), 'family_longest', True)]