Merge pull request #102 from SNoiraud/bug5449
5449: Pedigree view crashes if you happen to choose a child as ancestor
This commit is contained in:
commit
4342b319e3
42
example/gramps/child-father-child-loop.gramps
Normal file
42
example/gramps/child-father-child-loop.gramps
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.6.0//EN"
|
||||
"http://gramps-project.org/xml/1.6.0/grampsxml.dtd">
|
||||
<database xmlns="http://gramps-project.org/xml/1.6.0/">
|
||||
<header>
|
||||
<created date="2015-07-21" version="GrampsAIO64-4.1.3-1"/>
|
||||
<researcher>
|
||||
</researcher>
|
||||
</header>
|
||||
<people>
|
||||
<person handle="_d12d89b02aa5121726adba4f517" change="1437459556" id="I0000">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Child</first>
|
||||
<surname>Child</surname>
|
||||
</name>
|
||||
<childof hlink="_d12d8a32ae01d9449d5da18eda0"/>
|
||||
<parentin hlink="_d12d8a7573462f21848658c9c5d"/>
|
||||
</person>
|
||||
<person handle="_d12d89d4dce7eaaa68e31f6100e" change="1437459556" id="I0001">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Father</first>
|
||||
<surname>Father</surname>
|
||||
</name>
|
||||
<childof hlink="_d12d8a7573462f21848658c9c5d"/>
|
||||
<parentin hlink="_d12d8a32ae01d9449d5da18eda0"/>
|
||||
</person>
|
||||
</people>
|
||||
<families>
|
||||
<family handle="_d12d8a32ae01d9449d5da18eda0" change="1437459531" id="F0000">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d12d89d4dce7eaaa68e31f6100e"/>
|
||||
<childref hlink="_d12d89b02aa5121726adba4f517"/>
|
||||
</family>
|
||||
<family handle="_d12d8a7573462f21848658c9c5d" change="1437459556" id="F0001">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d12d89b02aa5121726adba4f517"/>
|
||||
<childref hlink="_d12d89d4dce7eaaa68e31f6100e"/>
|
||||
</family>
|
||||
</families>
|
||||
</database>
|
72
example/gramps/test_complex_loop.gramps
Normal file
72
example/gramps/test_complex_loop.gramps
Normal file
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.1//EN"
|
||||
"http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
|
||||
<database xmlns="http://gramps-project.org/xml/1.7.1/">
|
||||
<header>
|
||||
<created date="2016-04-10" version="5.0.0"/>
|
||||
<researcher>
|
||||
</researcher>
|
||||
</header>
|
||||
<people>
|
||||
<person handle="_d12d89b02aa5121726adba4f517" change="1456088166" id="I0000">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Child</first>
|
||||
<surname>Child</surname>
|
||||
</name>
|
||||
<childof hlink="_d12d8a32ae01d9449d5da18eda0"/>
|
||||
<childof hlink="_d3e382712c1429da71250fdc5b7"/>
|
||||
<parentin hlink="_d12d8a7573462f21848658c9c5d"/>
|
||||
</person>
|
||||
<person handle="_d12d89d4dce7eaaa68e31f6100e" change="1437459556" id="I0001">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Father</first>
|
||||
<surname>Father</surname>
|
||||
</name>
|
||||
<childof hlink="_d12d8a7573462f21848658c9c5d"/>
|
||||
<parentin hlink="_d12d8a32ae01d9449d5da18eda0"/>
|
||||
</person>
|
||||
<person handle="_d3e381c6e76124b59bb9a4c312e" change="1456088118" id="I0002">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Child2</first>
|
||||
<surname>Father</surname>
|
||||
</name>
|
||||
<childof hlink="_d12d8a32ae01d9449d5da18eda0"/>
|
||||
<parentin hlink="_d3e38215ed8371bb62a9f7785f1"/>
|
||||
</person>
|
||||
<person handle="_d3e382494d07adfa45784539dce" change="1456088166" id="I0003">
|
||||
<gender>M</gender>
|
||||
<name type="Birth Name">
|
||||
<first>Child3</first>
|
||||
<surname>Father</surname>
|
||||
</name>
|
||||
<childof hlink="_d3e38215ed8371bb62a9f7785f1"/>
|
||||
<parentin hlink="_d3e382712c1429da71250fdc5b7"/>
|
||||
</person>
|
||||
</people>
|
||||
<families>
|
||||
<family handle="_d12d8a32ae01d9449d5da18eda0" change="1456088074" id="F0000">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d12d89d4dce7eaaa68e31f6100e"/>
|
||||
<childref hlink="_d12d89b02aa5121726adba4f517"/>
|
||||
<childref hlink="_d3e381c6e76124b59bb9a4c312e"/>
|
||||
</family>
|
||||
<family handle="_d12d8a7573462f21848658c9c5d" change="1437459556" id="F0001">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d12d89b02aa5121726adba4f517"/>
|
||||
<childref hlink="_d12d89d4dce7eaaa68e31f6100e"/>
|
||||
</family>
|
||||
<family handle="_d3e38215ed8371bb62a9f7785f1" change="1456088118" id="F0002">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d3e381c6e76124b59bb9a4c312e"/>
|
||||
<childref hlink="_d3e382494d07adfa45784539dce"/>
|
||||
</family>
|
||||
<family handle="_d3e382712c1429da71250fdc5b7" change="1456088166" id="F0003">
|
||||
<rel type="Unknown"/>
|
||||
<father hlink="_d3e382494d07adfa45784539dce"/>
|
||||
<childref hlink="_d12d89b02aa5121726adba4f517"/>
|
||||
</family>
|
||||
</families>
|
||||
</database>
|
@ -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()
|
||||
|
123
gramps/plugins/tool/findloop.glade
Normal file
123
gramps/plugins/tool/findloop.glade
Normal file
@ -0,0 +1,123 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.16.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.10"/>
|
||||
<object class="GtkDialog" id="findloop">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="default_width">450</property>
|
||||
<property name="default_height">400</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<signal name="delete-event" handler="on_delete_event" swapped="no"/>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog-vbox1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog-action_area1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="close">
|
||||
<property name="label">gtk-close</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="destroy_passed_object" swapped="yes"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="helpbutton1">
|
||||
<property name="label">gtk-help</property>
|
||||
<property name="use_action_appearance">False</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can_default">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="clicked" handler="on_help_clicked" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="vbox1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="border_width">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="title">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="justify">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="padding">8</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">out</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="treeview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview-selection1"/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="0">close</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
</interface>
|
219
gramps/plugins/tool/findloop.py
Normal file
219
gramps/plugins/tool/findloop.py
Normal file
@ -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)
|
@ -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]
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user