From ea61a85402dbac8214be96ab93e04c256ac497a7 Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Sun, 2 Dec 2007 18:41:21 +0000 Subject: [PATCH] 2007-12-02 Douglas S.Blank * src/gen/lib/date.py: added new method copy_ymd() * src/plugins/CalculateEstimatedDates.py: new tool based on MenuOptions: calculates estimated dates * src/PluginUtils/_PluginWindows.py: refactoring, cleanup * po/POTFILES.in: renamed PluginStatus.py -> PluginWindows.py svn: r9438 --- ChangeLog | 8 +- src/PluginUtils/_PluginWindows.py | 89 ++++-- src/gen/lib/date.py | 8 + src/plugins/CalculateEstimatedDates.py | 381 +++++++++++++++++++++++++ 4 files changed, 458 insertions(+), 28 deletions(-) create mode 100644 src/plugins/CalculateEstimatedDates.py diff --git a/ChangeLog b/ChangeLog index e9af202cb..cbfd22e0e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2007-12-02 Douglas S.Blank + * src/gen/lib/date.py: added new method copy_ymd() + * src/plugins/CalculateEstimatedDates.py: new tool based on + MenuOptions: calculates estimated dates + * src/PluginUtils/_PluginWindows.py: refactoring, cleanup + 2007-12-01 Douglas S.Blank * src/Editors/_EditFamily.py: fixed issues with latin american surname guessing @@ -36,7 +42,7 @@ Added a change column in repoview, model, database column storage 2007-11-29 Douglas S.Blank - * po//POTFILES.in: renamed PluginStatus.py -> PluginWindows.py + * po/POTFILES.in: renamed PluginStatus.py -> PluginWindows.py 2007-11-29 Douglas S. Blank * src/plugins/ImportCSV.py: add progress meter diff --git a/src/PluginUtils/_PluginWindows.py b/src/PluginUtils/_PluginWindows.py index 14561626b..9fc5ca950 100644 --- a/src/PluginUtils/_PluginWindows.py +++ b/src/PluginUtils/_PluginWindows.py @@ -44,6 +44,7 @@ import ManagedWindow import Errors import _PluginMgr as PluginMgr import _Tool as Tool +from BasicUtils import name_displayer #------------------------------------------------------------------------- # @@ -156,18 +157,16 @@ class PluginTrace(ManagedWindow.ManagedWindow): # Main window for a batch tool # #------------------------------------------------------------------------- -class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): +class ToolManagedWindowBase(ManagedWindow.ManagedWindow): """ Copied from src/ReportBase/_BareReportDialog.py BareReportDialog """ frame_pad = 5 border_pad = 6 HELP_TOPIC = None - def __init__(self, dbstate, uistate, options_class, name, callback=None): + def __init__(self, dbstate, uistate, option_class, name, callback=None): self.dbstate = dbstate self.uistate = uistate - - Tool.BatchTool.__init__(self,dbstate,options_class,name) ManagedWindow.ManagedWindow.__init__(self, uistate, [], self) self.extra_menu = None @@ -177,12 +176,13 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): self.format_menu = None self.style_button = None - window = gtk.Dialog('GRAMPS') + window = gtk.Dialog('Tool') self.set_window(window,None,self.get_title()) self.window.set_has_separator(False) + #self.window.connect('response', self.close) self.cancel = self.window.add_button(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL) - self.cancel.connect('clicked',self.on_cancel) + self.cancel.connect('clicked',self.close) self.ok = self.window.add_button(gtk.STOCK_OK,gtk.RESPONSE_OK) self.ok.connect('clicked',self.on_ok_clicked) @@ -221,14 +221,39 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): # #------------------------------------------------------------------------ def on_cancel(self,*obj): - pass + pass # cancel just closes def on_ok_clicked(self, obj): """The user is satisfied with the dialog choices. Parse all options and close the window.""" - # Save options + self.options.parse_user_options(self) self.options.handler.save_options() + self.pre_run() + self.run() # activate results tab + self.post_run() + + def pre_run(self): + from Utils import ProgressMeter + self.progress = ProgressMeter(_('Tool')) + + def run(self): + raise NotImplementedError, "tool needs to define a run() method" + + def post_run(self): + self.progress.close() + + def on_center_person_change_clicked(self,*obj): + from Selectors import selector_factory + SelectPerson = selector_factory('Person') + sel_person = SelectPerson(self.dbstate,self.uistate,self.track) + new_person = sel_person.run() + if new_person: + self.new_person = new_person + new_name = name_displayer.display(new_person) + if new_name: + self.person_label.set_text( "%s" % new_name ) + self.person_label.set_use_markup(True) #------------------------------------------------------------------------ # @@ -237,7 +262,7 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): #------------------------------------------------------------------------ def get_title(self): """The window title for this dialog""" - return "%s - GRAMPS Book" % "FIXME" + return "Tool" def get_header(self, name): """The header line to put at the top of the contents of the @@ -245,8 +270,8 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): selected person. Most subclasses will customize this to give some indication of what the report will be, i.e. 'Descendant Report for %s'.""" - return _("%(report_name)s for GRAMPS Book") % { - 'report_name' : "FIXME"} + return _("%(tool_name)s for GRAMPS") % { + 'tool_name' : "Tool"} def setup_title(self): """Set up the title bar of the dialog. This function relies @@ -265,29 +290,29 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): title = self.get_header(self.name) label = gtk.Label('%s' % title) label.set_use_markup(True) - self.window.vbox.pack_start(label, True, True, - ToolManagedWindowBatch.border_pad) + self.window.vbox.pack_start(label, True, True, self.border_pad) def setup_center_person(self): """Set up center person labels and change button. Should be overwritten by standalone report dialogs. """ + pass - center_label = gtk.Label("%s" % _("Center Person")) - center_label.set_use_markup(True) - center_label.set_alignment(0.0,0.5) - self.tbl.set_border_width(12) - self.tbl.attach(center_label,0,4,self.col,self.col+1) - self.col += 1 +# center_label = gtk.Label("%s" % _("Center Person")) +# center_label.set_use_markup(True) +# center_label.set_alignment(0.0,0.5) +# self.tbl.set_border_width(12) +# self.tbl.attach(center_label,0,4,self.col,self.col+1) +# self.col += 1 - #name = name_displayer.display(self.person) - #self.person_label = gtk.Label( "%s" % name ) - #self.person_label.set_alignment(0.0,0.5) - #self.tbl.attach(self.person_label,2,3,self.col,self.col+1) +# name = name_displayer.display(self.person) +# self.person_label = gtk.Label( "%s" % name ) +# self.person_label.set_alignment(0.0,0.5) +# self.tbl.attach(self.person_label,2,3,self.col,self.col+1) - #change_button = gtk.Button("%s..." % _('C_hange') ) - #change_button.connect('clicked',self.on_center_person_change_clicked) - #self.tbl.attach(change_button,3,4,self.col,self.col+1,gtk.SHRINK) - #self.col += 1 +# change_button = gtk.Button("%s..." % _('C_hange') ) +# change_button.connect('clicked',self.on_center_person_change_clicked) +# self.tbl.attach(change_button,3,4,self.col,self.col+1,gtk.SHRINK) +# self.col += 1 def add_frame_option(self,frame_name,label_text,widget,tooltip=None): """Similar to add_option this method takes a frame_name, a @@ -344,4 +369,14 @@ class ToolManagedWindowBatch(Tool.BatchTool, ManagedWindow.ManagedWindow): this task.""" self.options.add_user_options(self) +class ToolManagedWindowBatch(Tool.BatchTool, ToolManagedWindowBase): + def __init__(self, dbstate, uistate, options_class, name, callback=None): + Tool.BatchTool.__init__(self,dbstate,options_class,name) + ToolManagedWindowBase.__init__(self, dbstate, uistate, options_class, + name, callback) +class ToolManagedWindow(Tool.Tool, ToolManagedWindowBase): + def __init__(self, dbstate, uistate, options_class, name, callback=None): + Tool.Tool.__init__(self,dbstate,options_class,name) + ToolManagedWindowBase.__init__(self, dbstate, uistate, options_class, + name, callback) diff --git a/src/gen/lib/date.py b/src/gen/lib/date.py index 642375c50..e7480f981 100644 --- a/src/gen/lib/date.py +++ b/src/gen/lib/date.py @@ -563,6 +563,14 @@ class Date: retval.set_yr_mon_day_offset(year, month, day) return retval + def copy_ymd(self, year=0, month=0, day=0): + """ + Returns a Date copy with year, month, and day set + """ + retval = Date(self) + retval.set_yr_mon_day(year, month, day) + return retval + def set_year(self, year): """ Sets the year value diff --git a/src/plugins/CalculateEstimatedDates.py b/src/plugins/CalculateEstimatedDates.py new file mode 100644 index 000000000..6adca22af --- /dev/null +++ b/src/plugins/CalculateEstimatedDates.py @@ -0,0 +1,381 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2007 Donald N. Allingham +# +# 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: $ + +"Calculate Estimated Dates" + +#------------------------------------------------------------------------ +# +# python modules +# +#------------------------------------------------------------------------ +from gettext import gettext as _ +import time + +#------------------------------------------------------------------------ +# +# GRAMPS modules +# +#------------------------------------------------------------------------ +from PluginUtils import Tool, register_tool, PluginWindows, \ + MenuToolOptions, BooleanOption, FilterListOption, StringOption +import gen.lib +import Config + +_MAX_AGE_PROB_ALIVE = Config.get(Config.MAX_AGE_PROB_ALIVE) +_MAX_SIB_AGE_DIFF = Config.get(Config.MAX_SIB_AGE_DIFF) +_MIN_GENERATION_YEARS = Config.get(Config.MIN_GENERATION_YEARS) +_AVG_GENERATION_GAP = Config.get(Config.AVG_GENERATION_GAP) + +class CalcEstDateOptions(MenuToolOptions): + """ Calculate Estimated Date options """ + + def add_menu_options(self, menu): + """ Adds the options """ + category_name = _("Options") + + filter = FilterListOption(_("Filter")) + filter.add_item("person") + filter.set_help(_("Select filter to restrict people")) + menu.add_option(category_name,"filter", filter) + + source_text = StringOption(_("Source text"), _("Calculated Date Estimates")) + source_text.set_help(_("Source to remove and/or add")) + menu.add_option(category_name, "source_text", source_text) + + remove = BooleanOption(_("Remove previously added dates"), True) + remove.set_help(_("Remove")) + menu.add_option(category_name, "remove", remove) + + birth = BooleanOption(_("Add estimated birth dates"), True) + birth.set_help(_("Add")) + menu.add_option(category_name, "add_birth", birth) + + death = BooleanOption(_("Add estimated death dates"), True) + death.set_help(_("Add estimated death dates")) + menu.add_option(category_name, "add_death", death) + +class CalcToolManagedWindow(PluginWindows.ToolManagedWindowBatch): + + def run(self): + self.trans = self.db.transaction_begin("",batch=True) + self.db.disable_signals() + self.filter = self.options.handler.options_dict['filter'] + people = self.filter.apply(self.db, + self.db.get_person_handles(sort_handles=False)) + source_text = self.options.handler.options_dict['source_text'] + add_birth = self.options.handler.options_dict['add_birth'] + add_death = self.options.handler.options_dict['add_death'] + if self.options.handler.options_dict['remove']: + self.progress.set_pass(_("Removing '%s'..." % source_text), len(people)) + for person_handle in people: + self.progress.step() + pupdate = 0 + person = self.db.get_person_from_handle(person_handle) + birth_ref = person.get_birth_ref() + if birth_ref: + birth = self.db.get_event_from_handle(birth_ref.ref) + source_list = birth.get_source_references() + for source_ref in source_list: + #print "birth handle:", source_ref + source = self.db.get_source_from_handle(source_ref.ref) + if source: + #print "birth source:", source, source.get_title() + if source.get_title() == source_text: + person.set_birth_ref(None) + person.remove_handle_references('Event',[birth_ref.ref]) + self.db.remove_event(birth_ref.ref, self.trans) + self.db.commit_source(source, self.trans) + pupdate = 1 + break + death_ref = person.get_death_ref() + if death_ref: + death = self.db.get_event_from_handle(death_ref.ref) + source_list = death.get_source_references() + for source_ref in source_list: + #print "death handle:", source_ref + source = self.db.get_source_from_handle(source_ref.ref) + if source: + #print "death source:", source, source.get_title() + if source.get_title() == source_text: + person.set_death_ref(None) + person.remove_handle_references('Event',[death_ref.ref]) + self.db.remove_event(death_ref.ref, self.trans) + self.db.commit_source(source, self.trans) + pupdate = 1 + break + if pupdate == 1: + self.db.commit_person(person, self.trans) + if add_birth or add_death: + self.progress.set_pass(_('Calculating estimated dates...'), len(people)) + source = self.get_or_create_source(source_text) + for person_handle in people: + self.progress.step() + person = self.db.get_person_from_handle(person_handle) + birth_ref = person.get_birth_ref() + death_ref = person.get_death_ref() + if birth_ref != None or death_ref != None: + date1, date2 = self.calc_estimates(person, birth_ref, death_ref) + if not birth_ref and add_birth and date1: + birth = self.create_event("Estimated birth date", + gen.lib.EventType.BIRTH, date1, source) + event_ref = gen.lib.EventRef() + event_ref.set_reference_handle(birth.get_handle()) + person.set_birth_ref(event_ref) + self.db.commit_person(person, self.trans) + if not death_ref and add_death and date2: + current_date = gen.lib.Date() + current_date.set_yr_mon_day(*time.localtime(time.time())[0:3]) + if current_date.match( date2, "<<"): + # don't add events in the future! + pass + else: + death = self.create_event("Estimated death date", + gen.lib.EventType.DEATH, date2, source) + event_ref = gen.lib.EventRef() + event_ref.set_reference_handle(death.get_handle()) + person.set_death_ref(event_ref) + self.db.commit_person(person, self.trans) + self.db.transaction_commit(self.trans, _("Calculate date estimates")) + self.db.enable_signals() + self.db.request_rebuild() + + def get_or_create_source(self, source_text): + source_list = self.db.get_source_handles() + for source_handle in source_list: + source = self.db.get_source_from_handle(source_handle) + if source.get_title() == source_text: + return source + source = gen.lib.Source() + source.set_title(source_text) + self.db.add_source(source, self.trans) + return source + + def create_event(self, description="Estimated date", + type=None, date=None, source=None): + event = gen.lib.Event() + event.set_description(description) + if type: + event.set_type(gen.lib.EventType(type)) + if date: + date.set_modifier(gen.lib.Date.MOD_ABOUT) + date.set_yr_mon_day(date.get_year(), 0, 0) + event.set_date_object(date) + if source: + sref = gen.lib.SourceRef() + sref.set_reference_handle(source.get_handle()) + event.add_source_reference(sref) + self.db.commit_source(source, self.trans) + self.db.add_event(event, self.trans) + return event + + def calc_estimates(self, person, birth_ref, death_ref): + death_date = None + birth_date = None + # If the recorded death year is before current year then + # things are simple. + if death_ref and death_ref.get_role() == gen.lib.EventRoleType.PRIMARY: + death = self.db.get_event_from_handle(death_ref.ref) + if death.get_date_object().get_start_date() != gen.lib.Date.EMPTY: + death_date = death.get_date_object() + + # Look for Cause Of Death, Burial or Cremation events. + # These are fairly good indications that someone's not alive. + for ev_ref in person.get_primary_event_ref_list(): + ev = self.db.get_event_from_handle(ev_ref.ref) + if ev and int(ev.get_type()) in [gen.lib.EventType.CAUSE_DEATH, + gen.lib.EventType.BURIAL, + gen.lib.EventType.CREMATION]: + if not death_date: + death_date = ev.get_date_object() + + # If they were born within 100 years before current year then + # assume they are alive (we already know they are not dead). + if birth_ref and birth_ref.get_role() == gen.lib.EventRoleType.PRIMARY: + birth = self.db.get_event_from_handle(birth_ref.ref) + if birth.get_date_object().get_start_date() != gen.lib.Date.EMPTY: + if not birth_date: + birth_date = birth.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=-_MAX_AGE_PROB_ALIVE) + + if not death_date and birth_date: + # person died more than MAX after current year + death_date = birth_date.copy_offset_ymd(year=_MAX_AGE_PROB_ALIVE) + + if death_date and birth_date: + return (birth_date, death_date) + + # Neither birth nor death events are available. Try looking + # at siblings. If a sibling was born more than 120 years past, + # or more than 20 future, then probably this person is + # not alive. If the sibling died more than 120 years + # past, or more than 120 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) + 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() != gen.lib.Date.EMPTY: + # if sibling birth date too far away, then not alive: + year = dobj.get_year() + if year != 0: + # sibling birth date + return (gen.lib.Date().copy_ymd(year - _MAX_SIB_AGE_DIFF), + gen.lib.Date().copy_ymd(year + _MAX_SIB_AGE_DIFF + _MAX_AGE_PROB_ALIVE)) + 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() != gen.lib.Date.EMPTY: + # if sibling death date too far away, then not alive: + year = dobj.get_year() + if year != 0: + # sibling death date + return (gen.lib.Date().copy_ymd(year - _MAX_SIB_AGE_DIFF - _MAX_AGE_PROB_ALIVE), + gen.lib.Date().copy_ymd(year + _MAX_SIB_AGE_DIFF)) + + # 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) + 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() != gen.lib.Date.EMPTY: + d = gen.lib.Date(dobj) + val = d.get_start_date() + val = d.get_year() - years + d.set_year(val) + return (d, d.copy_offset_ymd(_MAX_AGE_PROB_ALIVE)) + 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() != gen.lib.Date.EMPTY: + return (dobj.copy_offset_dmy(- _MIN_GENERATION_YEARS), + d.copy_offset_ymd(- _MIN_GENERATION_YEARS + _MAX_AGE_PROB_ALIVE)) + date1, date2 = descendants_too_old (child, years + _MIN_GENERATION_YEARS) + if date1 and date2: + return date1, date2 + return (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 = None, None + try: + date1, date2 = descendants_too_old(person, _MIN_GENERATION_YEARS) + except RuntimeError: + raise Errors.DatabaseError( + _("Database error: %s is defined as his or her own ancestor") % + name_displayer.display(person)) + + def ancestors_too_old(person, year): + family_handle = person.get_main_parents_family_handle() + if family_handle: + family = self.db.get_family_from_handle(family_handle) + 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() == gen.lib.EventRoleType.PRIMARY: + father_birth = self.db.get_event_from_handle( + father_birth_ref.ref) + dobj = father_birth.get_date_object() + if dobj.get_start_date() != gen.lib.Date.EMPTY: + return (dobj.copy_offset_ymd(- year), + dobj.copy_offset_ymd(- year + _MAX_AGE_PROB_ALIVE)) + father_death_ref = father.get_death_ref() + if father_death_ref and father_death_ref.get_role() == gen.lib.EventRoleType.PRIMARY: + father_death = self.db.get_event_from_handle( + father_death_ref.ref) + dobj = father_death.get_date_object() + if dobj.get_start_date() != gen.lib.Date.EMPTY: + return (dobj.copy_offset_ymd(- year - _MAX_AGE_PROB_ALIVE), + dobj.copy_offset_ymd(- year - _MAX_AGE_PROB_ALIVE + _MAX_AGE_PROB_ALIVE)) + date1, date2 = ancestors_too_old (father, year - _AVG_GENERATION_GAP) + if date1 and date2: + return date1, date2 + 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() == gen.lib.EventRoleType.PRIMARY: + mother_birth = self.db.get_event_from_handle(mother_birth_ref.ref) + dobj = mother_birth.get_date_object() + if dobj.get_start_date() != gen.lib.Date.EMPTY: + return (dobj.copy_offset_ymd(- year), + dobj.copy_offset_ymd(- year + _MAX_AGE_PROB_ALIVE)) + mother_death_ref = mother.get_death_ref() + if mother_death_ref and mother_death_ref.get_role() == gen.lib.EventRoleType.PRIMARY: + mother_death = self.db.get_event_from_handle( + mother_death_ref.ref) + dobj = mother_death.get_date_object() + if dobj.get_start_date() != gen.lib.Date.EMPTY: + return (dobj.copy_offset_ymd(- year - _MAX_AGE_PROB_ALIVE), + dobj.copy_offset_ymd(- year - _MAX_AGE_PROB_ALIVE + _MAX_AGE_PROB_ALIVE)) + date1, date2 = ancestors_too_old (mother, year - _AVG_GENERATION_GAP) + if date1 and date2: + return (date1, date2) + return (None, None) + + # If there are ancestors that would be too old in the current year + # then assume our person must be dead too. + date1, date2 = ancestors_too_old (person, - _MIN_GENERATION_YEARS) + if date1 and date2: + return (date1, date2) + + # If we can't find any reason to believe that they are dead we + # must assume they are alive. + return (None, None) + +#------------------------------------------------------------------------- +# +# Register the tool +# +#------------------------------------------------------------------------- +register_tool( + name = 'calculateestimateddates', + category = Tool.TOOL_DBPROC, + tool_class = CalcToolManagedWindow, + options_class = CalcEstDateOptions, + modes = Tool.MODE_GUI, + translated_name = _("Calculate Estimated Dates"), + status = _("Beta"), + author_name = "Douglas S. Blank", + author_email = "dblank@cs.brynmawr.edu", + description=_("Calculates estimated dates for birth and death.") + )