1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2007  Donald N. Allingham
5# Copyright (C) 2008       Brian G. Matherly
6# Copyright (C) 2010       Jakim Friant
7# Copyright (C) 2011       Paul Franklin
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22#
23
24"""
25A plugin to verify the data against user-adjusted tests.
26This is the research tool, not the low-level data ingerity check.
27
28Note that this tool has an old heritage (20-Oct-2002 at least) and
29so there are vestages of earlier ways of doing things which have not
30been converted to a more-modern way.  For instance the way the tool
31options are defined (and read in) is not done the way it would be now.
32"""
33
34# pylint: disable=not-callable
35# pylint: disable=no-self-use
36# pylint: disable=undefined-variable
37
38#------------------------------------------------------------------------
39#
40# standard python modules
41#
42#------------------------------------------------------------------------
43
44import os
45import pickle
46from hashlib import md5
47
48#------------------------------------------------------------------------
49#
50# GNOME/GTK modules
51#
52#------------------------------------------------------------------------
53from gi.repository import Gdk
54from gi.repository import Gtk
55from gi.repository import GObject
56
57#------------------------------------------------------------------------
58#
59# Gramps modules
60#
61#------------------------------------------------------------------------
62from gramps.gen.const import GRAMPS_LOCALE as glocale
63_ = glocale.translation.sgettext
64from gramps.gen.errors import WindowActiveError
65from gramps.gen.const import URL_MANUAL_PAGE, VERSION_DIR
66from gramps.gen.lib import (ChildRefType, EventRoleType, EventType,
67                            FamilyRelType, NameType, Person)
68from gramps.gen.lib.date import Today
69from gramps.gui.editors import EditPerson, EditFamily
70from gramps.gen.utils.db import family_name
71from gramps.gui.display import display_help
72from gramps.gui.managedwindow import ManagedWindow
73from gramps.gen.updatecallback import UpdateCallback
74from gramps.gui.plug import tool
75from gramps.gui.glade import Glade
76
77#-------------------------------------------------------------------------
78#
79# Constants
80#
81#-------------------------------------------------------------------------
82WIKI_HELP_PAGE = '%s_-_Tools' % URL_MANUAL_PAGE
83WIKI_HELP_SEC = _('manual|Verify_the_Data')
84
85#-------------------------------------------------------------------------
86#
87# temp storage and related functions
88#
89#-------------------------------------------------------------------------
90_person_cache = {}
91_family_cache = {}
92_event_cache = {}
93_today = Today().get_sort_value()
94
95def find_event(db, handle):
96    """ find an event, given a handle """
97    if handle in _event_cache:
98        obj = _event_cache[handle]
99    else:
100        obj = db.get_event_from_handle(handle)
101        _event_cache[handle] = obj
102    return obj
103
104def find_person(db, handle):
105    """ find a person, given a handle """
106    if handle in _person_cache:
107        obj = _person_cache[handle]
108    else:
109        obj = db.get_person_from_handle(handle)
110        _person_cache[handle] = obj
111    return obj
112
113def find_family(db, handle):
114    """ find a family, given a handle """
115    if handle in _family_cache:
116        obj = _family_cache[handle]
117    else:
118        obj = db.get_family_from_handle(handle)
119        _family_cache[handle] = obj
120    return obj
121
122def clear_cache():
123    """ clear the cache """
124    _person_cache.clear()
125    _family_cache.clear()
126    _event_cache.clear()
127
128#-------------------------------------------------------------------------
129#
130# helper functions
131#
132#-------------------------------------------------------------------------
133def get_date_from_event_handle(db, event_handle, estimate=False):
134    """ get a date from an event handle """
135    if not event_handle:
136        return 0
137    event = find_event(db, event_handle)
138    if event:
139        date_obj = event.get_date_object()
140        if (not estimate
141                and (date_obj.get_day() == 0 or date_obj.get_month() == 0)):
142            return 0
143        return date_obj.get_sort_value()
144    else:
145        return 0
146
147def get_date_from_event_type(db, person, event_type, estimate=False):
148    """ get a date from a person's specific event type """
149    if not person:
150        return 0
151    for event_ref in person.get_event_ref_list():
152        event = find_event(db, event_ref.ref)
153        if event:
154            if (event_ref.get_role() != EventRoleType.PRIMARY
155                    and event.get_type() == EventType.BURIAL):
156                continue
157            if event.get_type() == event_type:
158                date_obj = event.get_date_object()
159                if (not estimate
160                        and (date_obj.get_day() == 0
161                             or date_obj.get_month() == 0)):
162                    return 0
163                return date_obj.get_sort_value()
164    return 0
165
166def get_bapt_date(db, person, estimate=False):
167    """ get a person's baptism date """
168    return get_date_from_event_type(db, person,
169                                    EventType.BAPTISM, estimate)
170
171def get_bury_date(db, person, estimate=False):
172    """ get a person's burial date """
173    # check role on burial event
174    for event_ref in person.get_event_ref_list():
175        event = find_event(db, event_ref.ref)
176        if (event
177                and event.get_type() == EventType.BURIAL
178                and event_ref.get_role() == EventRoleType.PRIMARY):
179            return get_date_from_event_type(db, person,
180                                            EventType.BURIAL, estimate)
181
182def get_birth_date(db, person, estimate=False):
183    """ get a person's birth date (or baptism date if 'estimated') """
184    if not person:
185        return 0
186    birth_ref = person.get_birth_ref()
187    if not birth_ref:
188        ret = 0
189    else:
190        ret = get_date_from_event_handle(db, birth_ref.ref, estimate)
191    if estimate and (ret == 0):
192        ret = get_bapt_date(db, person, estimate)
193        ret = 0 if ret is None else ret
194    return ret
195
196def get_death(db, person):
197    """
198    boolean whether there is a death event or not
199    (if a user claims a person is dead, we will believe it even with no date)
200    """
201    if not person:
202        return False
203    death_ref = person.get_death_ref()
204    if death_ref:
205        return True
206    else:
207        return False
208
209def get_death_date(db, person, estimate=False):
210    """ get a person's death date (or burial date if 'estimated') """
211    if not person:
212        return 0
213    death_ref = person.get_death_ref()
214    if not death_ref:
215        ret = 0
216    else:
217        ret = get_date_from_event_handle(db, death_ref.ref, estimate)
218    if estimate and (ret == 0):
219        ret = get_bury_date(db, person, estimate)
220        ret = 0 if ret is None else ret
221    return ret
222
223def get_age_at_death(db, person, estimate):
224    """ get a person's age at death """
225    birth_date = get_birth_date(db, person, estimate)
226    death_date = get_death_date(db, person, estimate)
227    if (birth_date > 0) and (death_date > 0):
228        return death_date - birth_date
229    return 0
230
231def get_father(db, family):
232    """ get a family's father """
233    if not family:
234        return None
235    father_handle = family.get_father_handle()
236    if father_handle:
237        return find_person(db, father_handle)
238    return None
239
240def get_mother(db, family):
241    """ get a family's mother """
242    if not family:
243        return None
244    mother_handle = family.get_mother_handle()
245    if mother_handle:
246        return find_person(db, mother_handle)
247    return None
248
249def get_child_birth_dates(db, family, estimate):
250    """ get a family's children's birth dates """
251    dates = []
252    for child_ref in family.get_child_ref_list():
253        child = find_person(db, child_ref.ref)
254        child_birth_date = get_birth_date(db, child, estimate)
255        if child_birth_date > 0:
256            dates.append(child_birth_date)
257    return dates
258
259def get_n_children(db, person):
260    """ get the number of a family's children """
261    number = 0
262    for family_handle in person.get_family_handle_list():
263        family = find_family(db, family_handle)
264        if family:
265            number += len(family.get_child_ref_list())
266    return number
267
268def get_marriage_date(db, family):
269    """ get a family's marriage date """
270    if not family:
271        return 0
272    for event_ref in family.get_event_ref_list():
273        event = find_event(db, event_ref.ref)
274        if (event.get_type() == EventType.MARRIAGE
275                and (event_ref.get_role() == EventRoleType.FAMILY
276                     or event_ref.get_role() == EventRoleType.PRIMARY)):
277            date_obj = event.get_date_object()
278            return date_obj.get_sort_value()
279    return 0
280
281#-------------------------------------------------------------------------
282#
283# Actual tool
284#
285#-------------------------------------------------------------------------
286class Verify(tool.Tool, ManagedWindow, UpdateCallback):
287    """
288    A plugin to verify the data against user-adjusted tests.
289    This is the research tool, not the low-level data ingerity check.
290    """
291
292    def __init__(self, dbstate, user, options_class, name, callback=None):
293        """ initialize things """
294        uistate = user.uistate
295        self.label = _('Data Verify tool')
296        self.v_r = None
297        tool.Tool.__init__(self, dbstate, options_class, name)
298        ManagedWindow.__init__(self, uistate, [], self.__class__)
299        if uistate:
300            UpdateCallback.__init__(self, self.uistate.pulse_progressbar)
301
302        self.dbstate = dbstate
303        if uistate:
304            self.init_gui()
305        else:
306            self.add_results = self.add_results_cli
307            self.run_the_tool(cli=True)
308
309    def add_results_cli(self, results):
310        """ print data for the user, no GUI """
311        (msg, gramps_id, name, the_type, rule_id, severity, handle) = results
312        severity_str = 'S'
313        if severity == Rule.WARNING:
314            severity_str = 'W'
315        elif severity == Rule.ERROR:
316            severity_str = 'E'
317        # translators: needed for French+Arabic, ignore otherwise
318        print(_("%(severity)s: %(msg)s, %(type)s: %(gid)s, %(name)s"
319               ) % {'severity' : severity_str, 'msg' : msg, 'type' : the_type,
320                    'gid' : gramps_id, 'name' : name})
321
322    def init_gui(self):
323        """ Draw dialog and make it handle everything """
324        self.v_r = None
325        self.top = Glade()
326        self.top.connect_signals({
327            "destroy_passed_object" : self.close,
328            "on_help_clicked"       : self.on_help_clicked,
329            "on_verify_ok_clicked"  : self.on_apply_clicked,
330            "on_delete_event"       : self.close,
331        })
332
333        window = self.top.toplevel
334        self.set_window(window, self.top.get_object('title'), self.label)
335        self.setup_configs('interface.verify', 650, 400)
336
337        o_dict = self.options.handler.options_dict
338        for option in o_dict:
339            if option in ['estimate_age', 'invdate']:
340                self.top.get_object(option).set_active(o_dict[option])
341            else:
342                self.top.get_object(option).set_value(o_dict[option])
343        self.show()
344
345    def build_menu_names(self, obj):
346        """ build the menu names """
347        return (_("Tool settings"), self.label)
348
349    def on_help_clicked(self, obj):
350        """ Display the relevant portion of Gramps manual """
351        display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
352
353    def on_apply_clicked(self, obj):
354        """ event handler for user clicking the OK button: start things """
355        run_button = self.top.get_object('button4')
356        close_button = self.top.get_object('button5')
357        run_button.set_sensitive(False)
358        close_button.set_sensitive(False)
359        o_dict = self.options.handler.options_dict
360        for option in o_dict:
361            if option in ['estimate_age', 'invdate']:
362                o_dict[option] = self.top.get_object(option).get_active()
363            else:
364                o_dict[option] = self.top.get_object(option).get_value_as_int()
365
366        try:
367            self.v_r = VerifyResults(self.dbstate, self.uistate, self.track,
368                                     self.top, self.close)
369            self.add_results = self.v_r.add_results
370            self.v_r.load_ignored(self.db.full_name)
371        except WindowActiveError:
372            pass
373        except AttributeError: # VerifyResults.load_ignored was not run
374            self.v_r.ignores = {}
375
376        self.uistate.set_busy_cursor(True)
377        self.uistate.progress.show()
378        busy_cursor = Gdk.Cursor.new_for_display(Gdk.Display.get_default(),
379                                                 Gdk.CursorType.WATCH)
380        self.window.get_window().set_cursor(busy_cursor)
381        try:
382            self.v_r.window.get_window().set_cursor(busy_cursor)
383        except AttributeError:
384            pass
385
386        self.run_the_tool(cli=False)
387
388        self.uistate.progress.hide()
389        self.uistate.set_busy_cursor(False)
390        try:
391            self.window.get_window().set_cursor(None)
392            self.v_r.window.get_window().set_cursor(None)
393        except AttributeError:
394            pass
395        run_button.set_sensitive(True)
396        close_button.set_sensitive(True)
397        self.reset()
398
399        # Save options
400        self.options.handler.save_options()
401
402    def run_the_tool(self, cli=False):
403        """ run the tool """
404
405        person_handles = self.db.iter_person_handles()
406
407        for option, value in self.options.handler.options_dict.items():
408            exec('%s = %s' % (option, value), globals())
409            # TODO my pylint doesn't seem to understand these variables really
410            # are defined here, so I have disabled the undefined-variable error
411
412        if self.v_r:
413            self.v_r.real_model.clear()
414
415        self.set_total(self.db.get_number_of_people() +
416                       self.db.get_number_of_families())
417
418        for person_handle in person_handles:
419            person = find_person(self.db, person_handle)
420
421            rule_list = [
422                BirthAfterBapt(self.db, person),
423                DeathBeforeBapt(self.db, person),
424                BirthAfterBury(self.db, person),
425                DeathAfterBury(self.db, person),
426                BirthAfterDeath(self.db, person),
427                BaptAfterBury(self.db, person),
428                OldAge(self.db, person, oldage, estimate_age),
429                OldAgeButNoDeath(self.db, person, oldage, estimate_age),
430                UnknownGender(self.db, person),
431                MultipleParents(self.db, person),
432                MarriedOften(self.db, person, wedder),
433                OldUnmarried(self.db, person, oldunm, estimate_age),
434                TooManyChildren(self.db, person, mxchilddad, mxchildmom),
435                Disconnected(self.db, person),
436                InvalidBirthDate(self.db, person, invdate),
437                InvalidDeathDate(self.db, person, invdate),
438                BirthEqualsDeath(self.db, person),
439                BirthEqualsMarriage(self.db, person),
440                DeathEqualsMarriage(self.db, person),
441                ]
442
443            for rule in rule_list:
444                if rule.broken():
445                    self.add_results(rule.report_itself())
446
447            clear_cache()
448            if not cli:
449                self.update()
450
451        # Family-based rules
452        for family_handle in self.db.iter_family_handles():
453            family = find_family(self.db, family_handle)
454
455            rule_list = [
456                SameSexFamily(self.db, family),
457                FemaleHusband(self.db, family),
458                MaleWife(self.db, family),
459                SameSurnameFamily(self.db, family),
460                LargeAgeGapFamily(self.db, family, hwdif, estimate_age),
461                MarriageBeforeBirth(self.db, family, estimate_age),
462                MarriageAfterDeath(self.db, family, estimate_age),
463                EarlyMarriage(self.db, family, yngmar, estimate_age),
464                LateMarriage(self.db, family, oldmar, estimate_age),
465                OldParent(self.db, family, oldmom, olddad, estimate_age),
466                YoungParent(self.db, family, yngmom, yngdad, estimate_age),
467                UnbornParent(self.db, family, estimate_age),
468                DeadParent(self.db, family, estimate_age),
469                LargeChildrenSpan(self.db, family, cbspan, estimate_age),
470                LargeChildrenAgeDiff(self.db, family, cspace, estimate_age),
471                MarriedRelation(self.db, family),
472                ]
473
474            for rule in rule_list:
475                if rule.broken():
476                    self.add_results(rule.report_itself())
477
478            clear_cache()
479            if not cli:
480                self.update()
481
482#-------------------------------------------------------------------------
483#
484# Display the results
485#
486#-------------------------------------------------------------------------
487class VerifyResults(ManagedWindow):
488    """ GUI class to show the results in another dialog """
489    IGNORE_COL = 0
490    WARNING_COL = 1
491    OBJ_ID_COL = 2
492    OBJ_NAME_COL = 3
493    OBJ_TYPE_COL = 4
494    RULE_ID_COL = 5
495    OBJ_HANDLE_COL = 6
496    FG_COLOR_COL = 7
497    TRUE_COL = 8
498    SHOW_COL = 9
499
500    def __init__(self, dbstate, uistate, track, glade, closeall):
501        """ initialize things """
502        self.title = _('Data Verification Results')
503
504        ManagedWindow.__init__(self, uistate, track, self.__class__)
505
506        self.dbstate = dbstate
507        self.closeall = closeall
508        self._set_filename()
509        self.top = glade
510        window = self.top.get_object("verify_result")
511        self.set_window(window, self.top.get_object('title2'), self.title)
512        self.setup_configs('interface.verifyresults', 500, 300)
513        window.connect("close", self.close)
514        close_btn = self.top.get_object("closebutton1")
515        close_btn.connect("clicked", self.close)
516
517        self.warn_tree = self.top.get_object('warn_tree')
518        self.warn_tree.connect('button_press_event', self.double_click)
519
520        self.selection = self.warn_tree.get_selection()
521
522        self.hide_button = self.top.get_object('hide_button')
523        self.hide_button.connect('toggled', self.hide_toggled)
524
525        self.mark_button = self.top.get_object('mark_all')
526        self.mark_button.connect('clicked', self.mark_clicked)
527
528        self.unmark_button = self.top.get_object('unmark_all')
529        self.unmark_button.connect('clicked', self.unmark_clicked)
530
531        self.invert_button = self.top.get_object('invert_all')
532        self.invert_button.connect('clicked', self.invert_clicked)
533
534        self.real_model = Gtk.ListStore(GObject.TYPE_BOOLEAN,
535                                        GObject.TYPE_STRING,
536                                        GObject.TYPE_STRING,
537                                        GObject.TYPE_STRING,
538                                        GObject.TYPE_STRING, object,
539                                        GObject.TYPE_STRING,
540                                        GObject.TYPE_STRING,
541                                        GObject.TYPE_BOOLEAN,
542                                        GObject.TYPE_BOOLEAN)
543        self.filt_model = self.real_model.filter_new()
544        self.filt_model.set_visible_column(VerifyResults.TRUE_COL)
545        if hasattr(self.filt_model, "sort_new_with_model"):
546            self.sort_model = self.filt_model.sort_new_with_model()
547        else:
548            self.sort_model = Gtk.TreeModelSort.new_with_model(self.filt_model)
549        self.warn_tree.set_model(self.sort_model)
550
551        self.renderer = Gtk.CellRendererText()
552        self.img_renderer = Gtk.CellRendererPixbuf()
553        self.bool_renderer = Gtk.CellRendererToggle()
554        self.bool_renderer.connect('toggled', self.selection_toggled)
555
556        # Add ignore column
557        ignore_column = Gtk.TreeViewColumn(_('Mark'), self.bool_renderer,
558                                           active=VerifyResults.IGNORE_COL)
559        ignore_column.set_sort_column_id(VerifyResults.IGNORE_COL)
560        self.warn_tree.append_column(ignore_column)
561
562        # Add image column
563        img_column = Gtk.TreeViewColumn(None, self.img_renderer)
564        img_column.set_cell_data_func(self.img_renderer, self.get_image)
565        self.warn_tree.append_column(img_column)
566
567        # Add column with the warning text
568        warn_column = Gtk.TreeViewColumn(_('Warning'), self.renderer,
569                                         text=VerifyResults.WARNING_COL,
570                                         foreground=VerifyResults.FG_COLOR_COL)
571        warn_column.set_sort_column_id(VerifyResults.WARNING_COL)
572        self.warn_tree.append_column(warn_column)
573
574        # Add column with object gramps_id
575        id_column = Gtk.TreeViewColumn(_('ID'), self.renderer,
576                                       text=VerifyResults.OBJ_ID_COL,
577                                       foreground=VerifyResults.FG_COLOR_COL)
578        id_column.set_sort_column_id(VerifyResults.OBJ_ID_COL)
579        self.warn_tree.append_column(id_column)
580
581        # Add column with object name
582        name_column = Gtk.TreeViewColumn(_('Name'), self.renderer,
583                                         text=VerifyResults.OBJ_NAME_COL,
584                                         foreground=VerifyResults.FG_COLOR_COL)
585        name_column.set_sort_column_id(VerifyResults.OBJ_NAME_COL)
586        self.warn_tree.append_column(name_column)
587
588        self.show()
589        self.window_shown = False
590
591    def _set_filename(self):
592        """ set the file where people who will be ignored will be kept """
593        db_filename = self.dbstate.db.get_save_path()
594        if isinstance(db_filename, str):
595            db_filename = db_filename.encode('utf-8')
596        md5sum = md5(db_filename)
597        self.ignores_filename = os.path.join(
598            VERSION_DIR, md5sum.hexdigest() + os.path.extsep + 'vfm')
599
600    def load_ignored(self, db_filename):
601        """ get ready to load the file with the previously-ignored people """
602        ## a new Gramps major version means recreating the .vfm file.
603        ## User can copy over old one, with name of new one, but no guarantee
604        ## that will work.
605        if not self._load_ignored(self.ignores_filename):
606            self.ignores = {}
607
608    def _load_ignored(self, filename):
609        """ load the file with the people who were previously ignored """
610        try:
611            try:
612                file = open(filename, 'rb')
613            except IOError:
614                return False
615            self.ignores = pickle.load(file)
616            file.close()
617            return True
618        except (IOError, EOFError):
619            file.close()
620            return False
621
622    def save_ignored(self, new_ignores):
623        """ get ready to save the file with the ignored people """
624        self.ignores = new_ignores
625        self._save_ignored(self.ignores_filename)
626
627    def _save_ignored(self, filename):
628        """ save the file with the people the user wants to ignore """
629        try:
630            with open(filename, 'wb') as file:
631                pickle.dump(self.ignores, file, 1)
632            return True
633        except IOError:
634            return False
635
636    def get_marking(self, handle, rule_id):
637        if handle in self.ignores:
638            return rule_id in self.ignores[handle]
639        else:
640            return False
641
642    def get_new_marking(self):
643        new_ignores = {}
644        for row_num in range(len(self.real_model)):
645            path = (row_num,)
646            row = self.real_model[path]
647            ignore = row[VerifyResults.IGNORE_COL]
648            if ignore:
649                handle = row[VerifyResults.OBJ_HANDLE_COL]
650                rule_id = row[VerifyResults.RULE_ID_COL]
651                if handle not in new_ignores:
652                    new_ignores[handle] = set()
653                new_ignores[handle].add(rule_id)
654        return new_ignores
655
656    def close(self, *obj):
657        """ close the dialog and write out the file """
658        new_ignores = self.get_new_marking()
659        self.save_ignored(new_ignores)
660
661        ManagedWindow.close(self, *obj)
662        self.closeall()
663
664    def hide_toggled(self, button):
665        self.filt_model = self.real_model.filter_new()
666        if button.get_active():
667            button.set_label(_("_Show all"))
668            self.filt_model.set_visible_column(VerifyResults.SHOW_COL)
669        else:
670            button.set_label(_("_Hide marked"))
671            self.filt_model.set_visible_column(VerifyResults.TRUE_COL)
672        if hasattr(self.filt_model, "sort_new_with_model"):
673            self.sort_model = self.filt_model.sort_new_with_model()
674        else:
675            self.sort_model = Gtk.TreeModelSort.new_with_model(
676                self.filt_model)
677        self.warn_tree.set_model(self.sort_model)
678
679    def selection_toggled(self, cell, path_string):
680        sort_path = tuple(map(int, path_string.split(':')))
681        filt_path = self.sort_model.convert_path_to_child_path(
682            Gtk.TreePath(sort_path))
683        real_path = self.filt_model.convert_path_to_child_path(filt_path)
684        row = self.real_model[real_path]
685        row[VerifyResults.IGNORE_COL] = not row[VerifyResults.IGNORE_COL]
686        row[VerifyResults.SHOW_COL] = not row[VerifyResults.IGNORE_COL]
687        self.real_model.row_changed(real_path, row.iter)
688
689    def mark_clicked(self, mark_button):
690        for row_num in range(len(self.real_model)):
691            path = (row_num,)
692            row = self.real_model[path]
693            row[VerifyResults.IGNORE_COL] = True
694            row[VerifyResults.SHOW_COL] = False
695        self.filt_model.refilter()
696
697    def unmark_clicked(self, unmark_button):
698        for row_num in range(len(self.real_model)):
699            path = (row_num,)
700            row = self.real_model[path]
701            row[VerifyResults.IGNORE_COL] = False
702            row[VerifyResults.SHOW_COL] = True
703        self.filt_model.refilter()
704
705    def invert_clicked(self, invert_button):
706        for row_num in range(len(self.real_model)):
707            path = (row_num,)
708            row = self.real_model[path]
709            row[VerifyResults.IGNORE_COL] = not row[VerifyResults.IGNORE_COL]
710            row[VerifyResults.SHOW_COL] = not row[VerifyResults.SHOW_COL]
711        self.filt_model.refilter()
712
713    def double_click(self, obj, event):
714        """ the user wants to edit the selected person or family """
715        if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS
716                and event.button == 1):
717            (model, node) = self.selection.get_selected()
718            if not node:
719                return
720            sort_path = self.sort_model.get_path(node)
721            filt_path = self.sort_model.convert_path_to_child_path(sort_path)
722            real_path = self.filt_model.convert_path_to_child_path(filt_path)
723            row = self.real_model[real_path]
724            the_type = row[VerifyResults.OBJ_TYPE_COL]
725            handle = row[VerifyResults.OBJ_HANDLE_COL]
726            if the_type == 'Person':
727                try:
728                    person = self.dbstate.db.get_person_from_handle(handle)
729                    EditPerson(self.dbstate, self.uistate, self.track, person)
730                except WindowActiveError:
731                    pass
732            elif the_type == 'Family':
733                try:
734                    family = self.dbstate.db.get_family_from_handle(handle)
735                    EditFamily(self.dbstate, self.uistate, self.track, family)
736                except WindowActiveError:
737                    pass
738
739    def get_image(self, column, cell, model, iter_, user_data=None):
740        """ flag whether each line is a person or family """
741        the_type = model.get_value(iter_, VerifyResults.OBJ_TYPE_COL)
742        if the_type == 'Person':
743            cell.set_property('icon-name', 'gramps-person')
744        elif the_type == 'Family':
745            cell.set_property('icon-name', 'gramps-family')
746
747    def add_results(self, results):
748        (msg, gramps_id, name, the_type, rule_id, severity, handle) = results
749        ignore = self.get_marking(handle, rule_id)
750        if severity == Rule.ERROR:
751            line_color = 'red'
752        else:
753            line_color = None
754        self.real_model.append(row=[ignore, msg, gramps_id, name,
755                                    the_type, rule_id, handle, line_color,
756                                    True, not ignore])
757
758        if not self.window_shown:
759            self.window.show()
760            self.window_shown = True
761
762    def build_menu_names(self, obj):
763        """ build the menu names """
764        return (self.title, self.title)
765
766#------------------------------------------------------------------------
767#
768#
769#
770#------------------------------------------------------------------------
771class VerifyOptions(tool.ToolOptions):
772    """
773    Defines options and provides handling interface.
774    """
775
776    def __init__(self, name, person_id=None):
777        """ initialize the options """
778        tool.ToolOptions.__init__(self, name, person_id)
779
780        # Options specific for this report
781        self.options_dict = {
782            'oldage'       : 90,
783            'hwdif'        : 30,
784            'cspace'       : 8,
785            'cbspan'       : 25,
786            'yngmar'       : 17,
787            'oldmar'       : 50,
788            'oldmom'       : 48,
789            'yngmom'       : 17,
790            'yngdad'       : 18,
791            'olddad'       : 65,
792            'wedder'       : 3,
793            'mxchildmom'   : 12,
794            'mxchilddad'   : 15,
795            'lngwdw'       : 30,
796            'oldunm'       : 99,
797            'estimate_age' : 0,
798            'invdate'      : 1,
799        }
800        # TODO these strings are defined in the glade file (more or less, since
801        # those have accelerators), and so are not translated here, but that
802        # means that a CLI user who runs gramps in a non-English language and
803        # says (for instance) "show=oldage" will see "Maximum age" in English
804        # (but I think such a CLI use is very unlikely and so is low priority,
805        # especially since the tool's normal CLI output will be translated)
806        self.options_help = {
807            'oldage'       : ("=num", "Maximum age", "Age in years"),
808            'hwdif'        : ("=num", "Maximum husband-wife age difference",
809                              "Age difference in years"),
810            'cspace'       : ("=num",
811                              "Maximum number of years between children",
812                              "Number of years"),
813            'cbspan'       : ("=num",
814                              "Maximum span of years for all children",
815                              "Span in years"),
816            'yngmar'       : ("=num", "Minimum age to marry", "Age in years"),
817            'oldmar'       : ("=num", "Maximum age to marry", "Age in years"),
818            'oldmom'       : ("=num", "Maximum age to bear a child",
819                              "Age in years"),
820            'yngmom'       : ("=num", "Minimum age to bear a child",
821                              "Age in years"),
822            'yngdad'       : ("=num", "Minimum age to father a child",
823                              "Age in years"),
824            'olddad'       : ("=num", "Maximum age to father a child",
825                              "Age in years"),
826            'wedder'       : ("=num", "Maximum number of spouses for a person",
827                              "Number of spouses"),
828            'mxchildmom'   : ("=num", "Maximum number of children for a woman",
829                              "Number of children"),
830            'mxchilddad'   : ("=num", "Maximum  number of children for a man",
831                              "Number of chidlren"),
832            'lngwdw'       : ("=num", "Maximum number of consecutive years "
833                              "of widowhood before next marriage",
834                              "Number of years"),
835            'oldunm'       : ("=num", "Maximum age for an unmarried person"
836                              "Number of years"),
837            'estimate_age' : ("=0/1",
838                              "Whether to estimate missing or inexact dates",
839                              ["Do not estimate", "Estimate dates"],
840                              True),
841            'invdate'      : ("=0/1", "Whether to check for invalid dates"
842                              "Do not identify invalid dates",
843                              "Identify invalid dates", True),
844        }
845
846#-------------------------------------------------------------------------
847#
848# Base classes for different tests -- the rules
849#
850#-------------------------------------------------------------------------
851class Rule:
852    """
853    Basic class for use in this tool.
854
855    Other rules must inherit from this.
856    """
857    ID = 0
858    TYPE = ''
859
860    ERROR = 1
861    WARNING = 2
862
863    SEVERITY = WARNING
864
865    def __init__(self, db, obj):
866        """ initialize the rule """
867        self.db = db
868        self.obj = obj
869
870    def broken(self):
871        """
872        Return boolean indicating whether this rule is violated.
873        """
874        return False
875
876    def get_message(self):
877        """ return the rule's error message """
878        assert False, "Need to be overriden in the derived class"
879
880    def get_name(self):
881        """ return the person's primary name or the name of the family """
882        assert False, "Need to be overriden in the derived class"
883
884    def get_handle(self):
885        """ return the object's handle """
886        return self.obj.handle
887
888    def get_id(self):
889        """ return the object's gramps_id """
890        return self.obj.gramps_id
891
892    def get_rule_id(self):
893        """ return the rule's identification number, and parameters """
894        params = self._get_params()
895        return (self.ID, params)
896
897    def _get_params(self):
898        """ return the rule's parameters """
899        return tuple()
900
901    def report_itself(self):
902        """ return the details about a rule """
903        handle = self.get_handle()
904        the_type = self.TYPE
905        rule_id = self.get_rule_id()
906        severity = self.SEVERITY
907        name = self.get_name()
908        gramps_id = self.get_id()
909        msg = self.get_message()
910        return (msg, gramps_id, name, the_type, rule_id, severity, handle)
911
912class PersonRule(Rule):
913    """
914    Person-based class.
915    """
916    TYPE = 'Person'
917    def get_name(self):
918        """ return the person's primary name """
919        return self.obj.get_primary_name().get_name()
920
921class FamilyRule(Rule):
922    """
923    Family-based class.
924    """
925    TYPE = 'Family'
926    def get_name(self):
927        """ return the name of the family """
928        return family_name(self.obj, self.db)
929
930#-------------------------------------------------------------------------
931#
932# Actual rules for testing
933#
934#-------------------------------------------------------------------------
935class BirthAfterBapt(PersonRule):
936    """ test if a person was baptised before their birth """
937    ID = 1
938    SEVERITY = Rule.ERROR
939    def broken(self):
940        """ return boolean indicating whether this rule is violated """
941        birth_date = get_birth_date(self.db, self.obj)
942        bapt_date = get_bapt_date(self.db, self.obj)
943        birth_ok = birth_date > 0 if birth_date is not None else False
944        bapt_ok = bapt_date > 0 if bapt_date is not None else False
945        return birth_ok and bapt_ok and birth_date > bapt_date
946
947    def get_message(self):
948        """ return the rule's error message """
949        return _("Baptism before birth")
950
951class DeathBeforeBapt(PersonRule):
952    """ test if a person died before their baptism """
953    ID = 2
954    SEVERITY = Rule.ERROR
955    def broken(self):
956        """ return boolean indicating whether this rule is violated """
957        death_date = get_death_date(self.db, self.obj)
958        bapt_date = get_bapt_date(self.db, self.obj)
959        bapt_ok = bapt_date > 0 if bapt_date is not None else False
960        death_ok = death_date > 0 if death_date is not None else False
961        return death_ok and bapt_ok and bapt_date > death_date
962
963    def get_message(self):
964        """ return the rule's error message """
965        return _("Death before baptism")
966
967class BirthAfterBury(PersonRule):
968    """ test if a person was buried before their birth """
969    ID = 3
970    SEVERITY = Rule.ERROR
971    def broken(self):
972        """ return boolean indicating whether this rule is violated """
973        birth_date = get_birth_date(self.db, self.obj)
974        bury_date = get_bury_date(self.db, self.obj)
975        birth_ok = birth_date > 0 if birth_date is not None else False
976        bury_ok = bury_date > 0 if bury_date is not None else False
977        return birth_ok and bury_ok and birth_date > bury_date
978
979    def get_message(self):
980        """ return the rule's error message """
981        return _("Burial before birth")
982
983class DeathAfterBury(PersonRule):
984    """ test if a person was buried before their death """
985    ID = 4
986    SEVERITY = Rule.ERROR
987    def broken(self):
988        """ return boolean indicating whether this rule is violated """
989        death_date = get_death_date(self.db, self.obj)
990        bury_date = get_bury_date(self.db, self.obj)
991        death_ok = death_date > 0 if death_date is not None else False
992        bury_ok = bury_date > 0 if bury_date is not None else False
993        return death_ok and bury_ok and death_date > bury_date
994
995    def get_message(self):
996        """ return the rule's error message """
997        return _("Burial before death")
998
999class BirthAfterDeath(PersonRule):
1000    """ test if a person died before their birth """
1001    ID = 5
1002    SEVERITY = Rule.ERROR
1003    def broken(self):
1004        """ return boolean indicating whether this rule is violated """
1005        birth_date = get_birth_date(self.db, self.obj)
1006        death_date = get_death_date(self.db, self.obj)
1007        birth_ok = birth_date > 0 if birth_date is not None else False
1008        death_ok = death_date > 0 if death_date is not None else False
1009        return birth_ok and death_ok and birth_date > death_date
1010
1011    def get_message(self):
1012        """ return the rule's error message """
1013        return _("Death before birth")
1014
1015class BaptAfterBury(PersonRule):
1016    """ test if a person was buried before their baptism """
1017    ID = 6
1018    SEVERITY = Rule.ERROR
1019    def broken(self):
1020        """ return boolean indicating whether this rule is violated """
1021        bapt_date = get_bapt_date(self.db, self.obj)
1022        bury_date = get_bury_date(self.db, self.obj)
1023        bapt_ok = bapt_date > 0 if bapt_date is not None else False
1024        bury_ok = bury_date > 0 if bury_date is not None else False
1025        return bapt_ok and bury_ok and bapt_date > bury_date
1026
1027    def get_message(self):
1028        """ return the rule's error message """
1029        return _("Burial before baptism")
1030
1031class OldAge(PersonRule):
1032    """ test if a person died beyond the age the user has set """
1033    ID = 7
1034    SEVERITY = Rule.WARNING
1035    def __init__(self, db, person, old_age, est):
1036        """ initialize the rule """
1037        PersonRule.__init__(self, db, person)
1038        self.old_age = old_age
1039        self.est = est
1040
1041    def _get_params(self):
1042        """ return the rule's parameters """
1043        return (self.old_age, self.est)
1044
1045    def broken(self):
1046        """ return boolean indicating whether this rule is violated """
1047        age_at_death = get_age_at_death(self.db, self.obj, self.est)
1048        return age_at_death / 365 > self.old_age
1049
1050    def get_message(self):
1051        """ return the rule's error message """
1052        return _("Old age at death")
1053
1054class UnknownGender(PersonRule):
1055    """ test if a person is neither a male nor a female """
1056    ID = 8
1057    SEVERITY = Rule.WARNING
1058    def broken(self):
1059        """ return boolean indicating whether this rule is violated """
1060        female = self.obj.get_gender() == Person.FEMALE
1061        male = self.obj.get_gender() == Person.MALE
1062        return not (male or female)
1063
1064    def get_message(self):
1065        """ return the rule's error message """
1066        return _("Unknown gender")
1067
1068class MultipleParents(PersonRule):
1069    """ test if a person belongs to multiple families """
1070    ID = 9
1071    SEVERITY = Rule.WARNING
1072    def broken(self):
1073        """ return boolean indicating whether this rule is violated """
1074        n_parent_sets = len(self.obj.get_parent_family_handle_list())
1075        return n_parent_sets > 1
1076
1077    def get_message(self):
1078        """ return the rule's error message """
1079        return _("Multiple parents")
1080
1081class MarriedOften(PersonRule):
1082    """ test if a person was married 'often' """
1083    ID = 10
1084    SEVERITY = Rule.WARNING
1085    def __init__(self, db, person, wedder):
1086        """ initialize the rule """
1087        PersonRule.__init__(self, db, person)
1088        self.wedder = wedder
1089
1090    def _get_params(self):
1091        """ return the rule's parameters """
1092        return (self.wedder,)
1093
1094    def broken(self):
1095        """ return boolean indicating whether this rule is violated """
1096        n_spouses = len(self.obj.get_family_handle_list())
1097        return n_spouses > self.wedder
1098
1099    def get_message(self):
1100        """ return the rule's error message """
1101        return _("Married often")
1102
1103class OldUnmarried(PersonRule):
1104    """ test if a person was married when they died """
1105    ID = 11
1106    SEVERITY = Rule.WARNING
1107    def __init__(self, db, person, old_unm, est):
1108        """ initialize the rule """
1109        PersonRule.__init__(self, db, person)
1110        self.old_unm = old_unm
1111        self.est = est
1112
1113    def _get_params(self):
1114        """ return the rule's parameters """
1115        return (self.old_unm, self.est)
1116
1117    def broken(self):
1118        """ return boolean indicating whether this rule is violated """
1119        age_at_death = get_age_at_death(self.db, self.obj, self.est)
1120        n_spouses = len(self.obj.get_family_handle_list())
1121        return age_at_death / 365 > self.old_unm and n_spouses == 0
1122
1123    def get_message(self):
1124        """ return the rule's error message """
1125        return _("Old and unmarried")
1126
1127class TooManyChildren(PersonRule):
1128    """ test if a person had 'too many' children """
1129    ID = 12
1130    SEVERITY = Rule.WARNING
1131    def __init__(self, db, obj, mx_child_dad, mx_child_mom):
1132        """ initialize the rule """
1133        PersonRule.__init__(self, db, obj)
1134        self.mx_child_dad = mx_child_dad
1135        self.mx_child_mom = mx_child_mom
1136
1137    def _get_params(self):
1138        """ return the rule's parameters """
1139        return (self.mx_child_dad, self.mx_child_mom)
1140
1141    def broken(self):
1142        """ return boolean indicating whether this rule is violated """
1143        n_child = get_n_children(self.db, self.obj)
1144
1145        if (self.obj.get_gender == Person.MALE
1146                and n_child > self.mx_child_dad):
1147            return True
1148
1149        if (self.obj.get_gender == Person.FEMALE
1150                and n_child > self.mx_child_mom):
1151            return True
1152
1153        return False
1154
1155    def get_message(self):
1156        """ return the rule's error message """
1157        return _("Too many children")
1158
1159class SameSexFamily(FamilyRule):
1160    """ test if a family's parents are both male or both female """
1161    ID = 13
1162    SEVERITY = Rule.WARNING
1163    def broken(self):
1164        """ return boolean indicating whether this rule is violated """
1165        mother = get_mother(self.db, self.obj)
1166        father = get_father(self.db, self.obj)
1167        same_sex = (mother and father and
1168                    (mother.get_gender() == father.get_gender()))
1169        unknown_sex = (mother and
1170                       (mother.get_gender() == Person.UNKNOWN))
1171        return same_sex and not unknown_sex
1172
1173    def get_message(self):
1174        """ return the rule's error message """
1175        return _("Same sex marriage")
1176
1177class FemaleHusband(FamilyRule):
1178    """ test if a family's 'husband' is female """
1179    ID = 14
1180    SEVERITY = Rule.WARNING
1181    def broken(self):
1182        """ return boolean indicating whether this rule is violated """
1183        father = get_father(self.db, self.obj)
1184        return father and (father.get_gender() == Person.FEMALE)
1185
1186    def get_message(self):
1187        """ return the rule's error message """
1188        return _("Female husband")
1189
1190class MaleWife(FamilyRule):
1191    """ test if a family's 'wife' is male """
1192    ID = 15
1193    SEVERITY = Rule.WARNING
1194    def broken(self):
1195        """ return boolean indicating whether this rule is violated """
1196        mother = get_mother(self.db, self.obj)
1197        return mother and (mother.get_gender() == Person.MALE)
1198
1199    def get_message(self):
1200        """ return the rule's error message """
1201        return _("Male wife")
1202
1203class SameSurnameFamily(FamilyRule):
1204    """ test if a family's parents were born with the same surname """
1205    ID = 16
1206    SEVERITY = Rule.WARNING
1207    def broken(self):
1208        """ return boolean indicating whether this rule is violated """
1209        mother = get_mother(self.db, self.obj)
1210        father = get_father(self.db, self.obj)
1211        _broken = False
1212
1213        # Make sure both mother and father exist.
1214        if mother and father:
1215            mname = mother.get_primary_name()
1216            fname = father.get_primary_name()
1217            # Only compare birth names (not married names).
1218            if (mname.get_type() == NameType.BIRTH
1219                    and fname.get_type() == NameType.BIRTH):
1220                # Empty names don't count.
1221                if (len(mname.get_surname()) != 0
1222                        and len(fname.get_surname()) != 0):
1223                    # Finally, check if the names are the same.
1224                    if mname.get_surname() == fname.get_surname():
1225                        _broken = True
1226
1227        return _broken
1228
1229    def get_message(self):
1230        """ return the rule's error message """
1231        return _("Husband and wife with the same surname")
1232
1233class LargeAgeGapFamily(FamilyRule):
1234    """ test if a family's parents were born far apart """
1235    ID = 17
1236    SEVERITY = Rule.WARNING
1237    def __init__(self, db, obj, hw_diff, est):
1238        """ initialize the rule """
1239        FamilyRule.__init__(self, db, obj)
1240        self.hw_diff = hw_diff
1241        self.est = est
1242
1243    def _get_params(self):
1244        """ return the rule's parameters """
1245        return (self.hw_diff, self.est)
1246
1247    def broken(self):
1248        """ return boolean indicating whether this rule is violated """
1249        mother = get_mother(self.db, self.obj)
1250        father = get_father(self.db, self.obj)
1251        mother_birth_date = get_birth_date(self.db, mother, self.est)
1252        father_birth_date = get_birth_date(self.db, father, self.est)
1253        mother_birth_date_ok = mother_birth_date > 0
1254        father_birth_date_ok = father_birth_date > 0
1255        large_diff = abs(
1256            father_birth_date-mother_birth_date) / 365 > self.hw_diff
1257        return mother_birth_date_ok and father_birth_date_ok and large_diff
1258
1259    def get_message(self):
1260        """ return the rule's error message """
1261        return _("Large age difference between spouses")
1262
1263class MarriageBeforeBirth(FamilyRule):
1264    """ test if each family's parent was born before the marriage """
1265    ID = 18
1266    SEVERITY = Rule.ERROR
1267    def __init__(self, db, obj, est):
1268        """ initialize the rule """
1269        FamilyRule.__init__(self, db, obj)
1270        self.est = est
1271
1272    def _get_params(self):
1273        """ return the rule's parameters """
1274        return (self.est,)
1275
1276    def broken(self):
1277        """ return boolean indicating whether this rule is violated """
1278        marr_date = get_marriage_date(self.db, self.obj)
1279        marr_date_ok = marr_date > 0
1280
1281        mother = get_mother(self.db, self.obj)
1282        father = get_father(self.db, self.obj)
1283        mother_birth_date = get_birth_date(self.db, mother, self.est)
1284        father_birth_date = get_birth_date(self.db, father, self.est)
1285        mother_birth_date_ok = mother_birth_date > 0
1286        father_birth_date_ok = father_birth_date > 0
1287
1288        father_broken = (father_birth_date_ok and marr_date_ok
1289                         and (father_birth_date > marr_date))
1290        mother_broken = (mother_birth_date_ok and marr_date_ok
1291                         and (mother_birth_date > marr_date))
1292
1293        return father_broken or mother_broken
1294
1295    def get_message(self):
1296        """ return the rule's error message """
1297        return _("Marriage before birth")
1298
1299class MarriageAfterDeath(FamilyRule):
1300    """ test if each family's parent died before the marriage """
1301    ID = 19
1302    SEVERITY = Rule.ERROR
1303    def __init__(self, db, obj, est):
1304        """ initialize the rule """
1305        FamilyRule.__init__(self, db, obj)
1306        self.est = est
1307
1308    def _get_params(self):
1309        """ return the rule's parameters """
1310        return (self.est,)
1311
1312    def broken(self):
1313        """ return boolean indicating whether this rule is violated """
1314        marr_date = get_marriage_date(self.db, self.obj)
1315        marr_date_ok = marr_date > 0
1316
1317        mother = get_mother(self.db, self.obj)
1318        father = get_father(self.db, self.obj)
1319        mother_death_date = get_death_date(self.db, mother, self.est)
1320        father_death_date = get_death_date(self.db, father, self.est)
1321        mother_death_date_ok = mother_death_date > 0
1322        father_death_date_ok = father_death_date > 0
1323
1324        father_broken = (father_death_date_ok and marr_date_ok
1325                         and (father_death_date < marr_date))
1326        mother_broken = (mother_death_date_ok and marr_date_ok
1327                         and (mother_death_date < marr_date))
1328
1329        return father_broken or mother_broken
1330
1331    def get_message(self):
1332        """ return the rule's error message """
1333        return _("Marriage after death")
1334
1335class EarlyMarriage(FamilyRule):
1336    """ test if each family's parent was 'too young' at the marriage """
1337    ID = 20
1338    SEVERITY = Rule.WARNING
1339    def __init__(self, db, obj, yng_mar, est):
1340        """ initialize the rule """
1341        FamilyRule.__init__(self, db, obj)
1342        self.yng_mar = yng_mar
1343        self.est = est
1344
1345    def _get_params(self):
1346        """ return the rule's parameters """
1347        return (self.yng_mar, self.est,)
1348
1349    def broken(self):
1350        """ return boolean indicating whether this rule is violated """
1351        marr_date = get_marriage_date(self.db, self.obj)
1352        marr_date_ok = marr_date > 0
1353
1354        mother = get_mother(self.db, self.obj)
1355        father = get_father(self.db, self.obj)
1356        mother_birth_date = get_birth_date(self.db, mother, self.est)
1357        father_birth_date = get_birth_date(self.db, father, self.est)
1358        mother_birth_date_ok = mother_birth_date > 0
1359        father_birth_date_ok = father_birth_date > 0
1360
1361        father_broken = (
1362            father_birth_date_ok and marr_date_ok and
1363            father_birth_date < marr_date and
1364            ((marr_date - father_birth_date) / 365 < self.yng_mar))
1365        mother_broken = (
1366            mother_birth_date_ok and marr_date_ok and
1367            mother_birth_date < marr_date and
1368            ((marr_date - mother_birth_date) / 365 < self.yng_mar))
1369
1370        return father_broken or mother_broken
1371
1372    def get_message(self):
1373        """ return the rule's error message """
1374        return _("Early marriage")
1375
1376class LateMarriage(FamilyRule):
1377    """ test if each family's parent was 'too old' at the marriage """
1378    ID = 21
1379    SEVERITY = Rule.WARNING
1380    def __init__(self, db, obj, old_mar, est):
1381        """ initialize the rule """
1382        FamilyRule.__init__(self, db, obj)
1383        self.old_mar = old_mar
1384        self.est = est
1385
1386    def _get_params(self):
1387        """ return the rule's parameters """
1388        return (self.old_mar, self.est)
1389
1390    def broken(self):
1391        """ return boolean indicating whether this rule is violated """
1392        marr_date = get_marriage_date(self.db, self.obj)
1393        marr_date_ok = marr_date > 0
1394
1395        mother = get_mother(self.db, self.obj)
1396        father = get_father(self.db, self.obj)
1397        mother_birth_date = get_birth_date(self.db, mother, self.est)
1398        father_birth_date = get_birth_date(self.db, father, self.est)
1399        mother_birth_date_ok = mother_birth_date > 0
1400        father_birth_date_ok = father_birth_date > 0
1401
1402        father_broken = (
1403            father_birth_date_ok and marr_date_ok and
1404            ((marr_date - father_birth_date) / 365 > self.old_mar))
1405        mother_broken = (
1406            mother_birth_date_ok and marr_date_ok and
1407            ((marr_date - mother_birth_date) / 365 > self.old_mar))
1408
1409        return father_broken or mother_broken
1410
1411    def get_message(self):
1412        """ return the rule's error message """
1413        return _("Late marriage")
1414
1415class OldParent(FamilyRule):
1416    """ test if each family's parent was 'too old' at a child's birth """
1417    ID = 22
1418    SEVERITY = Rule.WARNING
1419    def __init__(self, db, obj, old_mom, old_dad, est):
1420        """ initialize the rule """
1421        FamilyRule.__init__(self, db, obj)
1422        self.old_mom = old_mom
1423        self.old_dad = old_dad
1424        self.est = est
1425
1426    def _get_params(self):
1427        """ return the rule's parameters """
1428        return (self.old_mom, self.old_dad, self.est)
1429
1430    def broken(self):
1431        """ return boolean indicating whether this rule is violated """
1432        mother = get_mother(self.db, self.obj)
1433        father = get_father(self.db, self.obj)
1434        mother_birth_date = get_birth_date(self.db, mother, self.est)
1435        father_birth_date = get_birth_date(self.db, father, self.est)
1436        mother_birth_date_ok = mother_birth_date > 0
1437        father_birth_date_ok = father_birth_date > 0
1438
1439        for child_ref in self.obj.get_child_ref_list():
1440            child = find_person(self.db, child_ref.ref)
1441            child_birth_date = get_birth_date(self.db, child, self.est)
1442            child_birth_date_ok = child_birth_date > 0
1443            if not child_birth_date_ok:
1444                continue
1445            father_broken = (
1446                father_birth_date_ok and
1447                ((child_birth_date - father_birth_date) / 365 > self.old_dad))
1448            if father_broken:
1449                self.get_message = self.father_message
1450                return True
1451
1452            mother_broken = (
1453                mother_birth_date_ok and
1454                ((child_birth_date - mother_birth_date) / 365 > self.old_mom))
1455            if mother_broken:
1456                self.get_message = self.mother_message
1457                return True
1458        return False
1459
1460    def father_message(self):
1461        """ return the rule's error message """
1462        return _("Old father")
1463
1464    def mother_message(self):
1465        """ return the rule's error message """
1466        return _("Old mother")
1467
1468class YoungParent(FamilyRule):
1469    """ test if each family's parent was 'too young' at a child's birth """
1470    ID = 23
1471    SEVERITY = Rule.WARNING
1472    def __init__(self, db, obj, yng_mom, yng_dad, est):
1473        """ initialize the rule """
1474        FamilyRule.__init__(self, db, obj)
1475        self.yng_dad = yng_dad
1476        self.yng_mom = yng_mom
1477        self.est = est
1478
1479    def _get_params(self):
1480        """ return the rule's parameters """
1481        return (self.yng_mom, self.yng_dad, self.est)
1482
1483    def broken(self):
1484        """ return boolean indicating whether this rule is violated """
1485        mother = get_mother(self.db, self.obj)
1486        father = get_father(self.db, self.obj)
1487        mother_birth_date = get_birth_date(self.db, mother, self.est)
1488        father_birth_date = get_birth_date(self.db, father, self.est)
1489        mother_birth_date_ok = mother_birth_date > 0
1490        father_birth_date_ok = father_birth_date > 0
1491
1492        for child_ref in self.obj.get_child_ref_list():
1493            child = find_person(self.db, child_ref.ref)
1494            child_birth_date = get_birth_date(self.db, child, self.est)
1495            child_birth_date_ok = child_birth_date > 0
1496            if not child_birth_date_ok:
1497                continue
1498            father_broken = (
1499                father_birth_date_ok and
1500                ((child_birth_date - father_birth_date) / 365 < self.yng_dad))
1501            if father_broken:
1502                self.get_message = self.father_message
1503                return True
1504
1505            mother_broken = (
1506                mother_birth_date_ok and
1507                ((child_birth_date - mother_birth_date) / 365 < self.yng_mom))
1508            if mother_broken:
1509                self.get_message = self.mother_message
1510                return True
1511        return False
1512
1513    def father_message(self):
1514        """ return the rule's error message """
1515        return _("Young father")
1516
1517    def mother_message(self):
1518        """ return the rule's error message """
1519        return _("Young mother")
1520
1521class UnbornParent(FamilyRule):
1522    """ test if each family's parent was not yet born at a child's birth """
1523    ID = 24
1524    SEVERITY = Rule.ERROR
1525    def __init__(self, db, obj, est):
1526        """ initialize the rule """
1527        FamilyRule.__init__(self, db, obj)
1528        self.est = est
1529
1530    def _get_params(self):
1531        """ return the rule's parameters """
1532        return (self.est,)
1533
1534    def broken(self):
1535        """ return boolean indicating whether this rule is violated """
1536        mother = get_mother(self.db, self.obj)
1537        father = get_father(self.db, self.obj)
1538        mother_birth_date = get_birth_date(self.db, mother, self.est)
1539        father_birth_date = get_birth_date(self.db, father, self.est)
1540        mother_birth_date_ok = mother_birth_date > 0
1541        father_birth_date_ok = father_birth_date > 0
1542
1543        for child_ref in self.obj.get_child_ref_list():
1544            child = find_person(self.db, child_ref.ref)
1545            child_birth_date = get_birth_date(self.db, child, self.est)
1546            child_birth_date_ok = child_birth_date > 0
1547            if not child_birth_date_ok:
1548                continue
1549            father_broken = (father_birth_date_ok
1550                             and (father_birth_date > child_birth_date))
1551            if father_broken:
1552                self.get_message = self.father_message
1553                return True
1554
1555            mother_broken = (mother_birth_date_ok
1556                             and (mother_birth_date > child_birth_date))
1557            if mother_broken:
1558                self.get_message = self.mother_message
1559                return True
1560
1561    def father_message(self):
1562        """ return the rule's error message """
1563        return _("Unborn father")
1564
1565    def mother_message(self):
1566        """ return the rule's error message """
1567        return _("Unborn mother")
1568
1569class DeadParent(FamilyRule):
1570    """ test if each family's parent was dead at a child's birth """
1571    ID = 25
1572    SEVERITY = Rule.ERROR
1573    def __init__(self, db, obj, est):
1574        """ initialize the rule """
1575        FamilyRule.__init__(self, db, obj)
1576        self.est = est
1577
1578    def _get_params(self):
1579        """ return the rule's parameters """
1580        return (self.est,)
1581
1582    def broken(self):
1583        """ return boolean indicating whether this rule is violated """
1584        mother = get_mother(self.db, self.obj)
1585        father = get_father(self.db, self.obj)
1586        mother_death_date = get_death_date(self.db, mother, self.est)
1587        father_death_date = get_death_date(self.db, father, self.est)
1588        mother_death_date_ok = mother_death_date > 0
1589        father_death_date_ok = father_death_date > 0
1590
1591        for child_ref in self.obj.get_child_ref_list():
1592            child = find_person(self.db, child_ref.ref)
1593            child_birth_date = get_birth_date(self.db, child, self.est)
1594            child_birth_date_ok = child_birth_date > 0
1595            if not child_birth_date_ok:
1596                continue
1597
1598            has_birth_rel_to_mother = child_ref.mrel == ChildRefType.BIRTH
1599            has_birth_rel_to_father = child_ref.frel == ChildRefType.BIRTH
1600
1601            father_broken = (
1602                has_birth_rel_to_father
1603                and father_death_date_ok
1604                and ((father_death_date + 294) < child_birth_date))
1605            if father_broken:
1606                self.get_message = self.father_message
1607                return True
1608
1609            mother_broken = (has_birth_rel_to_mother
1610                             and mother_death_date_ok
1611                             and (mother_death_date < child_birth_date))
1612            if mother_broken:
1613                self.get_message = self.mother_message
1614                return True
1615
1616    def father_message(self):
1617        """ return the rule's error message """
1618        return _("Dead father")
1619
1620    def mother_message(self):
1621        """ return the rule's error message """
1622        return _("Dead mother")
1623
1624class LargeChildrenSpan(FamilyRule):
1625    """ test if a family's first and last children were born far apart """
1626    ID = 26
1627    SEVERITY = Rule.WARNING
1628    def __init__(self, db, obj, cb_span, est):
1629        """ initialize the rule """
1630        FamilyRule.__init__(self, db, obj)
1631        self.cbs = cb_span
1632        self.est = est
1633
1634    def _get_params(self):
1635        """ return the rule's parameters """
1636        return (self.cbs, self.est)
1637
1638    def broken(self):
1639        """ return boolean indicating whether this rule is violated """
1640        child_birh_dates = get_child_birth_dates(self.db, self.obj, self.est)
1641        child_birh_dates.sort()
1642
1643        return (child_birh_dates and
1644                ((child_birh_dates[-1] - child_birh_dates[0]) / 365 > self.cbs))
1645
1646    def get_message(self):
1647        """ return the rule's error message """
1648        return _("Large year span for all children")
1649
1650class LargeChildrenAgeDiff(FamilyRule):
1651    """ test if any of a family's children were born far apart """
1652    ID = 27
1653    SEVERITY = Rule.WARNING
1654    def __init__(self, db, obj, c_space, est):
1655        """ initialize the rule """
1656        FamilyRule.__init__(self, db, obj)
1657        self.c_space = c_space
1658        self.est = est
1659
1660    def _get_params(self):
1661        """ return the rule's parameters """
1662        return (self.c_space, self.est)
1663
1664    def broken(self):
1665        """ return boolean indicating whether this rule is violated """
1666        child_birh_dates = get_child_birth_dates(self.db, self.obj, self.est)
1667        child_birh_dates_diff = [child_birh_dates[i+1] - child_birh_dates[i]
1668                                 for i in range(len(child_birh_dates)-1)]
1669
1670        return (child_birh_dates_diff and
1671                max(child_birh_dates_diff) / 365 > self.c_space)
1672
1673    def get_message(self):
1674        """ return the rule's error message """
1675        return _("Large age differences between children")
1676
1677class Disconnected(PersonRule):
1678    """ test if a person has no children and no parents """
1679    ID = 28
1680    SEVERITY = Rule.WARNING
1681    def broken(self):
1682        """ return boolean indicating whether this rule is violated """
1683        return (len(self.obj.get_parent_family_handle_list())
1684                + len(self.obj.get_family_handle_list()) == 0)
1685
1686    def get_message(self):
1687        """ return the rule's error message """
1688        return _("Disconnected individual")
1689
1690class InvalidBirthDate(PersonRule):
1691    """ test if a person has an 'invalid' birth date """
1692    ID = 29
1693    SEVERITY = Rule.ERROR
1694    def __init__(self, db, person, invdate):
1695        """ initialize the rule """
1696        PersonRule.__init__(self, db, person)
1697        self._invdate = invdate
1698
1699    def broken(self):
1700        """ return boolean indicating whether this rule is violated """
1701        if not self._invdate: # should we check?
1702            return False
1703        # if so, let's get the birth date
1704        person = self.obj
1705        birth_ref = person.get_birth_ref()
1706        if birth_ref:
1707            birth_event = self.db.get_event_from_handle(birth_ref.ref)
1708            birth_date = birth_event.get_date_object()
1709            if birth_date and not birth_date.get_valid():
1710                return True
1711        return False
1712
1713    def get_message(self):
1714        """ return the rule's error message """
1715        return _("Invalid birth date")
1716
1717class InvalidDeathDate(PersonRule):
1718    """ test if a person has an 'invalid' death date """
1719    ID = 30
1720    SEVERITY = Rule.ERROR
1721    def __init__(self, db, person, invdate):
1722        """ initialize the rule """
1723        PersonRule.__init__(self, db, person)
1724        self._invdate = invdate
1725
1726    def broken(self):
1727        """ return boolean indicating whether this rule is violated """
1728        if not self._invdate: # should we check?
1729            return False
1730        # if so, let's get the death date
1731        person = self.obj
1732        death_ref = person.get_death_ref()
1733        if death_ref:
1734            death_event = self.db.get_event_from_handle(death_ref.ref)
1735            death_date = death_event.get_date_object()
1736            if death_date and not death_date.get_valid():
1737                return True
1738        return False
1739
1740    def get_message(self):
1741        """ return the rule's error message """
1742        return _("Invalid death date")
1743
1744class MarriedRelation(FamilyRule):
1745    """ test if a family has a marriage date but is not marked 'married' """
1746    ID = 31
1747    SEVERITY = Rule.WARNING
1748    def __init__(self, db, obj):
1749        """ initialize the rule """
1750        FamilyRule.__init__(self, db, obj)
1751
1752    def broken(self):
1753        """ return boolean indicating whether this rule is violated """
1754        marr_date = get_marriage_date(self.db, self.obj)
1755        marr_date_ok = marr_date > 0
1756        married = self.obj.get_relationship() == FamilyRelType.MARRIED
1757        if not married and marr_date_ok:
1758            return self.get_message
1759
1760    def get_message(self):
1761        """ return the rule's error message """
1762        return _("Marriage date but not married")
1763
1764class OldAgeButNoDeath(PersonRule):
1765    """ test if a person is 'too old' but is not shown as dead """
1766    ID = 32
1767    SEVERITY = Rule.WARNING
1768    def __init__(self, db, person, old_age, est):
1769        """ initialize the rule """
1770        PersonRule.__init__(self, db, person)
1771        self.old_age = old_age
1772        self.est = est
1773
1774    def _get_params(self):
1775        """ return the rule's parameters """
1776        return (self.old_age, self.est)
1777
1778    def broken(self):
1779        """ return boolean indicating whether this rule is violated """
1780        birth_date = get_birth_date(self.db, self.obj, self.est)
1781        dead = get_death(self.db, self.obj)
1782        death_date = get_death_date(self.db, self.obj, True) # or burial date
1783        if dead or death_date or not birth_date:
1784            return 0
1785        age = (_today - birth_date) / 365
1786        return age > self.old_age
1787
1788    def get_message(self):
1789        """ return the rule's error message """
1790        return _("Old age but no death")
1791
1792class BirthEqualsDeath(PersonRule):
1793    """ test if a person's birth date is the same as their death date """
1794    ID = 33
1795    SEVERITY = Rule.WARNING
1796    def broken(self):
1797        """ return boolean indicating whether this rule is violated """
1798        birth_date = get_birth_date(self.db, self.obj)
1799        death_date = get_death_date(self.db, self.obj)
1800        birth_ok = birth_date > 0 if birth_date is not None else False
1801        death_ok = death_date > 0 if death_date is not None else False
1802        return death_ok and birth_ok and birth_date == death_date
1803
1804    def get_message(self):
1805        """ return the rule's error message """
1806        return _("Birth equals death")
1807
1808class BirthEqualsMarriage(PersonRule):
1809    """ test if a person's birth date is the same as their marriage date """
1810    ID = 34
1811    SEVERITY = Rule.ERROR
1812    def broken(self):
1813        """ return boolean indicating whether this rule is violated """
1814        birth_date = get_birth_date(self.db, self.obj)
1815        birth_ok = birth_date > 0 if birth_date is not None else False
1816        for fhandle in self.obj.get_family_handle_list():
1817            family = self.db.get_family_from_handle(fhandle)
1818            marr_date = get_marriage_date(self.db, family)
1819            marr_ok = marr_date > 0 if marr_date is not None else False
1820            return marr_ok and birth_ok and birth_date == marr_date
1821
1822    def get_message(self):
1823        """ return the rule's error message """
1824        return _("Birth equals marriage")
1825
1826class DeathEqualsMarriage(PersonRule):
1827    """ test if a person's death date is the same as their marriage date """
1828    ID = 35
1829    SEVERITY = Rule.WARNING # it's possible
1830    def broken(self):
1831        """ return boolean indicating whether this rule is violated """
1832        death_date = get_death_date(self.db, self.obj)
1833        death_ok = death_date > 0 if death_date is not None else False
1834        for fhandle in self.obj.get_family_handle_list():
1835            family = self.db.get_family_from_handle(fhandle)
1836            marr_date = get_marriage_date(self.db, family)
1837            marr_ok = marr_date > 0 if marr_date is not None else False
1838            return marr_ok and death_ok and death_date == marr_date
1839
1840    def get_message(self):
1841        """ return the rule's error message """
1842        return _("Death equals marriage")
1843
1844