1#
2# Gramps - a GTK+/GNOME based genealogy program
3#
4# Copyright (C) 2000-2006  Donald N. Allingham
5# Copyright (C) 2008       Brian G. Matherly
6# Copyright (C) 2010       Jakim Friant
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21#
22
23"""Tools/Analysis and Exploration/Compare Individual Events"""
24
25#------------------------------------------------------------------------
26#
27# python modules
28#
29#------------------------------------------------------------------------
30import os
31from collections import defaultdict
32
33#------------------------------------------------------------------------
34#
35# GNOME/GTK modules
36#
37#------------------------------------------------------------------------
38from gi.repository import Gtk
39
40#------------------------------------------------------------------------
41#
42# Gramps modules
43#
44#------------------------------------------------------------------------
45from gramps.gen.filters import GenericFilter, rules
46from gramps.gui.filters import build_filter_model
47from gramps.gen.sort import Sort
48from gramps.gui.utils import ProgressMeter
49from gramps.gen.utils.docgen import ODSTab
50from gramps.gen.const import CUSTOM_FILTERS, URL_MANUAL_PAGE
51from gramps.gen.errors import WindowActiveError
52from gramps.gen.datehandler import get_date
53from gramps.gui.dialog import WarningDialog
54from gramps.gui.plug import tool
55from gramps.gen.plug.report import utils
56from gramps.gui.display import display_help
57from gramps.gui.managedwindow import ManagedWindow
58from gramps.gen.const import GRAMPS_LOCALE as glocale
59_ = glocale.translation.sgettext
60from gramps.gui.glade import Glade
61from gramps.gui.editors import FilterEditor
62from gramps.gen.constfunc import get_curr_dir
63
64#-------------------------------------------------------------------------
65#
66# Constants
67#
68#-------------------------------------------------------------------------
69WIKI_HELP_PAGE = '%s_-_Tools' % URL_MANUAL_PAGE
70WIKI_HELP_SEC = _('manual|Compare_Individual_Events')
71
72#------------------------------------------------------------------------
73#
74# EventCmp
75#
76#------------------------------------------------------------------------
77class TableReport:
78    """
79    This class provides an interface for the spreadsheet table
80    used to save the data into the file.
81    """
82
83    def __init__(self,filename,doc):
84        self.filename = filename
85        self.doc = doc
86
87    def initialize(self,cols):
88        self.doc.open(self.filename)
89        self.doc.start_page()
90
91    def finalize(self):
92        self.doc.end_page()
93        self.doc.close()
94
95    def write_table_data(self,data,skip_columns=[]):
96        self.doc.start_row()
97        index = -1
98        for item in data:
99            index += 1
100            if index not in skip_columns:
101                self.doc.write_cell(item)
102        self.doc.end_row()
103
104    def set_row(self,val):
105        self.row = val + 2
106
107    def write_table_head(self, data):
108        self.doc.start_row()
109        list(map(self.doc.write_cell, data))
110        self.doc.end_row()
111
112#------------------------------------------------------------------------
113#
114#
115#
116#------------------------------------------------------------------------
117class EventComparison(tool.Tool,ManagedWindow):
118    def __init__(self, dbstate, user, options_class, name, callback=None):
119        uistate = user.uistate
120        self.dbstate = dbstate
121        self.uistate = uistate
122
123        tool.Tool.__init__(self,dbstate, options_class, name)
124        ManagedWindow.__init__(self, uistate, [], self)
125        self.qual = 0
126
127        self.filterDialog = Glade(toplevel="filters", also_load=["liststore1"])
128        self.filterDialog.connect_signals({
129            "on_apply_clicked"       : self.on_apply_clicked,
130            "on_editor_clicked"      : self.filter_editor_clicked,
131            "on_help_clicked"        : self.on_help_clicked,
132            "destroy_passed_object"  : self.close,
133            "on_write_table"         : self.__dummy,
134            })
135
136        window = self.filterDialog.toplevel
137        self.filters = self.filterDialog.get_object("filter_list")
138        self.label = _('Event comparison filter selection')
139        self.set_window(window,self.filterDialog.get_object('title'),
140                        self.label)
141        self.setup_configs('interface.eventcomparison', 640, 220)
142
143        self.on_filters_changed('Person')
144        uistate.connect('filters-changed', self.on_filters_changed)
145
146        self.show()
147
148    def __dummy(self, obj):
149        """dummy callback, needed because widget is in same glade file
150        as another widget, so callbacks must be defined to avoid warnings.
151        """
152        pass
153
154    def on_filters_changed(self, name_space):
155        if name_space == 'Person':
156            all_filter = GenericFilter()
157            all_filter.set_name(_("Entire Database"))
158            all_filter.add_rule(rules.person.Everyone([]))
159            self.filter_model = build_filter_model('Person', [all_filter])
160            self.filters.set_model(self.filter_model)
161            self.filters.set_active(0)
162
163    def on_help_clicked(self, obj):
164        """Display the relevant portion of Gramps manual"""
165        display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
166
167    def build_menu_names(self, obj):
168        return (_("Filter selection"),_("Event Comparison tool"))
169
170    def filter_editor_clicked(self, obj):
171        try:
172            FilterEditor('Person',CUSTOM_FILTERS,
173                                      self.dbstate,self.uistate)
174        except WindowActiveError:
175            pass
176
177    def on_apply_clicked(self, obj):
178        cfilter = self.filter_model[self.filters.get_active()][1]
179
180        progress_bar = ProgressMeter(_('Comparing events'), '',
181                                     parent=self.window)
182        progress_bar.set_pass(_('Selecting people'),1)
183
184        plist = cfilter.apply(self.db,
185                              self.db.iter_person_handles())
186
187        progress_bar.step()
188        progress_bar.close()
189        self.options.handler.options_dict['filter'] = self.filters.get_active()
190        # Save options
191        self.options.handler.save_options()
192
193        if len(plist) == 0:
194            WarningDialog(_("No matches were found"),
195                          parent=self.window)
196        else:
197            EventComparisonResults(self.dbstate, self.uistate, plist, self.track)
198
199#-------------------------------------------------------------------------
200#
201#
202#
203#-------------------------------------------------------------------------
204##def by_value(first,second):
205##    return cmp(second[0],first[0])
206
207#-------------------------------------------------------------------------
208#
209#
210#
211#-------------------------------------------------------------------------
212def fix(line):
213    l = line.strip().replace('&','&').replace('>','>')
214    return l.replace(l,'<','&lt;').replace(l,'"','&quot;')
215
216#-------------------------------------------------------------------------
217#
218#
219#
220#-------------------------------------------------------------------------
221class EventComparisonResults(ManagedWindow):
222    def __init__(self,dbstate,uistate,people_list,track):
223        self.dbstate = dbstate
224        self.uistate = uistate
225
226        ManagedWindow.__init__(self, uistate, track, self)
227
228        self.db = dbstate.db
229        self.my_list = people_list
230        self.row_data = []
231        self.save_form = None
232
233        self.topDialog = Glade(toplevel="eventcmp")
234        self.topDialog.connect_signals({
235            "on_write_table"        : self.on_write_table,
236            "destroy_passed_object" : self.close,
237            "on_help_clicked"       : self.on_help_clicked,
238            "on_apply_clicked"      : self.__dummy,
239            "on_editor_clicked"     : self.__dummy,
240            })
241
242        window = self.topDialog.toplevel
243        self.set_window(window, self.topDialog.get_object('title'),
244                        _('Event Comparison Results'))
245        self.setup_configs('interface.eventcomparisonresults', 750, 400)
246
247        self.eventlist = self.topDialog.get_object('treeview')
248        self.sort = Sort(self.db)
249        self.my_list.sort(key=self.sort.by_last_name_key)
250
251        self.event_titles = self.make_event_titles()
252
253        self.table_titles = [_("Person"),_("ID")]
254        for event_name in self.event_titles:
255            self.table_titles.append(_("%(event_name)s Date") %
256                {'event_name' :event_name}
257                )
258            self.table_titles.append('sort') # This won't be shown in a tree
259            self.table_titles.append(_("%(event_name)s Place") %
260                {'event_name' :event_name}
261                )
262
263        self.build_row_data()
264        self.draw_display()
265        self.show()
266
267    def __dummy(self, obj):
268        """dummy callback, needed because widget is in same glade file
269        as another widget, so callbacks must be defined to avoid warnings.
270        """
271        pass
272
273    def on_help_clicked(self, obj):
274        """Display the relevant portion of Gramps manual"""
275        display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC)
276
277    def build_menu_names(self, obj):
278        return (_("Event Comparison Results"),None)
279
280    def draw_display(self):
281
282        model_index = 0
283        tree_index = 0
284        mylist = []
285        renderer = Gtk.CellRendererText()
286        for title in self.table_titles:
287            mylist.append(str)
288            if title == 'sort':
289                # This will override the previously defined column
290                self.eventlist.get_column(
291                    tree_index-1).set_sort_column_id(model_index)
292            else:
293                column = Gtk.TreeViewColumn(title,renderer,text=model_index)
294                column.set_sort_column_id(model_index)
295                self.eventlist.append_column(column)
296                # This one numbers the tree columns: increment on new column
297                tree_index += 1
298            # This one numbers the model columns: always increment
299            model_index += 1
300
301        model = Gtk.ListStore(*mylist)
302        self.eventlist.set_model(model)
303
304        self.progress_bar.set_pass(_('Building display'),len(self.row_data))
305        for data in self.row_data:
306            model.append(row=list(data))
307            self.progress_bar.step()
308        self.progress_bar.close()
309
310    def build_row_data(self):
311        self.progress_bar = ProgressMeter(
312            _('Comparing Events'), '', parent=self.uistate.window)
313        self.progress_bar.set_pass(_('Building data'),len(self.my_list))
314        for individual_id in self.my_list:
315            individual = self.db.get_person_from_handle(individual_id)
316            name = individual.get_primary_name().get_name()
317            gid = individual.get_gramps_id()
318
319            the_map = defaultdict(list)
320            for ievent_ref in individual.get_event_ref_list():
321                ievent = self.db.get_event_from_handle(ievent_ref.ref)
322                event_name = str(ievent.get_type())
323                the_map[event_name].append(ievent_ref.ref)
324
325            first = True
326            done = False
327            while not done:
328                added = False
329                tlist = [name, gid] if first else ["", ""]
330
331                for ename in self.event_titles:
332                    if ename in the_map and len(the_map[ename]) > 0:
333                        event_handle = the_map[ename][0]
334                        del the_map[ename][0]
335                        date = place = ""
336
337                        if event_handle:
338                            event = self.db.get_event_from_handle(event_handle)
339                            date = get_date(event)
340                            sortdate = "%09d" % (
341                                       event.get_date_object().get_sort_value()
342                                       )
343                            place_handle = event.get_place_handle()
344                            if place_handle:
345                                place = self.db.get_place_from_handle(
346                                            place_handle).get_title()
347                        tlist += [date, sortdate, place]
348                        added = True
349                    else:
350                        tlist += [""]*3
351
352                if first:
353                    first = False
354                    self.row_data.append(tlist)
355                elif not added:
356                    done = True
357                else:
358                    self.row_data.append(tlist)
359            self.progress_bar.step()
360
361    def make_event_titles(self):
362        """
363        Create the list of unique event types, along with the person's
364        name, birth, and death.
365        This should be the column titles of the report.
366        """
367        the_map = defaultdict(int)
368        for individual_id in self.my_list:
369            individual = self.db.get_person_from_handle(individual_id)
370            for event_ref in individual.get_event_ref_list():
371                event = self.db.get_event_from_handle(event_ref.ref)
372                name = str(event.get_type())
373                if not name:
374                    break
375                the_map[name] += 1
376
377        unsort_list = sorted([(d, k) for k,d in the_map.items()],
378                             key=lambda x: x[0], reverse=True)
379
380        sort_list = [ item[1] for item in unsort_list ]
381## Presently there's no Birth and Death. Instead there's Birth Date and
382## Birth Place, as well as Death Date and Death Place.
383##         # Move birth and death to the begining of the list
384##         if _("Death") in the_map:
385##             sort_list.remove(_("Death"))
386##             sort_list = [_("Death")] + sort_list
387
388##         if _("Birth") in the_map:
389##             sort_list.remove(_("Birth"))
390##             sort_list = [_("Birth")] + sort_list
391
392        return sort_list
393
394    def on_write_table(self, obj):
395        f = Gtk.FileChooserDialog(_("Select filename"),
396                                  parent=self.window,
397                                  action=Gtk.FileChooserAction.SAVE,
398                                  buttons=(_('_Cancel'),
399                                           Gtk.ResponseType.CANCEL,
400                                           _('_Save'),
401                                           Gtk.ResponseType.OK))
402
403        f.set_current_folder(get_curr_dir())
404        status = f.run()
405        f.hide()
406
407        if status == Gtk.ResponseType.OK:
408            name = f.get_filename()
409            doc = ODSTab(len(self.row_data))
410            doc.creator(self.db.get_researcher().get_name())
411
412            spreadsheet = TableReport(name, doc)
413
414            new_titles = []
415            skip_columns = []
416            index = 0
417            for title in self.table_titles:
418                if title == 'sort':
419                    skip_columns.append(index)
420                else:
421                    new_titles.append(title)
422                index += 1
423            spreadsheet.initialize(len(new_titles))
424
425            spreadsheet.write_table_head(new_titles)
426
427            index = 0
428            for top in self.row_data:
429                spreadsheet.set_row(index%2)
430                index += 1
431                spreadsheet.write_table_data(top,skip_columns)
432
433            spreadsheet.finalize()
434        f.destroy()
435
436#------------------------------------------------------------------------
437#
438#
439#
440#------------------------------------------------------------------------
441class EventComparisonOptions(tool.ToolOptions):
442    """
443    Defines options and provides handling interface.
444    """
445
446    def __init__(self, name,person_id=None):
447        tool.ToolOptions.__init__(self, name,person_id)
448
449        # Options specific for this report
450        self.options_dict = {
451            'filter'   : 0,
452        }
453        filters = utils.get_person_filters(None)
454        self.options_help = {
455            'filter'   : ("=num","Filter number.",
456                          [ filt.get_name() for filt in filters ],
457                          True ),
458        }
459