# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2000-2007 Donald N. Allingham # Copyright (C) 2008 Brian G. Matherly # Copyright (C) 2008 Stephane Charette # Copyright (C) 2010 Jakim Friant # # 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # $Id$ "Find unused objects and remove with the user's permission." #------------------------------------------------------------------------- # # python modules # #------------------------------------------------------------------------- from __future__ import with_statement from gen.ggettext import gettext as _ #------------------------------------------------------------------------ # # Set up logging # #------------------------------------------------------------------------ import logging log = logging.getLogger(".RemoveUnused") #------------------------------------------------------------------------- # # gtk modules # #------------------------------------------------------------------------- import gtk import gobject #------------------------------------------------------------------------- # # GRAMPS modules # #------------------------------------------------------------------------- import Errors import ManagedWindow from DateHandler import displayer as _dd from gen.updatecallback import UpdateCallback from gui.plug import tool from glade import Glade #------------------------------------------------------------------------- # # runTool # #------------------------------------------------------------------------- class RemoveUnused(tool.Tool, ManagedWindow.ManagedWindow, UpdateCallback): MARK_COL = 0 OBJ_ID_COL = 1 OBJ_NAME_COL = 2 OBJ_TYPE_COL = 3 OBJ_HANDLE_COL = 4 def __init__(self, dbstate, uistate, options_class, name, callback=None): self.title = _('Unused Objects') tool.Tool.__init__(self, dbstate, options_class, name) if self.db.readonly: return ManagedWindow.ManagedWindow.__init__(self, uistate,[], self.__class__) UpdateCallback.__init__(self, self.uistate.pulse_progressbar) self.dbstate = dbstate self.uistate = uistate self.tables = { 'events' : {'get_func': self.db.get_event_from_handle, 'remove' : self.db.remove_event, 'get_text': self.get_event_text, 'editor' : 'EditEvent', 'stock' : 'gramps-event', 'name_ix' : 4}, 'sources' : {'get_func': self.db.get_source_from_handle, 'remove' : self.db.remove_source, 'get_text': None, 'editor' : 'EditSource', 'stock' : 'gramps-source', 'name_ix' : 2}, 'places' : {'get_func': self.db.get_place_from_handle, 'remove' : self.db.remove_place, 'get_text': None, 'editor' : 'EditPlace', 'stock' : 'gramps-place', 'name_ix' : 2}, 'media' : {'get_func': self.db.get_object_from_handle, 'remove' : self.db.remove_object, 'get_text': None, 'editor' : 'EditMedia', 'stock' : 'gramps-media', 'name_ix' : 4}, 'repos' : {'get_func': self.db.get_repository_from_handle, 'remove' : self.db.remove_repository, 'get_text': None, 'editor' : 'EditRepository', 'stock' : 'gramps-repository', 'name_ix' : 3}, 'notes' : {'get_func': self.db.get_note_from_handle, 'remove' : self.db.remove_note, 'get_text': self.get_note_text, 'editor' : 'EditNote', 'stock' : 'gramps-notes', 'name_ix' : 2}, } self.init_gui() def init_gui(self): self.top = Glade() window = self.top.toplevel self.set_window(window, self.top.get_object('title'), self.title) self.events_box = self.top.get_object('events_box') self.sources_box = self.top.get_object('sources_box') self.places_box = self.top.get_object('places_box') self.media_box = self.top.get_object('media_box') self.repos_box = self.top.get_object('repos_box') self.notes_box = self.top.get_object('notes_box') self.find_button = self.top.get_object('find_button') self.remove_button = self.top.get_object('remove_button') self.events_box.set_active(self.options.handler.options_dict['events']) self.sources_box.set_active( self.options.handler.options_dict['sources']) self.places_box.set_active( self.options.handler.options_dict['places']) self.media_box.set_active(self.options.handler.options_dict['media']) self.repos_box.set_active(self.options.handler.options_dict['repos']) self.notes_box.set_active(self.options.handler.options_dict['notes']) self.warn_tree = self.top.get_object('warn_tree') self.warn_tree.connect('button_press_event', self.double_click) self.selection = self.warn_tree.get_selection() self.mark_button = self.top.get_object('mark_button') self.mark_button.connect('clicked', self.mark_clicked) self.unmark_button = self.top.get_object('unmark_button') self.unmark_button.connect('clicked', self.unmark_clicked) self.invert_button = self.top.get_object('invert_button') self.invert_button.connect('clicked', self.invert_clicked) self.real_model = gtk.ListStore(gobject.TYPE_BOOLEAN, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING) self.sort_model = gtk.TreeModelSort(self.real_model) self.warn_tree.set_model(self.sort_model) self.renderer = gtk.CellRendererText() self.img_renderer = gtk.CellRendererPixbuf() self.bool_renderer = gtk.CellRendererToggle() self.bool_renderer.connect('toggled', self.selection_toggled) # Add mark column mark_column = gtk.TreeViewColumn(_('Mark'), self.bool_renderer, active=RemoveUnused.MARK_COL) mark_column.set_sort_column_id(RemoveUnused.MARK_COL) self.warn_tree.append_column(mark_column) # Add image column img_column = gtk.TreeViewColumn(None, self.img_renderer ) img_column.set_cell_data_func(self.img_renderer, self.get_image) self.warn_tree.append_column(img_column) # Add column with object gramps_id id_column = gtk.TreeViewColumn(_('ID'), self.renderer, text=RemoveUnused.OBJ_ID_COL) id_column.set_sort_column_id(RemoveUnused.OBJ_ID_COL) self.warn_tree.append_column(id_column) # Add column with object name name_column = gtk.TreeViewColumn(_('Name'), self.renderer, text=RemoveUnused.OBJ_NAME_COL) name_column.set_sort_column_id(RemoveUnused.OBJ_NAME_COL) self.warn_tree.append_column(name_column) self.top.connect_signals({ "destroy_passed_object" : self.close, "on_remove_button_clicked": self.do_remove, "on_find_button_clicked" : self.find, "on_delete_event" : self.close, }) self.dc_label = self.top.get_object('dc_label') self.sensitive_list = [self.warn_tree, self.mark_button, self.unmark_button, self.invert_button, self.dc_label, self.remove_button] for item in self.sensitive_list: item.set_sensitive(False) self.show() def build_menu_names(self, obj): return (self.title, None) def find(self, obj): self.options.handler.options_dict.update( events = self.events_box.get_active(), sources = self.sources_box.get_active(), places = self.places_box.get_active(), media = self.media_box.get_active(), repos = self.repos_box.get_active(), notes = self.notes_box.get_active(), ) for item in self.sensitive_list: item.set_sensitive(True) self.uistate.window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) self.uistate.progress.show() self.window.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) self.real_model.clear() self.collect_unused() self.uistate.progress.hide() self.uistate.window.window.set_cursor(None) self.window.window.set_cursor(None) self.reset() # Save options self.options.handler.save_options() def collect_unused(self): # Run through all requested tables and check all objects # for being referenced some place. If not, add_results on them. db = self.db tables = ( ('events', db.get_event_cursor, db.get_number_of_events), ('sources', db.get_source_cursor, db.get_number_of_sources), ('places', db.get_place_cursor, db.get_number_of_places), ('media', db.get_media_cursor, db.get_number_of_media_objects), ('repos', db.get_repository_cursor, db.get_number_of_repositories), ('notes', db.get_note_cursor, db.get_number_of_notes), ) for (the_type, cursor_func, total_func) in tables: if not self.options.handler.options_dict[the_type]: # This table was not requested. Skip it. continue with cursor_func() as cursor: self.set_total(total_func()) fbh = db.find_backlink_handles for handle, data in cursor: if not any(h for h in fbh(handle)): self.add_results((the_type, handle, data)) self.update() self.reset() def do_remove(self, obj): with self.db.transaction_begin(_("Remove unused objects"), batch=False) as trans: self.db.disable_signals() for row_num in range(len(self.real_model)-1, -1, -1): path = (row_num,) row = self.real_model[path] if not row[RemoveUnused.MARK_COL]: continue the_type = row[RemoveUnused.OBJ_TYPE_COL] handle = row[RemoveUnused.OBJ_HANDLE_COL] remove_func = self.tables[the_type]['remove'] remove_func(handle, trans) self.real_model.remove(row.iter) self.db.enable_signals() self.db.request_rebuild() def selection_toggled(self, cell, path_string): sort_path = tuple(map(int, path_string.split(':'))) real_path = self.sort_model.convert_path_to_child_path(sort_path) row = self.real_model[real_path] row[RemoveUnused.MARK_COL] = not row[RemoveUnused.MARK_COL] self.real_model.row_changed(real_path, row.iter) def mark_clicked(self, mark_button): for row_num in range(len(self.real_model)): path = (row_num,) row = self.real_model[path] row[RemoveUnused.MARK_COL] = True def unmark_clicked(self, unmark_button): for row_num in range(len(self.real_model)): path = (row_num,) row = self.real_model[path] row[RemoveUnused.MARK_COL] = False def invert_clicked(self, invert_button): for row_num in range(len(self.real_model)): path = (row_num,) row = self.real_model[path] row[RemoveUnused.MARK_COL] = not row[RemoveUnused.MARK_COL] def double_click(self, obj, event): if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1: (model, node) = self.selection.get_selected() if not node: return sort_path = self.sort_model.get_path(node) real_path = self.sort_model.convert_path_to_child_path(sort_path) row = self.real_model[real_path] the_type = row[RemoveUnused.OBJ_TYPE_COL] handle = row[RemoveUnused.OBJ_HANDLE_COL] self.call_editor(the_type, handle) def call_editor(self, the_type, handle): try: obj = self.tables[the_type]['get_func'](handle) editor_str = 'from gui.editors import %s as editor' % ( self.tables[the_type]['editor'] ) exec(editor_str) editor(self.dbstate, self.uistate, [], obj) except Errors.WindowActiveError: pass def get_image(self, column, cell, model, iter, user_data=None): the_type = model.get_value(iter, RemoveUnused.OBJ_TYPE_COL) the_stock = self.tables[the_type]['stock'] cell.set_property('stock-id', the_stock) def add_results(self, results): (the_type, handle, data) = results gramps_id = data[1] # if we have a function that will return to us some type # of text summary, then we should use it; otherwise we'll # use the generic field index provided in the tables above if self.tables[the_type]['get_text']: text = self.tables[the_type]['get_text'](the_type, handle, data) else: # grab the text field index we know about, and hope # it represents something useful to the user name_ix = self.tables[the_type]['name_ix'] text = data[name_ix] # insert a new row into the table self.real_model.append(row=[False, gramps_id, text, the_type, handle]) def get_event_text(self, the_type, handle, data): """ Come up with a short line of text that we can use as a summary to represent this event. """ # get the event: event = self.tables[the_type]['get_func'](handle) # first check to see if the event has a descriptive name text = event.get_description() # (this is rarely set for events) # if we don't have a description... if text == '': # ... then we merge together several fields # get the event type (marriage, birth, death, etc.) text = str(event.get_type()) # see if there is a date date = _dd.display(event.get_date_object()) if date != '': text += '; %s' % date # see if there is a place place_handle = event.get_place_handle() if place_handle: place = self.db.get_place_from_handle(place_handle) text += '; %s' % place.get_title() return text def get_note_text(self, the_type, handle, data): """ We need just the first few words of a note as a summary. """ # get the note object note = self.tables[the_type]['get_func'](handle) # get the note text; this ignores (discards) formatting text = note.get() # convert whitespace to a single space text = u" ".join(text.split()) # if the note is too long, truncate it if len(text) > 80: text = text[:80] + "..." return text #------------------------------------------------------------------------ # # # #------------------------------------------------------------------------ class CheckOptions(tool.ToolOptions): """ Defines options and provides handling interface. """ def __init__(self, name, person_id=None): tool.ToolOptions.__init__(self, name, person_id) # Options specific for this report self.options_dict = { 'events' : 1, 'sources' : 1, 'places' : 1, 'media' : 1, 'repos' : 1, 'notes' : 1, } self.options_help = { 'events' : ("=0/1","Whether to use check for unused events", ["Do not check events","Check events"], True), 'sources' : ("=0/1","Whether to use check for unused sources", ["Do not check sources","Check sources"], True), 'places' : ("=0/1","Whether to use check for unused places", ["Do not check places","Check places"], True), 'media' : ("=0/1","Whether to use check for unused media", ["Do not check media","Check media"], True), 'repos' : ("=0/1","Whether to use check for unused repositories", ["Do not check repositories","Check repositories"], True), 'notes' : ("=0/1","Whether to use check for unused notes", ["Do not check notes","Check notes"], True), }