diff --git a/example/gramps/child-father-child-loop.gramps b/example/gramps/child-father-child-loop.gramps
new file mode 100644
index 000000000..b2e7a69b9
--- /dev/null
+++ b/example/gramps/child-father-child-loop.gramps
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+ M
+
+ Child
+ Child
+
+
+
+
+
+ M
+
+ Father
+ Father
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/gramps/test_complex_loop.gramps b/example/gramps/test_complex_loop.gramps
new file mode 100644
index 000000000..d733b88ad
--- /dev/null
+++ b/example/gramps/test_complex_loop.gramps
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+ M
+
+ Child
+ Child
+
+
+
+
+
+
+ M
+
+ Father
+ Father
+
+
+
+
+
+ M
+
+ Child2
+ Father
+
+
+
+
+
+ M
+
+ Child3
+ Father
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gramps/gen/utils/alive.py b/gramps/gen/utils/alive.py
index 3f6e5f045..dbbe7b796 100644
--- a/gramps/gen/utils/alive.py
+++ b/gramps/gen/utils/alive.py
@@ -86,6 +86,7 @@ class ProbablyAlive(object):
self.MAX_SIB_AGE_DIFF = max_sib_age_diff
self.MAX_AGE_PROB_ALIVE = max_age_prob_alive
self.AVG_GENERATION_GAP = avg_generation_gap
+ self.pset = set()
def probably_alive_range(self, person, is_spouse=False):
# FIXME: some of these computed dates need to be a span. For
@@ -95,6 +96,7 @@ class ProbablyAlive(object):
# "between 1930 and 1940")
if person is None:
return (None, None, "", None)
+ self.pset = set()
birth_ref = person.get_birth_ref()
death_ref = person.get_death_ref()
death_date = None
@@ -272,6 +274,9 @@ class ProbablyAlive(object):
# ago.
def descendants_too_old (person, years):
+ if person.handle in self.pset:
+ return (None, None, "", None)
+ self.pset.add(person.handle)
for family_handle in person.get_family_handle_list():
family = self.db.get_family_from_handle(family_handle)
if not family:
@@ -343,6 +348,9 @@ class ProbablyAlive(object):
return (date1, date2, explain, other)
def ancestors_too_old(person, year):
+ if person.handle in self.pset:
+ return (None, None, "", None)
+ self.pset.add(person.handle)
LOG.debug("ancestors_too_old('%s', %s)".format(
name_displayer.display(person), year) )
family_handle = person.get_main_parents_family_handle()
diff --git a/gramps/plugins/tool/findloop.glade b/gramps/plugins/tool/findloop.glade
new file mode 100644
index 000000000..3fc52ec70
--- /dev/null
+++ b/gramps/plugins/tool/findloop.glade
@@ -0,0 +1,123 @@
+
+
+
+
+
+
diff --git a/gramps/plugins/tool/findloop.py b/gramps/plugins/tool/findloop.py
new file mode 100644
index 000000000..e71ce39f5
--- /dev/null
+++ b/gramps/plugins/tool/findloop.py
@@ -0,0 +1,219 @@
+#
+# Gramps - a GTK+/GNOME based genealogy program
+#
+# Copyright (C) 2007-2009 Stephane Charette
+# Copyright (C) 2016- Serge Noiraud
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+
+"Find possible loop in a people descendance"
+
+#------------------------------------------------------------------------
+#
+# GNOME/GTK modules
+#
+#------------------------------------------------------------------------
+from gi.repository import Gtk
+from gi.repository import GObject
+
+#------------------------------------------------------------------------
+#
+# Gramps modules
+#
+#------------------------------------------------------------------------
+from gramps.gen.const import GRAMPS_LOCALE as glocale
+_ = glocale.translation.sgettext
+ngettext = glocale.translation.ngettext # else "nearby" comments are ignored
+from gramps.gen.const import URL_MANUAL_PAGE
+from gramps.gui.plug import tool
+from gramps.gen.plug.report import utils as ReportUtils
+from gramps.gui.editors import EditPerson, EditFamily
+from gramps.gui.managedwindow import ManagedWindow
+from gramps.gui.utils import ProgressMeter
+from gramps.gui.display import display_help
+from gramps.gui.glade import Glade
+from gramps.gen.lib import Tag
+from gramps.gen.db import DbTxn
+from gramps.gen.display.name import displayer as _nd
+from gramps.gui.editors import EditFamily
+
+#-------------------------------------------------------------------------
+#
+# Constants
+#
+#-------------------------------------------------------------------------
+WIKI_HELP_PAGE = '%s_-_Tools' % URL_MANUAL_PAGE
+WIKI_HELP_SEC = _('manual|Find_database_loop')
+
+#------------------------------------------------------------------------
+#
+# FindLoop class
+#
+#------------------------------------------------------------------------
+class FindLoop(ManagedWindow) :
+
+ def __init__(self, dbstate, user, options_class, name, callback=None):
+ uistate = user.uistate
+
+ self.title = _('Find database loop')
+ ManagedWindow.__init__(self, uistate, [], self.__class__)
+ self.dbstate = dbstate
+ self.uistate = uistate
+ self.db = dbstate.db
+
+ topDialog = Glade()
+
+ topDialog.connect_signals({
+ "destroy_passed_object" : self.close,
+ "on_help_clicked" : self.on_help_clicked,
+ "on_delete_event" : self.close,
+ })
+
+ window = topDialog.toplevel
+ title = topDialog.get_object("title")
+ self.set_window(window, title, self.title)
+
+ # start the progress indicator
+ self.progress = ProgressMeter(self.title,_('Starting'),
+ parent=self.window)
+ self.progress.set_pass(_('Looking for possible loop for each person'),
+ self.db.get_number_of_people())
+
+ self.model = Gtk.ListStore(
+ GObject.TYPE_STRING, # 0==father id
+ GObject.TYPE_STRING, # 1==father
+ GObject.TYPE_STRING, # 2==son id
+ GObject.TYPE_STRING, # 3==son
+ GObject.TYPE_STRING) # 4==family gid
+
+ self.treeView = topDialog.get_object("treeview")
+ self.treeView.set_model(self.model)
+ col1 = Gtk.TreeViewColumn(_('Gramps ID'), Gtk.CellRendererText(), text=0)
+ col2 = Gtk.TreeViewColumn(_('Ancestor'), Gtk.CellRendererText(), text=1)
+ col3 = Gtk.TreeViewColumn(_('Gramps ID'), Gtk.CellRendererText(), text=2)
+ col4 = Gtk.TreeViewColumn(_('Descendant'), Gtk.CellRendererText(), text=3)
+ col5 = Gtk.TreeViewColumn(_('Family ID'), Gtk.CellRendererText(), text=4)
+ col1.set_resizable(True)
+ col2.set_resizable(True)
+ col3.set_resizable(True)
+ col4.set_resizable(True)
+ col5.set_resizable(True)
+ col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ col3.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ col4.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ col5.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ col1.set_sort_column_id(0)
+ col2.set_sort_column_id(1)
+ col3.set_sort_column_id(2)
+ col4.set_sort_column_id(3)
+ col5.set_sort_column_id(4)
+ self.treeView.append_column(col1)
+ self.treeView.append_column(col2)
+ self.treeView.append_column(col3)
+ self.treeView.append_column(col4)
+ self.treeView.append_column(col5)
+ self.treeSelection = self.treeView.get_selection()
+ self.treeView.connect('row-activated', self.rowActivated)
+
+ people = self.db.get_person_handles()
+ count = 0
+ for person_handle in people:
+ person = self.db.get_person_from_handle(person_handle)
+ count += 1
+ self.current = person
+ self.parent = None
+ self.descendants(person_handle, set())
+ self.progress.set_header("%d/%d" % (count, len(people)))
+ self.progress.step()
+
+ # close the progress bar
+ self.progress.close()
+
+ self.show()
+
+ def descendants(self, person_handle, new_list):
+ person = self.db.get_person_from_handle(person_handle)
+ pset = set()
+ for item in new_list:
+ pset.add(item)
+ if person.handle in pset:
+ # We found one loop
+ father_id = self.current.get_gramps_id()
+ father = _nd.display(self.current)
+ son_id = self.parent.get_gramps_id()
+ son = _nd.display(self.parent)
+ value = (father_id, father, son_id, son, self.curr_fam)
+ found = False
+ for pth in range(len(self.model)):
+ path = Gtk.TreePath(pth)
+ treeiter = self.model.get_iter(path)
+ find = (self.model.get_value(treeiter, 0),
+ self.model.get_value(treeiter, 1),
+ self.model.get_value(treeiter, 2),
+ self.model.get_value(treeiter, 3),
+ self.model.get_value(treeiter, 4))
+ if find == value:
+ found = True
+ if not found:
+ self.model.append(value)
+ return
+ pset.add(person.handle)
+ for family_handle in person.get_family_handle_list():
+ family = self.db.get_family_from_handle(family_handle)
+ self.curr_fam = family.get_gramps_id()
+ if not family:
+ # can happen with LivingProxyDb(PrivateProxyDb(db))
+ continue
+ for child_ref in family.get_child_ref_list():
+ child_handle = child_ref.ref
+ self.parent = person
+ self.descendants(child_handle, pset)
+
+ def rowActivated(self, treeView, path, column) :
+ # first we need to check that the row corresponds to a person
+ iter = self.model.get_iter(path)
+ From_id = self.model.get_value(iter, 0)
+ To_id = self.model.get_value(iter, 2)
+ Fam_id = self.model.get_value(iter, 4)
+ fam = self.dbstate.db.get_family_from_gramps_id(Fam_id)
+ if fam:
+ try:
+ EditFamily(self.dbstate, self.uistate, [], fam)
+ except WindowActiveError:
+ pass
+ return True
+ return False
+
+ def on_help_clicked(self, obj):
+ """Display the relevant portion of GRAMPS manual"""
+ display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
+
+ def close(self, *obj):
+ ManagedWindow.close(self,*obj)
+
+#------------------------------------------------------------------------
+#
+# FindLoopOptions
+#
+#------------------------------------------------------------------------
+class FindLoopOptions(tool.ToolOptions):
+ """
+ Defines options and provides handling interface.
+ """
+ def __init__(self, name, person_id=None):
+ """ Initialize the options class """
+ tool.ToolOptions.__init__(self, name, person_id)
diff --git a/gramps/plugins/tool/tools.gpr.py b/gramps/plugins/tool/tools.gpr.py
index 8ff1f9696..98a86ce10 100644
--- a/gramps/plugins/tool/tools.gpr.py
+++ b/gramps/plugins/tool/tools.gpr.py
@@ -474,3 +474,25 @@ optionclass = 'MergeCitationsOptions',
tool_modes = [TOOL_MODE_GUI]
)
+#------------------------------------------------------------------------
+#
+# Find database Loop
+#
+#------------------------------------------------------------------------
+
+register(TOOL,
+id = 'loop',
+name = _("Find database loop"),
+description = _("Searches the entire database, looking for "
+ "a possible loop."),
+version = '1.0',
+gramps_target_version = MODULE_VERSION,
+status = STABLE,
+fname = 'findloop.py',
+authors = ["Serge Noiraud"],
+authors_email = ["serge.noiraud@free.fr"],
+category = TOOL_UTILS,
+toolclass = 'FindLoop',
+optionclass = 'FindLoopOptions',
+tool_modes = [TOOL_MODE_GUI]
+ )