1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2007-2009   Stephane Charette
5# Copyright (C) 2019-       Serge Noiraud
6#
7# This program is free software; you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation; either version 2 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program; if not, write to the Free Software
19# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20#
21
22"Find possible leading and/or trailing spaces in places name and people"
23
24#------------------------------------------------------------------------
25#
26# GNOME/GTK modules
27#
28#------------------------------------------------------------------------
29from gi.repository import Gtk
30from gi.repository import GObject
31
32#------------------------------------------------------------------------
33#
34# Gramps modules
35#
36#------------------------------------------------------------------------
37from gramps.gen.const import URL_MANUAL_PAGE
38from gramps.gui.plug import tool
39from gramps.gui.editors import (EditPlace, EditPerson)
40from gramps.gen.errors import WindowActiveError
41from gramps.gui.managedwindow import ManagedWindow
42from gramps.gui.utils import ProgressMeter
43from gramps.gui.display import display_help
44from gramps.gui.glade import Glade
45from gramps.gen.const import GRAMPS_LOCALE as glocale
46_ = glocale.translation.sgettext
47
48#-------------------------------------------------------------------------
49#
50# Constants
51#
52#-------------------------------------------------------------------------
53WIKI_HELP_PAGE = '%s_-_Tools' % URL_MANUAL_PAGE
54WIKI_HELP_SEC = _('manual|Remove_leading_and_trailing_spaces')
55
56def validate_lat_lon(field):
57    """
58    Return True if some characters are found in the field
59    # hyphen (u+2010)
60    # non-breaking hyphen (u+2011)
61    # figure dash (u+2012)
62    # en dash (u+2013)
63    # em dash (u+2014)
64    # horizontal bar (u+2015)
65    """
66    for char in (',', '\u2010', '\u2011', '\u2012',
67                 '\u2013', '\u2014', '\u2015'):
68        if field.find(char) != -1:
69            return True
70    return False
71
72#------------------------------------------------------------------------
73#
74# RemoveSpaces class
75#
76#------------------------------------------------------------------------
77class RemoveSpaces(ManagedWindow):
78    """
79    Find leading and trailing spaces in Place names and person names
80    """
81    def __init__(self, dbstate, user, options_class, name, callback=None):
82        uistate = user.uistate
83        dummy_opt = options_class
84        dummy_nme = name
85        dummy_cb = callback
86
87        self.title = _('Clean input data')
88        ManagedWindow.__init__(self, uistate, [], self.__class__)
89        self.dbstate = dbstate
90        self.uistate = uistate
91        self.db = dbstate.db
92
93        top_dialog = Glade()
94
95        top_dialog.connect_signals({
96            "destroy_passed_object" : self.close,
97            "on_help_clicked"       : self.on_help_clicked,
98            "on_delete_event"       : self.close,
99        })
100
101        window = top_dialog.toplevel
102        title = top_dialog.get_object("title")
103        self.set_window(window, title, self.title)
104        tip = _('Search leading and/or trailing spaces for persons'
105                ' and places. Search comma in coordinates fields.\n'
106                'Double click on a row to edit its content.')
107        title.set_tooltip_text(tip)
108
109        # start the progress indicator
110        self.progress = ProgressMeter(self.title, _('Starting'),
111                                      parent=uistate.window)
112        steps = self.db.get_number_of_people() + self.db.get_number_of_places()
113        self.progress.set_pass(_('Looking for possible fields with leading or'
114                                 ' trailing spaces'), steps)
115
116        self.model_1 = Gtk.ListStore(
117            GObject.TYPE_STRING,    # 0==handle
118            GObject.TYPE_STRING,    # 1==firstname
119            GObject.TYPE_STRING,    # 2==surname
120            GObject.TYPE_STRING,    # 3==alternate name
121            GObject.TYPE_STRING,    # 4==group_as
122            )
123        self.model_1.set_sort_column_id(
124            Gtk.TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, 1)
125
126        self.label_1 = top_dialog.get_object("label_1")
127        self.label_1.set_text(_('Person'))
128        self.treeview_1 = top_dialog.get_object("treeview_1")
129        self.treeview_1.set_model(self.model_1)
130        col1 = Gtk.TreeViewColumn(_('handle'),
131                                  Gtk.CellRendererText(), text=0)
132        renderer1 = Gtk.CellRendererText()
133        renderer1.set_property('underline-set', True)
134        renderer1.set_property('underline', 2) # 2=double underline
135        col2 = Gtk.TreeViewColumn(_('firstname'), renderer1, text=1)
136        renderer2 = Gtk.CellRendererText()
137        renderer2.set_property('underline-set', True)
138        renderer2.set_property('underline', 2) # 2=double underline
139        col3 = Gtk.TreeViewColumn(_('surname'), renderer2, text=2)
140        renderer3 = Gtk.CellRendererText()
141        renderer3.set_property('underline-set', True)
142        renderer3.set_property('underline', 2) # 2=double underline
143        col4 = Gtk.TreeViewColumn(_('alternate name'), renderer3, text=3)
144        renderer4 = Gtk.CellRendererText()
145        renderer4.set_property('underline-set', True)
146        renderer4.set_property('underline', 2) # 2=double underline
147        col5 = Gtk.TreeViewColumn(_('group as'), renderer4, text=4)
148        col1.set_resizable(True)
149        col1.set_visible(False)
150        col2.set_resizable(True)
151        col3.set_resizable(True)
152        col4.set_resizable(True)
153        col5.set_resizable(True)
154        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
155        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
156        col3.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
157        col4.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
158        col5.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
159        self.treeview_1.append_column(col1)
160        self.treeview_1.append_column(col2)
161        self.treeview_1.append_column(col3)
162        self.treeview_1.append_column(col4)
163        self.treeview_1.append_column(col5)
164        self.treeselection = self.treeview_1.get_selection()
165        self.treeview_1.connect('row-activated', self.rowactivated_cb1)
166
167        self.model_2 = Gtk.ListStore(
168            GObject.TYPE_STRING,    # 0==handle
169            GObject.TYPE_STRING,    # 1==name
170            GObject.TYPE_STRING,    # 2==latitude
171            GObject.TYPE_STRING)    # 3==longitude
172        self.model_2.set_sort_column_id(
173            Gtk.TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, 1)
174
175        self.label_2 = top_dialog.get_object("label_2")
176        self.label_2.set_text(_('Place'))
177        self.treeview_2 = top_dialog.get_object("treeview_2")
178        self.treeview_2.set_model(self.model_2)
179        col1 = Gtk.TreeViewColumn(_('handle'),
180                                  Gtk.CellRendererText(), text=0)
181        renderer5 = Gtk.CellRendererText()
182        renderer5.set_property('underline-set', True)
183        renderer5.set_property('underline', 2) # 2=double underline
184        col2 = Gtk.TreeViewColumn(_('name'), renderer5, text=1)
185        renderer6 = Gtk.CellRendererText()
186        renderer6.set_property('underline-set', True)
187        renderer6.set_property('underline', 2) # 2=double underline
188        col3 = Gtk.TreeViewColumn(_('latitude'), renderer6, text=2)
189        renderer7 = Gtk.CellRendererText()
190        renderer7.set_property('underline-set', True)
191        renderer7.set_property('underline', 2) # 2=double underline
192        col4 = Gtk.TreeViewColumn(_('longitude'), renderer7, text=3)
193        col1.set_resizable(True)
194        col1.set_visible(False)
195        col2.set_resizable(True)
196        col3.set_resizable(True)
197        col4.set_resizable(True)
198        col1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
199        col2.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
200        col3.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
201        col4.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
202        self.treeview_2.append_column(col1)
203        self.treeview_2.append_column(col2)
204        self.treeview_2.append_column(col3)
205        self.treeview_2.append_column(col4)
206        self.treeselection = self.treeview_2.get_selection()
207        self.treeview_2.connect('row-activated', self.rowactivated_cb2)
208
209        self.places()
210        self.people()
211
212        # close the progress bar
213        self.progress.close()
214
215        self.show()
216
217    def places(self):
218        """
219        For all places in the database, if the name contains leading or
220        trailing spaces.
221        """
222        for place_handle in self.db.get_place_handles():
223            self.progress.step()
224            place = self.db.get_place_from_handle(place_handle)
225            place_name = place.get_name()
226            pname = place_name.get_value()
227            found = False
228            if pname != pname.strip():
229                found = True
230            plat = place.get_latitude()
231            if plat != plat.strip():
232                found = True
233            if validate_lat_lon(plat):
234                found = True
235            plon = place.get_longitude()
236            if plon != plon.strip():
237                found = True
238            if validate_lat_lon(plon):
239                found = True
240            if found:
241                value = (place_handle, pname, plat, plon)
242                self.model_2.append(value)
243        return True
244
245    def people(self):
246        """
247        For all persons in the database, if the name contains leading or
248        trailing spaces. Works for alternate names and group_as.
249        """
250        for person_handle in self.db.get_person_handles():
251            self.progress.step()
252            person = self.db.get_person_from_handle(person_handle)
253            primary_name = person.get_primary_name()
254            fname = primary_name.get_first_name()
255            found = False
256            if fname != fname.strip():
257                found = True
258            sname = primary_name.get_primary_surname().get_surname()
259            if sname != sname.strip():
260                found = True
261            paname = ""
262            for name in primary_name.get_surname_list():
263                aname = name.get_surname()
264                if aname != sname and aname != aname.strip():
265                    found = True
266                    if paname != "":
267                        paname += ', '
268                    paname += aname
269            groupas = primary_name.group_as
270            if groupas != groupas.strip():
271                found = True
272            if found:
273                value = (person_handle, fname, sname, paname, groupas)
274                self.model_1.append(value)
275        return True
276
277    def rowactivated_cb1(self, treeview, path, column):
278        """
279        Called when a Person row is activated.
280        """
281        dummy_tv = treeview
282        dummy_col = column
283        iter_ = self.model_1.get_iter(path)
284        handle = self.model_1.get_value(iter_, 0)
285        person = self.dbstate.db.get_person_from_handle(handle)
286        if person:
287            try:
288                EditPerson(self.dbstate, self.uistate, [], person)
289            except WindowActiveError:
290                pass
291            return True
292        return False
293
294    def rowactivated_cb2(self, treeview, path, column):
295        """
296        Called when a Place row is activated.
297        """
298        dummy_tv = treeview
299        dummy_col = column
300        iter_ = self.model_2.get_iter(path)
301        handle = self.model_2.get_value(iter_, 0)
302        place = self.dbstate.db.get_place_from_handle(handle)
303        if place:
304            try:
305                EditPlace(self.dbstate, self.uistate, [], place)
306            except WindowActiveError:
307                pass
308            return True
309        return False
310
311    def on_help_clicked(self, _obj):
312        """
313        Display the relevant portion of Gramps manual.
314        """
315        display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
316
317    def close(self, *obj):
318        ManagedWindow.close(self, *obj)
319
320#------------------------------------------------------------------------
321#
322# RemoveSpacesOptions
323#
324#------------------------------------------------------------------------
325class RemoveSpacesOptions(tool.ToolOptions):
326    """
327    Defines options and provides handling interface.
328    """
329    def __init__(self, name, person_id=None):
330        """ Initialize the options class """
331        tool.ToolOptions.__init__(self, name, person_id)
332