# 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("%s" % 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_class): Report.__init__(self, database, options_class) menu = options_class.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)]