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,'<','<').replace(l,'"','"') 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