1# 2# Gramps - a GTK+/GNOME based genealogy program 3# 4# Copyright (C) 2000-2007 Donald N. Allingham 5# Copyright (C) 2008 Brian G. Matherly 6# Copyright (C) 2008 Stephane Charette 7# Copyright (C) 2010 Jakim Friant 8# 9# This program is free software; you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation; either version 2 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program; if not, write to the Free Software 21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22# 23 24"Find unused objects and remove with the user's permission." 25 26#------------------------------------------------------------------------- 27# 28# gtk modules 29# 30#------------------------------------------------------------------------- 31from gi.repository import Gdk 32from gi.repository import Gtk 33from gi.repository import GObject 34 35#------------------------------------------------------------------------- 36# 37# Gramps modules 38# 39#------------------------------------------------------------------------- 40from gramps.gen.db import DbTxn 41from gramps.gen.errors import WindowActiveError 42from gramps.gui.managedwindow import ManagedWindow 43from gramps.gen.datehandler import displayer as _dd 44from gramps.gen.display.place import displayer as _pd 45from gramps.gen.updatecallback import UpdateCallback 46from gramps.gui.plug import tool 47from gramps.gui.glade import Glade 48from gramps.gen.filters import GenericFilterFactory, rules 49from gramps.gen.const import GRAMPS_LOCALE as glocale 50_ = glocale.translation.gettext 51 52#------------------------------------------------------------------------- 53# 54# runTool 55# 56#------------------------------------------------------------------------- 57class RemoveUnused(tool.Tool, ManagedWindow, UpdateCallback): 58 MARK_COL = 0 59 OBJ_ID_COL = 1 60 OBJ_NAME_COL = 2 61 OBJ_TYPE_COL = 3 62 OBJ_HANDLE_COL = 4 63 64 BUSY_CURSOR = Gdk.Cursor.new_for_display(Gdk.Display.get_default(), 65 Gdk.CursorType.WATCH) 66 67 def __init__(self, dbstate, user, options_class, name, callback=None): 68 uistate = user.uistate 69 self.title = _('Unused Objects') 70 71 tool.Tool.__init__(self, dbstate, options_class, name) 72 73 if self.db.readonly: 74 return 75 76 ManagedWindow.__init__(self, uistate, [], self.__class__) 77 UpdateCallback.__init__(self, self.uistate.pulse_progressbar) 78 79 self.dbstate = dbstate 80 self.uistate = uistate 81 82 self.tables = { 83 'events': {'get_func': self.db.get_event_from_handle, 84 'remove': self.db.remove_event, 85 'get_text': self.get_event_text, 86 'editor': 'EditEvent', 87 'icon': 'gramps-event', 88 'name_ix': 4}, 89 'sources': {'get_func': self.db.get_source_from_handle, 90 'remove': self.db.remove_source, 91 'get_text': None, 92 'editor': 'EditSource', 93 'icon': 'gramps-source', 94 'name_ix': 2}, 95 'citations': {'get_func': self.db.get_citation_from_handle, 96 'remove': self.db.remove_citation, 97 'get_text': None, 98 'editor': 'EditCitation', 99 'icon': 'gramps-citation', 100 'name_ix': 3}, 101 'places': {'get_func': self.db.get_place_from_handle, 102 'remove': self.db.remove_place, 103 'get_text': self.get_place_text, 104 'editor': 'EditPlace', 105 'icon': 'gramps-place', 106 'name_ix': 2}, 107 'media': {'get_func': self.db.get_media_from_handle, 108 'remove': self.db.remove_media, 109 'get_text': None, 110 'editor': 'EditMedia', 111 'icon': 'gramps-media', 112 'name_ix': 4}, 113 'repos': {'get_func': self.db.get_repository_from_handle, 114 'remove': self.db.remove_repository, 115 'get_text': None, 116 'editor': 'EditRepository', 117 'icon': 'gramps-repository', 118 'name_ix': 3}, 119 'notes': {'get_func': self.db.get_note_from_handle, 120 'remove': self.db.remove_note, 121 'get_text': self.get_note_text, 122 'editor': 'EditNote', 123 'icon': 'gramps-notes', 124 'name_ix': 2}, 125 } 126 127 self.init_gui() 128 129 def init_gui(self): 130 self.top = Glade() 131 window = self.top.toplevel 132 self.set_window(window, self.top.get_object('title'), self.title) 133 self.setup_configs('interface.removeunused', 400, 520) 134 135 self.events_box = self.top.get_object('events_box') 136 self.sources_box = self.top.get_object('sources_box') 137 self.citations_box = self.top.get_object('citations_box') 138 self.places_box = self.top.get_object('places_box') 139 self.media_box = self.top.get_object('media_box') 140 self.repos_box = self.top.get_object('repos_box') 141 self.notes_box = self.top.get_object('notes_box') 142 self.find_button = self.top.get_object('find_button') 143 self.remove_button = self.top.get_object('remove_button') 144 145 self.events_box.set_active(self.options.handler.options_dict['events']) 146 self.sources_box.set_active( 147 self.options.handler.options_dict['sources']) 148 self.citations_box.set_active( 149 self.options.handler.options_dict['citations']) 150 self.places_box.set_active( 151 self.options.handler.options_dict['places']) 152 self.media_box.set_active(self.options.handler.options_dict['media']) 153 self.repos_box.set_active(self.options.handler.options_dict['repos']) 154 self.notes_box.set_active(self.options.handler.options_dict['notes']) 155 156 self.warn_tree = self.top.get_object('warn_tree') 157 self.warn_tree.connect('button_press_event', self.double_click) 158 159 self.selection = self.warn_tree.get_selection() 160 161 self.mark_button = self.top.get_object('mark_button') 162 self.mark_button.connect('clicked', self.mark_clicked) 163 164 self.unmark_button = self.top.get_object('unmark_button') 165 self.unmark_button.connect('clicked', self.unmark_clicked) 166 167 self.invert_button = self.top.get_object('invert_button') 168 self.invert_button.connect('clicked', self.invert_clicked) 169 170 self.real_model = Gtk.ListStore(GObject.TYPE_BOOLEAN, 171 GObject.TYPE_STRING, 172 GObject.TYPE_STRING, 173 GObject.TYPE_STRING, 174 GObject.TYPE_STRING) 175 # a short term Gtk introspection means we need to try both ways: 176 if hasattr(self.real_model, "sort_new_with_model"): 177 self.sort_model = self.real_model.sort_new_with_model() 178 else: 179 self.sort_model = Gtk.TreeModelSort.new_with_model(self.real_model) 180 self.warn_tree.set_model(self.sort_model) 181 182 self.renderer = Gtk.CellRendererText() 183 self.img_renderer = Gtk.CellRendererPixbuf() 184 self.bool_renderer = Gtk.CellRendererToggle() 185 self.bool_renderer.connect('toggled', self.selection_toggled) 186 187 # Add mark column 188 mark_column = Gtk.TreeViewColumn(_('Mark'), self.bool_renderer, 189 active=RemoveUnused.MARK_COL) 190 mark_column.set_sort_column_id(RemoveUnused.MARK_COL) 191 self.warn_tree.append_column(mark_column) 192 193 # Add image column 194 img_column = Gtk.TreeViewColumn(None, self.img_renderer) 195 img_column.set_cell_data_func(self.img_renderer, self.get_image) 196 self.warn_tree.append_column(img_column) 197 198 # Add column with object gramps_id 199 id_column = Gtk.TreeViewColumn(_('ID'), self.renderer, 200 text=RemoveUnused.OBJ_ID_COL) 201 id_column.set_sort_column_id(RemoveUnused.OBJ_ID_COL) 202 self.warn_tree.append_column(id_column) 203 204 # Add column with object name 205 name_column = Gtk.TreeViewColumn(_('Name'), self.renderer, 206 text=RemoveUnused.OBJ_NAME_COL) 207 name_column.set_sort_column_id(RemoveUnused.OBJ_NAME_COL) 208 self.warn_tree.append_column(name_column) 209 210 self.top.connect_signals({ 211 "destroy_passed_object" : self.close, 212 "on_remove_button_clicked": self.do_remove, 213 "on_find_button_clicked" : self.find, 214 "on_delete_event" : self.close, 215 }) 216 217 self.dc_label = self.top.get_object('dc_label') 218 219 self.sensitive_list = [self.warn_tree, self.mark_button, 220 self.unmark_button, self.invert_button, 221 self.dc_label, self.remove_button] 222 223 for item in self.sensitive_list: 224 item.set_sensitive(False) 225 226 self.show() 227 228 def build_menu_names(self, obj): 229 return (self.title, None) 230 231 def find(self, obj): 232 self.options.handler.options_dict.update( 233 events=self.events_box.get_active(), 234 sources=self.sources_box.get_active(), 235 citations=self.citations_box.get_active(), 236 places=self.places_box.get_active(), 237 media=self.media_box.get_active(), 238 repos=self.repos_box.get_active(), 239 notes=self.notes_box.get_active(), 240 ) 241 242 for item in self.sensitive_list: 243 item.set_sensitive(True) 244 245 self.uistate.set_busy_cursor(True) 246 self.uistate.progress.show() 247 self.window.get_window().set_cursor(self.BUSY_CURSOR) 248 249 self.real_model.clear() 250 self.collect_unused() 251 252 self.uistate.progress.hide() 253 self.uistate.set_busy_cursor(False) 254 self.window.get_window().set_cursor(None) 255 self.reset() 256 257 # Save options 258 self.options.handler.save_options() 259 260 def collect_unused(self): 261 # Run through all requested tables and check all objects 262 # for being referenced some place. If not, add_results on them. 263 264 db = self.db 265 tables = ( 266 ('events', db.get_event_cursor, db.get_number_of_events), 267 ('sources', db.get_source_cursor, db.get_number_of_sources), 268 ('citations', db.get_citation_cursor, db.get_number_of_citations), 269 ('places', db.get_place_cursor, db.get_number_of_places), 270 ('media', db.get_media_cursor, db.get_number_of_media), 271 ('repos', db.get_repository_cursor, db.get_number_of_repositories), 272 ('notes', db.get_note_cursor, db.get_number_of_notes), 273 ) 274 275 # bug 7619 : don't select notes from to do list. 276 # notes associated to the todo list doesn't have references. 277 # get the todo list (from get_note_list method of the todo gramplet ) 278 all_notes = self.dbstate.db.get_note_handles() 279 FilterClass = GenericFilterFactory('Note') 280 filter1 = FilterClass() 281 filter1.add_rule(rules.note.HasType(["To Do"])) 282 todo_list = filter1.apply(self.dbstate.db, all_notes) 283 filter2 = FilterClass() 284 filter2.add_rule(rules.note.HasType(["Link"])) 285 link_list = filter2.apply(self.dbstate.db, all_notes) 286 287 for (the_type, cursor_func, total_func) in tables: 288 if not self.options.handler.options_dict[the_type]: 289 # This table was not requested. Skip it. 290 continue 291 292 with cursor_func() as cursor: 293 self.set_total(total_func()) 294 fbh = db.find_backlink_handles 295 for handle, data in cursor: 296 if not any(h for h in fbh(handle)): 297 if handle not in todo_list and handle not in link_list: 298 self.add_results((the_type, handle, data)) 299 self.update() 300 self.reset() 301 302 def do_remove(self, obj): 303 with DbTxn(_("Remove unused objects"), self.db, batch=False) as trans: 304 self.db.disable_signals() 305 306 for row_num in range(len(self.real_model)-1, -1, -1): 307 path = (row_num,) 308 row = self.real_model[path] 309 if not row[RemoveUnused.MARK_COL]: 310 continue 311 312 the_type = row[RemoveUnused.OBJ_TYPE_COL] 313 handle = row[RemoveUnused.OBJ_HANDLE_COL] 314 remove_func = self.tables[the_type]['remove'] 315 remove_func(handle, trans) 316 317 self.real_model.remove(row.iter) 318 319 self.db.enable_signals() 320 self.db.request_rebuild() 321 322 def selection_toggled(self, cell, path_string): 323 sort_path = tuple(map(int, path_string.split(':'))) 324 real_path = self.sort_model.convert_path_to_child_path(Gtk.TreePath(sort_path)) 325 row = self.real_model[real_path] 326 row[RemoveUnused.MARK_COL] = not row[RemoveUnused.MARK_COL] 327 self.real_model.row_changed(real_path, row.iter) 328 329 def mark_clicked(self, mark_button): 330 for row_num in range(len(self.real_model)): 331 path = (row_num,) 332 row = self.real_model[path] 333 row[RemoveUnused.MARK_COL] = True 334 335 def unmark_clicked(self, unmark_button): 336 for row_num in range(len(self.real_model)): 337 path = (row_num,) 338 row = self.real_model[path] 339 row[RemoveUnused.MARK_COL] = False 340 341 def invert_clicked(self, invert_button): 342 for row_num in range(len(self.real_model)): 343 path = (row_num,) 344 row = self.real_model[path] 345 row[RemoveUnused.MARK_COL] = not row[RemoveUnused.MARK_COL] 346 347 def double_click(self, obj, event): 348 if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS 349 and event.button == 1): 350 (model, node) = self.selection.get_selected() 351 if not node: 352 return 353 sort_path = self.sort_model.get_path(node) 354 real_path = self.sort_model.convert_path_to_child_path(sort_path) 355 row = self.real_model[real_path] 356 the_type = row[RemoveUnused.OBJ_TYPE_COL] 357 handle = row[RemoveUnused.OBJ_HANDLE_COL] 358 self.call_editor(the_type, handle) 359 360 def call_editor(self, the_type, handle): 361 try: 362 obj = self.tables[the_type]['get_func'](handle) 363 editor_str = 'from gramps.gui.editors import %s as editor' % ( 364 self.tables[the_type]['editor']) 365 exec(editor_str, globals()) 366 editor(self.dbstate, self.uistate, [], obj) 367 except WindowActiveError: 368 pass 369 370 def get_image(self, column, cell, model, iter, user_data=None): 371 the_type = model.get_value(iter, RemoveUnused.OBJ_TYPE_COL) 372 the_icon = self.tables[the_type]['icon'] 373 cell.set_property('icon-name', the_icon) 374 375 def add_results(self, results): 376 (the_type, handle, data) = results 377 gramps_id = data[1] 378 379 # if we have a function that will return to us some type 380 # of text summary, then we should use it; otherwise we'll 381 # use the generic field index provided in the tables above 382 if self.tables[the_type]['get_text']: 383 text = self.tables[the_type]['get_text'](the_type, handle, data) 384 else: 385 # grab the text field index we know about, and hope 386 # it represents something useful to the user 387 name_ix = self.tables[the_type]['name_ix'] 388 text = data[name_ix] 389 390 # insert a new row into the table 391 self.real_model.append(row=[False, gramps_id, text, the_type, handle]) 392 393 def get_event_text(self, the_type, handle, data): 394 """ 395 Come up with a short line of text that we can use as 396 a summary to represent this event. 397 """ 398 399 # get the event: 400 event = self.tables[the_type]['get_func'](handle) 401 402 # first check to see if the event has a descriptive name 403 text = event.get_description() # (this is rarely set for events) 404 405 # if we don't have a description... 406 if text == '': 407 # ... then we merge together several fields 408 409 # get the event type (marriage, birth, death, etc.) 410 text = str(event.get_type()) 411 412 # see if there is a date 413 date = _dd.display(event.get_date_object()) 414 if date != '': 415 text += '; %s' % date 416 417 # see if there is a place 418 if event.get_place_handle(): 419 text += '; %s' % _pd.display_event(self.db, event) 420 421 return text 422 423 def get_note_text(self, the_type, handle, data): 424 """ 425 We need just the first few words of a note as a summary. 426 """ 427 # get the note object 428 note = self.tables[the_type]['get_func'](handle) 429 430 # get the note text; this ignores (discards) formatting 431 text = note.get() 432 433 # convert whitespace to a single space 434 text = " ".join(text.split()) 435 436 # if the note is too long, truncate it 437 if len(text) > 80: 438 text = text[:80] + "..." 439 440 return text 441 442 def get_place_text(self, the_type, handle, data): 443 """ 444 We need just the place name. 445 """ 446 # get the place object 447 place = self.tables[the_type]['get_func'](handle) 448 449 # get the name 450 text = place.get_name().get_value() 451 452 return text 453#------------------------------------------------------------------------ 454# 455# 456# 457#------------------------------------------------------------------------ 458class CheckOptions(tool.ToolOptions): 459 """ 460 Defines options and provides handling interface. 461 """ 462 463 def __init__(self, name, person_id=None): 464 tool.ToolOptions.__init__(self, name, person_id) 465 466 # Options specific for this report 467 self.options_dict = { 468 'events': 1, 469 'sources': 1, 470 'citations': 1, 471 'places': 1, 472 'media': 1, 473 'repos': 1, 474 'notes': 1, 475 } 476 self.options_help = { 477 'events': ("=0/1", "Whether to use check for unused events", 478 ["Do not check events", "Check events"], 479 True), 480 'sources': ("=0/1", "Whether to use check for unused sources", 481 ["Do not check sources", "Check sources"], 482 True), 483 'citations': ("=0/1", "Whether to use check for unused citations", 484 ["Do not check citations", "Check citations"], 485 True), 486 'places': ("=0/1", "Whether to use check for unused places", 487 ["Do not check places", "Check places"], 488 True), 489 'media': ("=0/1", "Whether to use check for unused media", 490 ["Do not check media", "Check media"], 491 True), 492 'repos': ("=0/1", "Whether to use check for unused repositories", 493 ["Do not check repositories", "Check repositories"], 494 True), 495 'notes': ("=0/1", "Whether to use check for unused notes", 496 ["Do not check notes", "Check notes"], 497 True), 498 } 499