Fix for: 1277: database corroption on delete outside of DisplayTabs while tab open
Introduces the concept of callman.py as one single way to follow handles an interface is interested in. dbguielement.py contains a small base class using that, usable for all windows/ guielements that need to track database changes to handles svn: r12881
This commit is contained in:
parent
f34d4656a7
commit
ee69317b62
@ -56,11 +56,10 @@ src/cli/grampscli.py
|
||||
src/gen/__init__.py
|
||||
|
||||
# gen utils API
|
||||
src/gen/utils/dbutils.py
|
||||
src/gen/utils/progressmon.py
|
||||
src/gen/utils/__init__.py
|
||||
src/gen/utils/dbutils.py
|
||||
src/gen/utils/callback.py
|
||||
src/gen/utils/callman.py
|
||||
src/gen/utils/dbutils.py
|
||||
src/gen/utils/longop.py
|
||||
src/gen/utils/progressmon.py
|
||||
|
||||
@ -182,6 +181,7 @@ src/gen/plug/docbackend/docbackend.py
|
||||
|
||||
# gui - GUI code
|
||||
src/gui/__init__.py
|
||||
src/gui/dbguielement.py
|
||||
src/gui/dbloader.py
|
||||
src/gui/dbman.py
|
||||
src/gui/grampsgui.py
|
||||
|
@ -495,7 +495,8 @@ class EmbeddedList(ButtonTab):
|
||||
"""
|
||||
The view must be remade when data changes outside this tab.
|
||||
Use this method to connect to after a db change. It makes sure the
|
||||
data is obtained again from db and the view rebuild
|
||||
data is obtained again from the present object and the db what is not
|
||||
present in the obj, and the view rebuild
|
||||
"""
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
@ -39,13 +39,14 @@ import Errors
|
||||
from DdTargets import DdTargets
|
||||
from _GroupEmbeddedList import GroupEmbeddedList
|
||||
from _EventRefModel import EventRefModel
|
||||
from gui.dbguielement import DbGUIElement
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# EventEmbedList
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class EventEmbedList(GroupEmbeddedList):
|
||||
class EventEmbedList(DbGUIElement, GroupEmbeddedList):
|
||||
|
||||
_HANDLE_COL = 7
|
||||
_DND_TYPE = DdTargets.EVENTREF
|
||||
@ -86,9 +87,55 @@ class EventEmbedList(GroupEmbeddedList):
|
||||
self.obj = obj
|
||||
self._groups = []
|
||||
self._data = []
|
||||
DbGUIElement.__init__(self, dbstate.db)
|
||||
GroupEmbeddedList.__init__(self, dbstate, uistate, track, _('_Events'),
|
||||
build_model, share_button=True,
|
||||
move_buttons=True)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
called on init of DbGUIElement, connect to db as required.
|
||||
"""
|
||||
#note: event-rebuild closes the editors, so no need to connect to it
|
||||
self.callman.register_callbacks(
|
||||
{'event-update': self.event_change, #change to an event we track
|
||||
'event-delete': self.event_delete, #delete of event we track
|
||||
})
|
||||
self.callman.connect_all(keys=['event'])
|
||||
|
||||
def event_change(self, *obj):
|
||||
"""
|
||||
Callback method called when a tracked event changes (description
|
||||
changes, source added, ...)
|
||||
Note that adding an event
|
||||
"""
|
||||
self.rebuild_callback()
|
||||
|
||||
def event_delete(self, obj):
|
||||
"""
|
||||
Callback method called when a tracked event is deleted.
|
||||
There are two possibilities:
|
||||
* a tracked non-workgroup event is deleted, just rebuilding the view
|
||||
will correct this.
|
||||
* a workgroup event is deleted. The event must be removed from the obj
|
||||
so that no inconsistent data is shown.
|
||||
"""
|
||||
for handle in obj:
|
||||
refs = self.get_data()[self._WORKGROUP]
|
||||
ref_list = [eref.ref for eref in refs]
|
||||
indexlist = []
|
||||
last = 0
|
||||
while True:
|
||||
try:
|
||||
last = ref_list.index(handle)
|
||||
indexlist.append(last)
|
||||
except ValueError:
|
||||
break
|
||||
#remove the deleted workgroup events from the object
|
||||
for index in indexlist.reverse():
|
||||
del refs[index]
|
||||
#now rebuild the display tab
|
||||
self.rebuild_callback()
|
||||
|
||||
def get_ref_editor(self):
|
||||
from Editors import EditFamilyEventRef
|
||||
@ -118,6 +165,10 @@ class EventEmbedList(GroupEmbeddedList):
|
||||
if mdata:
|
||||
self._groups.append((mhandle, self._MOTHNAME))
|
||||
self._data.append(mdata)
|
||||
#we register all events that need to be tracked
|
||||
for group in self._data:
|
||||
self.callman.register_handles(
|
||||
{'event': [eref.ref for eref in group]})
|
||||
self.changed = False
|
||||
|
||||
return self._data
|
||||
@ -195,10 +246,17 @@ class EventEmbedList(GroupEmbeddedList):
|
||||
def object_added(self, reference, primary):
|
||||
reference.ref = primary.handle
|
||||
self.get_data()[self._WORKGROUP].append(reference)
|
||||
self.callman.register_handles({'event': [primary.handle]})
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
def object_edited(self, ref, event):
|
||||
"""
|
||||
Called as callback after eventref has been edited.
|
||||
Note that if the event changes too (so not only the ref data), then
|
||||
an event-update signal from the database will also be raised, and the
|
||||
rebuild done here will not be needed. There is no way to avoid this ...
|
||||
"""
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
|
@ -46,6 +46,7 @@ import gobject
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gui.utils import open_file_with_default_application
|
||||
from gui.dbguielement import DbGUIElement
|
||||
import gen.lib
|
||||
import Utils
|
||||
import ThumbNails
|
||||
@ -67,7 +68,7 @@ def make_launcher(path):
|
||||
# GalleryTab
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class GalleryTab(ButtonTab):
|
||||
class GalleryTab(ButtonTab, DbGUIElement):
|
||||
|
||||
_DND_TYPE = DdTargets.MEDIAREF
|
||||
_DND_EXTRA = DdTargets.URI_LIST
|
||||
@ -75,8 +76,11 @@ class GalleryTab(ButtonTab):
|
||||
def __init__(self, dbstate, uistate, track, media_list, update=None):
|
||||
self.iconlist = gtk.IconView()
|
||||
ButtonTab.__init__(self, dbstate, uistate, track, _('_Gallery'), True)
|
||||
DbGUIElement.__init__(self, dbstate.db)
|
||||
self.track_ref_for_deletion("iconlist")
|
||||
self.media_list = media_list
|
||||
self.callman.register_handles({'media': [mref.ref for mref
|
||||
in self.media_list]})
|
||||
self.update = update
|
||||
|
||||
self._set_dnd()
|
||||
@ -84,11 +88,16 @@ class GalleryTab(ButtonTab):
|
||||
self.rebuild()
|
||||
self.show_all()
|
||||
|
||||
def connect_db_signals(self):
|
||||
#connect external remove/change of object to rebuild of grampstab
|
||||
self._add_db_signal('media-delete', self.media_delete)
|
||||
self._add_db_signal('media-rebuild', self.rebuild)
|
||||
self._add_db_signal('media-update', self.media_update)
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Implement base class DbGUIElement method
|
||||
"""
|
||||
#note: media-rebuild closes the editors, so no need to connect to it
|
||||
self.callman.register_callbacks(
|
||||
{'media-delete': self.media_delete, # delete a mediaobj we track
|
||||
'media-update': self.media_update, # change a mediaobj we track
|
||||
})
|
||||
self.callman.connect_all(keys=['media'])
|
||||
|
||||
def double_click(self, obj, event):
|
||||
"""
|
||||
@ -259,12 +268,13 @@ class GalleryTab(ButtonTab):
|
||||
def add_callback(self, media_ref, media):
|
||||
media_ref.ref = media.handle
|
||||
self.get_data().append(media_ref)
|
||||
self.callman.register_handles({'media': [media.handle]})
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
def share_button_clicked(self, obj):
|
||||
"""
|
||||
Function called when the Add button is clicked.
|
||||
Function called when the Share button is clicked.
|
||||
|
||||
This function should be overridden by the derived class.
|
||||
|
||||
|
@ -73,8 +73,6 @@ class GrampsTab(gtk.VBox):
|
||||
self.changed = False
|
||||
self.__refs_for_deletion = []
|
||||
|
||||
self._add_db_signal = None
|
||||
|
||||
# save name used for notebook label, and build the widget used
|
||||
# for the label
|
||||
|
||||
@ -168,19 +166,6 @@ class GrampsTab(gtk.VBox):
|
||||
return
|
||||
return True
|
||||
|
||||
def add_db_signal_callback(self, add_db_signal):
|
||||
"""
|
||||
The grampstab must be able to react to database signals, however
|
||||
on destroy of the editor to which the tab is attached, these signals
|
||||
must be disconnected.
|
||||
This method sets the method with which to add database signals on tabs,
|
||||
typically EditPrimary and EditSecondary add tabs, and have methods to
|
||||
connect signals and register them so they are correctly disconnected
|
||||
on close
|
||||
"""
|
||||
self._add_db_signal = add_db_signal
|
||||
self.connect_db_signals()
|
||||
|
||||
def _set_label(self, show_image=True):
|
||||
"""
|
||||
Updates the label based of if the tab contains information. Tabs
|
||||
@ -208,14 +193,6 @@ class GrampsTab(gtk.VBox):
|
||||
can be used to add widgets to the interface.
|
||||
"""
|
||||
pass
|
||||
|
||||
def connect_db_signals(self):
|
||||
"""
|
||||
Function to connect db signals to GrampsTab methods. This function
|
||||
should be overridden in the derived class.
|
||||
It is called after the interface is build.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_parent_notebook(self, book):
|
||||
self.parent_notebook = book
|
||||
|
@ -40,6 +40,7 @@ from gettext import gettext as _
|
||||
#-------------------------------------------------------------------------
|
||||
import Errors
|
||||
import gen.lib
|
||||
from gui.dbguielement import DbGUIElement
|
||||
from _NoteModel import NoteModel
|
||||
from _EmbeddedList import EmbeddedList
|
||||
from DdTargets import DdTargets
|
||||
@ -49,7 +50,7 @@ from DdTargets import DdTargets
|
||||
# NoteTab
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class NoteTab(EmbeddedList):
|
||||
class NoteTab(EmbeddedList, DbGUIElement):
|
||||
"""
|
||||
Note List display tab for edit dialogs.
|
||||
|
||||
@ -83,12 +84,19 @@ class NoteTab(EmbeddedList):
|
||||
EmbeddedList.__init__(self, dbstate, uistate, track,
|
||||
_("_Notes"), NoteModel, share_button=True,
|
||||
move_buttons=True)
|
||||
DbGUIElement.__init__(self, dbstate.db)
|
||||
self.callman.register_handles({'note': self.data})
|
||||
|
||||
def connect_db_signals(self):
|
||||
#connect external remove/change of object to rebuild of grampstab
|
||||
self._add_db_signal('note-delete', self.note_delete)
|
||||
self._add_db_signal('note-rebuild', self.rebuild)
|
||||
self._add_db_signal('note-update',self.note_update)
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Implement base class DbGUIElement method
|
||||
"""
|
||||
#note: note-rebuild closes the editors, so no need to connect to it
|
||||
self.callman.register_callbacks(
|
||||
{'note-delete': self.note_delete, # delete a note we track
|
||||
'note-update': self.note_update, # change a note we track
|
||||
})
|
||||
self.callman.connect_all(keys=['note'])
|
||||
|
||||
def get_editor(self):
|
||||
pass
|
||||
@ -133,6 +141,7 @@ class NoteTab(EmbeddedList):
|
||||
Called to update the screen when a new note is added
|
||||
"""
|
||||
self.get_data().append(name)
|
||||
self.callman.register_handles({'note': [name]})
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
|
@ -33,6 +33,7 @@ from gettext import gettext as _
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gen.lib
|
||||
from gui.dbguielement import DbGUIElement
|
||||
import Errors
|
||||
from DdTargets import DdTargets
|
||||
from _RepoRefModel import RepoRefModel
|
||||
@ -43,7 +44,7 @@ from _EmbeddedList import EmbeddedList
|
||||
# RepoEmbedList
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class RepoEmbedList(EmbeddedList):
|
||||
class RepoEmbedList(EmbeddedList, DbGUIElement):
|
||||
|
||||
_HANDLE_COL = 4
|
||||
_DND_TYPE = DdTargets.REPOREF
|
||||
@ -72,6 +73,20 @@ class RepoEmbedList(EmbeddedList):
|
||||
EmbeddedList.__init__(self, dbstate, uistate, track,
|
||||
_('_Repositories'), RepoRefModel,
|
||||
share_button=True, move_buttons=True)
|
||||
DbGUIElement.__init__(self, dbstate.db)
|
||||
self.callman.register_handles({'repository': [rref.ref for rref
|
||||
in self.obj]})
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Implement base class DbGUIElement method
|
||||
"""
|
||||
#note: repository-rebuild closes the editors, so no need to connect
|
||||
self.callman.register_callbacks(
|
||||
{'repository-delete': self.repo_delete, # delete a repo we track
|
||||
'repository-update': self.repo_update, # change a repo we track
|
||||
})
|
||||
self.callman.connect_all(keys=['repository'])
|
||||
|
||||
def get_icon_name(self):
|
||||
return 'gramps-repository'
|
||||
@ -135,6 +150,7 @@ class RepoEmbedList(EmbeddedList):
|
||||
def add_callback(self, value):
|
||||
value[0].ref = value[1].handle
|
||||
self.get_data().append(value[0])
|
||||
self.callman.register_handles({'repository': [value[1].handle]})
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
@ -164,3 +180,39 @@ class RepoEmbedList(EmbeddedList):
|
||||
def edit_callback(self, name):
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
def repo_delete(self, del_repo_handle_list):
|
||||
"""
|
||||
Outside of this tab repo objects have been deleted. Check if tab
|
||||
and object must be changed.
|
||||
Note: delete of object will cause reference on database to be removed,
|
||||
so this method need not do this
|
||||
"""
|
||||
rebuild = False
|
||||
ref_handles = [rref.ref for rref in self.obj]
|
||||
for handle in del_repo_handle_list :
|
||||
while 1:
|
||||
pos = None
|
||||
try :
|
||||
pos = ref_handles.index(handle)
|
||||
except ValueError :
|
||||
break
|
||||
|
||||
if pos is not None:
|
||||
#oeps, we need to remove this reference, and rebuild tab
|
||||
del self.obj[pos]
|
||||
del ref_handles[pos]
|
||||
rebuild = True
|
||||
if rebuild:
|
||||
self.rebuild()
|
||||
|
||||
def repo_update(self, upd_repo_handle_list):
|
||||
"""
|
||||
Outside of this tab repo objects have been changed. Check if tab
|
||||
and object must be changed.
|
||||
"""
|
||||
ref_handles = [rref.ref for rref in self.obj]
|
||||
for handle in upd_repo_handle_list :
|
||||
if handle in ref_handles:
|
||||
self.rebuild()
|
||||
break
|
||||
|
@ -33,6 +33,7 @@ from gettext import gettext as _
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gen.lib
|
||||
from gui.dbguielement import DbGUIElement
|
||||
import Errors
|
||||
from DdTargets import DdTargets
|
||||
from _SourceRefModel import SourceRefModel
|
||||
@ -43,7 +44,7 @@ from _EmbeddedList import EmbeddedList
|
||||
# SourceEmbedList
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class SourceEmbedList(EmbeddedList):
|
||||
class SourceEmbedList(EmbeddedList, DbGUIElement):
|
||||
|
||||
_HANDLE_COL = 4
|
||||
_DND_TYPE = DdTargets.SOURCEREF
|
||||
@ -72,6 +73,20 @@ class SourceEmbedList(EmbeddedList):
|
||||
EmbeddedList.__init__(self, dbstate, uistate, track, _('_Sources'),
|
||||
SourceRefModel, share_button=True,
|
||||
move_buttons=True)
|
||||
DbGUIElement.__init__(self, dbstate.db)
|
||||
self.callman.register_handles({'source': [sref.ref for sref
|
||||
in self.obj.get_source_references()]})
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Implement base class DbGUIElement method
|
||||
"""
|
||||
#note: source-rebuild closes the editors, so no need to connect to it
|
||||
self.callman.register_callbacks(
|
||||
{'source-delete': self.source_delete, # delete a source we track
|
||||
'source-update': self.source_update, # change a source we track
|
||||
})
|
||||
self.callman.connect_all(keys=['source'])
|
||||
|
||||
def get_icon_name(self):
|
||||
return 'gramps-source'
|
||||
@ -141,12 +156,26 @@ class SourceEmbedList(EmbeddedList):
|
||||
)
|
||||
|
||||
def object_added(self, reference, primary):
|
||||
"""
|
||||
Callback from sourceref editor after adding a new reference (to a new
|
||||
or an existing source).
|
||||
Note that if it was to an existing source already present in the
|
||||
sourcelist, then the source-update signal will also cause a rebuild
|
||||
at that time.
|
||||
"""
|
||||
reference.ref = primary.handle
|
||||
self.get_data().append(reference)
|
||||
self.callman.register_handles({'source': [primary.handle]})
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
def object_edited(self, refererence, primary):
|
||||
"""
|
||||
Callback from sourceref editor. If the source changes itself, also
|
||||
the source-change signal will cause a rebuild.
|
||||
This could be solved in the source editor if it only calls this
|
||||
method in the case the sourceref part only changes.
|
||||
"""
|
||||
self.changed = True
|
||||
self.rebuild()
|
||||
|
||||
@ -160,3 +189,40 @@ class SourceEmbedList(EmbeddedList):
|
||||
src, sref, self.object_added)
|
||||
except Errors.WindowActiveError:
|
||||
pass
|
||||
|
||||
def source_delete(self, del_src_handle_list):
|
||||
"""
|
||||
Outside of this tab source objects have been deleted. Check if tab
|
||||
and object must be changed.
|
||||
Note: delete of object will cause reference on database to be removed,
|
||||
so this method need not do this
|
||||
"""
|
||||
rebuild = False
|
||||
sourceref_list = self.get_data()
|
||||
ref_handles = [sref.ref for sref in sourceref_list]
|
||||
for handle in del_src_handle_list :
|
||||
while 1:
|
||||
pos = None
|
||||
try :
|
||||
pos = ref_handles.index(handle)
|
||||
except ValueError :
|
||||
break
|
||||
|
||||
if pos is not None:
|
||||
#oeps, we need to remove this reference, and rebuild tab
|
||||
del sourceref_list[pos]
|
||||
del ref_handles[pos]
|
||||
rebuild = True
|
||||
if rebuild:
|
||||
self.rebuild()
|
||||
|
||||
def source_update(self, upd_src_handle_list):
|
||||
"""
|
||||
Outside of this tab media objects have been changed. Check if tab
|
||||
and object must be changed.
|
||||
"""
|
||||
ref_handles = [sref.ref for sref in self.get_data()]
|
||||
for handle in upd_src_handle_list :
|
||||
if handle in ref_handles:
|
||||
self.rebuild()
|
||||
break
|
||||
|
@ -129,7 +129,15 @@ class EditChildRef(EditSecondary):
|
||||
self.define_ok_button(self.ok_button, self.save)
|
||||
self.edit_button.connect('button-press-event', self.edit_child)
|
||||
self.edit_button.connect('key-press-event', self.edit_child)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('person-update', self.person_change)
|
||||
self._add_db_signal('person-rebuild', self.close)
|
||||
self._add_db_signal('person-delete', self.check_for_close)
|
||||
|
||||
def _create_tabbed_pages(self):
|
||||
"""
|
||||
@ -185,6 +193,15 @@ class EditChildRef(EditSecondary):
|
||||
self.callback(self.obj)
|
||||
self.close()
|
||||
|
||||
def check_for_close(self, handles):
|
||||
"""
|
||||
Callback method for delete signals.
|
||||
If there is a delete signal of the primary object we are editing, the
|
||||
editor (and all child windows spawned) should be closed
|
||||
"""
|
||||
if self.obj.ref in handles:
|
||||
self.close()
|
||||
|
||||
def button_activated(event, mouse_button):
|
||||
if (event.type == gtk.gdk.BUTTON_PRESS and \
|
||||
event.button == mouse_button) or \
|
||||
|
@ -116,6 +116,14 @@ class EditEvent(EditPrimary):
|
||||
self.ok_button.set_sensitive(not self.db.readonly)
|
||||
self.ok_button.connect('clicked', self.save)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('event-rebuild', self._do_close)
|
||||
self._add_db_signal('event-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
|
||||
# place, select_place, add_del_place
|
||||
|
@ -96,6 +96,14 @@ class EditEventRef(EditReference):
|
||||
# FIXME: activate when help page is available
|
||||
#self.define_help_button(self.top.get_object('help'))
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('event-rebuild', self.close)
|
||||
self._add_db_signal('event-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
|
||||
self.ref_privacy = PrivacyButton(
|
||||
|
@ -445,47 +445,92 @@ class EditFamily(EditPrimary):
|
||||
|
||||
def _local_init(self):
|
||||
self.build_interface()
|
||||
|
||||
self._add_db_signal('family-update', self.check_for_family_change)
|
||||
self._add_db_signal('family-delete', self.check_for_close)
|
||||
|
||||
# Add a signal pick up changes to events, bug #1329
|
||||
self._add_db_signal('event-update', self.event_updated)
|
||||
|
||||
self.added = self.obj.handle is None
|
||||
if self.added:
|
||||
self.obj.handle = Utils.create_id()
|
||||
|
||||
self.load_data()
|
||||
|
||||
def check_for_close(self, handles):
|
||||
if self.obj.get_handle() in handles:
|
||||
self._do_close()
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
implement from base class DbGUIElement
|
||||
Register the callbacks we need.
|
||||
Note:
|
||||
* we do not connect to person-delete, as a delete of a person in
|
||||
the family outside of this editor will cause a family-update
|
||||
signal of this family
|
||||
"""
|
||||
self.callman.register_handles({'family': [self.obj.get_handle()]})
|
||||
self.callman.register_callbacks(
|
||||
{'family-update': self.check_for_family_change,
|
||||
'family-delete': self.check_for_close,
|
||||
'family-rebuild': self._do_close,
|
||||
'event-update': self.topdata_updated, # change eg birth event fath
|
||||
'event-rebuild': self.topdata_updated,
|
||||
'event-delete': self.topdata_updated, # delete eg birth event fath
|
||||
'person-update': self.topdata_updated, # change eg name of father
|
||||
'person-rebuild': self._do_close,
|
||||
})
|
||||
self.callman.connect_all(keys=['family', 'event', 'person'])
|
||||
|
||||
def check_for_family_change(self, handles):
|
||||
|
||||
# check to see if the handle matches the current object
|
||||
"""
|
||||
Callback for family-update signal
|
||||
1. This method checks to see if the family shown has been changed. This
|
||||
is possible eg in the relationship view. If the family was changed,
|
||||
the view is refreshed and a warning dialog shown to indicate all
|
||||
changes have been lost.
|
||||
If a source/note/event is deleted, this method is called too. This
|
||||
is unfortunate as the displaytabs can track themself a delete and
|
||||
correct the view for this. Therefore, these tabs are not rebuild.
|
||||
Conclusion: this method updates so that remove/change of parent or
|
||||
remove/change of children in relationship view reloads the family
|
||||
from db.
|
||||
2. Changes in other families are of no consequence to the family shown
|
||||
"""
|
||||
if self.obj.get_handle() in handles:
|
||||
#rebuild data
|
||||
## Todo: Gallery and note tab are not rebuild ??
|
||||
objreal = self.dbstate.db.get_family_from_handle(
|
||||
self.obj.get_handle())
|
||||
#update selection of data that we obtain from database change:
|
||||
maindatachanged = (self.obj.gramps_id != objreal.gramps_id or
|
||||
self.obj.father_handle != objreal.father_handle or
|
||||
self.obj.mother_handle != objreal.mother_handle or
|
||||
self.obj.private != objreal.private or
|
||||
self.obj.type != objreal.type or
|
||||
self.obj.marker != objreal.marker or
|
||||
self.obj.child_ref_list != objreal.child_ref_list)
|
||||
if maindatachanged:
|
||||
self.obj.gramps_id = objreal.gramps_id
|
||||
self.obj.father_handle = objreal.father_handle
|
||||
self.obj.mother_handle = objreal.mother_handle
|
||||
self.obj.private = objreal.private
|
||||
self.obj.type = objreal.type
|
||||
self.obj.marker = objreal.marker
|
||||
self.obj.child_ref_list = objreal.child_ref_list
|
||||
self.reload_people()
|
||||
|
||||
self.obj = self.dbstate.db.get_family_from_handle(self.obj.get_handle())
|
||||
self.reload_people()
|
||||
self.event_list.rebuild()
|
||||
self.source_list.rebuild()
|
||||
self.attr_list.data = self.obj.get_attribute_list()
|
||||
self.attr_list.rebuild()
|
||||
self.lds_embed.data = self.obj.get_lds_ord_list()
|
||||
self.lds_embed.rebuild()
|
||||
|
||||
# No matter why the family changed (eg delete of a source), we notify
|
||||
# the user
|
||||
WarningDialog(
|
||||
_("Family has changed"),
|
||||
_("The family you are editing has changed. To make sure that the "
|
||||
"database is not corrupted, GRAMPS has updated the family to "
|
||||
"reflect these changes. Any edits you have made may have been lost."))
|
||||
_("Family has changed"),
|
||||
_("The %(object)s you are editing has changed outside this editor."
|
||||
" This can be due to a change in one of the main views, for "
|
||||
"example a source used here is deleted in the source view.\n"
|
||||
"To make sure the information shown is still correct, the "
|
||||
"data shown has been updated. Some edits you have made may have"
|
||||
" been lost.") % {'object': _('family')}, parent=self.window)
|
||||
|
||||
def event_updated(self, obj):
|
||||
def topdata_updated(self, *obj):
|
||||
"""
|
||||
Callback method called if data shown in top part of family editor
|
||||
(a parent, birth/death event of parent) changes
|
||||
Note: person events shown in the event list are not tracked, the
|
||||
tabpage itself tracks it
|
||||
"""
|
||||
self.load_data()
|
||||
#place in event might have changed, or person event shown in the list
|
||||
self.event_list.rebuild_callback()
|
||||
|
||||
def show_buttons(self):
|
||||
"""
|
||||
@ -617,6 +662,10 @@ class EditFamily(EditPrimary):
|
||||
)
|
||||
|
||||
def load_data(self):
|
||||
"""
|
||||
Show top data of family editor: father and mother info
|
||||
and set self.phandles with all person handles in the family
|
||||
"""
|
||||
fhandle = self.obj.get_father_handle()
|
||||
self.update_father(fhandle)
|
||||
|
||||
@ -839,7 +888,8 @@ class EditFamily(EditPrimary):
|
||||
'in the database. If you save, you will create '
|
||||
'a duplicate family. It is recommended that '
|
||||
'you cancel the editing of this window, and '
|
||||
'select the existing family'))
|
||||
'select the existing family'),
|
||||
parent=self.window)
|
||||
|
||||
def edit_father(self, obj, event):
|
||||
handle = self.obj.get_father_handle()
|
||||
@ -871,10 +921,17 @@ class EditFamily(EditPrimary):
|
||||
name = "%s [%s]" % (name_displayer.display(person),
|
||||
person.gramps_id)
|
||||
birth = ReportUtils.get_birth_or_fallback(db, person)
|
||||
self.callman.register_handles({'person': [handle]})
|
||||
if birth:
|
||||
#if event changes it view needs to update
|
||||
self.callman.register_handles({'event': [birth.get_handle()]})
|
||||
if birth and birth.get_type() == gen.lib.EventType.BAPTISM:
|
||||
birth_label.set_label(_("Baptism:"))
|
||||
|
||||
death = ReportUtils.get_death_or_fallback(db, person)
|
||||
if death:
|
||||
#if event changes it view needs to update
|
||||
self.callman.register_handles({'event': [death.get_handle()]})
|
||||
if death and death.get_type() == gen.lib.EventType.BURIAL:
|
||||
death_label.set_label(_("Burial:"))
|
||||
|
||||
@ -985,9 +1042,7 @@ class EditFamily(EditPrimary):
|
||||
# We disconnect the callbacks to all signals we connected earlier.
|
||||
# This prevents the signals originating in any of the following
|
||||
# commits from being caught by us again.
|
||||
for key in self.signal_keys:
|
||||
self.db.disconnect(key)
|
||||
self.signal_keys = []
|
||||
self._cleanup_callbacks()
|
||||
|
||||
if not original and not self.object_is_empty():
|
||||
trans = self.db.transaction_begin()
|
||||
|
@ -103,6 +103,14 @@ class EditMedia(EditPrimary):
|
||||
self.define_ok_button(self.glade.get_object('ok'), self.save)
|
||||
self.define_help_button(self.glade.get_object('button102'))
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('media-rebuild', self._do_close)
|
||||
self._add_db_signal('media-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
self.date_field = MonitoredDate(self.glade.get_object("date_entry"),
|
||||
self.glade.get_object("date_edit"),
|
||||
|
@ -475,6 +475,14 @@ class EditMediaRef(EditReference):
|
||||
self.define_cancel_button(self.top.get_object('button84'))
|
||||
self.define_ok_button(self.top.get_object('button82'),self.save)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('media-rebuild', self.close)
|
||||
self._add_db_signal('media-delete', self.check_for_close)
|
||||
|
||||
def _create_tabbed_pages(self):
|
||||
"""
|
||||
Create the notebook tabs and inserts them into the main
|
||||
|
@ -226,7 +226,15 @@ class EditNote(EditPrimary):
|
||||
self.define_ok_button(self.top.get_object('ok'), self.save)
|
||||
self.define_cancel_button(self.top.get_object('cancel'))
|
||||
self.define_help_button(self.top.get_object('help'))
|
||||
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('note-rebuild', self._do_close)
|
||||
self._add_db_signal('note-delete', self.check_for_close)
|
||||
|
||||
def _create_tabbed_pages(self):
|
||||
"""Create the notebook tabs and inserts them into the main window."""
|
||||
notebook = self.top.get_object("note_notebook")
|
||||
|
@ -167,9 +167,7 @@ class EditPerson(EditPrimary):
|
||||
def _connect_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
|
||||
"""
|
||||
self.define_cancel_button(self.top.get_object("button15"))
|
||||
self.define_ok_button(self.top.get_object("ok"), self.save)
|
||||
@ -182,6 +180,13 @@ class EditPerson(EditPrimary):
|
||||
self.eventbox.connect('button-press-event',
|
||||
self._image_button_press)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('person-rebuild', self._do_close)
|
||||
self._add_db_signal('person-delete', self.check_for_close)
|
||||
self._add_db_signal('family-rebuild', self.family_change)
|
||||
self._add_db_signal('family-delete', self.family_change)
|
||||
self._add_db_signal('family-update', self.family_change)
|
||||
@ -409,7 +414,6 @@ class EditPerson(EditPrimary):
|
||||
notebook.show_all()
|
||||
self.top.get_object('vbox').pack_start(notebook, True)
|
||||
|
||||
|
||||
def _changed_name(self, obj):
|
||||
"""
|
||||
callback to changes typed by user to the person name.
|
||||
|
@ -107,6 +107,23 @@ class EditPersonRef(EditSecondary):
|
||||
self.define_ok_button(self.top.get_object('ok'),self.save)
|
||||
self.top.get_object('select').connect('clicked',self._select_person)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('person-rebuild', self.close)
|
||||
self._add_db_signal('person-delete', self.check_for_close)
|
||||
|
||||
def check_for_close(self, handles):
|
||||
"""
|
||||
Callback method for delete signals.
|
||||
If there is a delete signal of the primary object we are editing, the
|
||||
editor (and all child windows spawned) should be closed
|
||||
"""
|
||||
if self.obj.ref in handles:
|
||||
self.close()
|
||||
|
||||
def _select_person(self, obj):
|
||||
from Selectors import selector_factory
|
||||
SelectPerson = selector_factory('Person')
|
||||
|
@ -142,6 +142,14 @@ class EditPlace(EditPrimary):
|
||||
self.define_cancel_button(self.top.get_object('cancel'))
|
||||
self.define_help_button(self.top.get_object('help'))
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('place-rebuild', self._do_close)
|
||||
self._add_db_signal('place-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
mloc = self.obj.get_main_location()
|
||||
|
||||
|
@ -21,9 +21,25 @@
|
||||
|
||||
# $Id$
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gettext import gettext as _
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GTK modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import gtk
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
import ManagedWindow
|
||||
import DateHandler
|
||||
from BasicUtils import name_displayer
|
||||
@ -31,8 +47,9 @@ import Config
|
||||
import GrampsDisplay
|
||||
from QuestionDialog import SaveDialog
|
||||
import gen.lib
|
||||
from gui.dbguielement import DbGUIElement
|
||||
|
||||
class EditPrimary(ManagedWindow.ManagedWindow):
|
||||
class EditPrimary(ManagedWindow.ManagedWindow, DbGUIElement):
|
||||
|
||||
QR_CATEGORY = -1
|
||||
|
||||
@ -52,19 +69,23 @@ class EditPrimary(ManagedWindow.ManagedWindow):
|
||||
self.uistate = uistate
|
||||
self.db = state.db
|
||||
self.callback = callback
|
||||
self.signal_keys = []
|
||||
self.ok_button = None
|
||||
self.get_from_handle = get_from_handle
|
||||
self.get_from_gramps_id = get_from_gramps_id
|
||||
self.contexteventbox = None
|
||||
self.__tabs = []
|
||||
|
||||
ManagedWindow.ManagedWindow.__init__(self, uistate, track, obj)
|
||||
DbGUIElement.__init__(self, self.db)
|
||||
|
||||
self._local_init()
|
||||
self._set_size()
|
||||
self._create_tabbed_pages()
|
||||
self._setup_fields()
|
||||
self._connect_signals()
|
||||
#if the database is changed, all info shown is invalid and the window
|
||||
# should close
|
||||
self.dbstate.connect('database-changed', self._do_close)
|
||||
self.show()
|
||||
self._post_init()
|
||||
|
||||
@ -80,18 +101,15 @@ class EditPrimary(ManagedWindow.ManagedWindow):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _add_db_signal(self, name, callback):
|
||||
self.signal_keys.append(self.db.connect(name, callback))
|
||||
|
||||
def _connect_signals(self):
|
||||
pass
|
||||
|
||||
def _setup_fields(self):
|
||||
pass
|
||||
|
||||
def _create_tabbed_pages(self):
|
||||
pass
|
||||
|
||||
def _connect_signals(self):
|
||||
pass
|
||||
|
||||
def build_window_key(self, obj):
|
||||
if obj and obj.get_handle():
|
||||
return obj.get_handle()
|
||||
@ -126,8 +144,8 @@ class EditPrimary(ManagedWindow.ManagedWindow):
|
||||
notebook.set_current_page(page_no)
|
||||
|
||||
def _add_tab(self, notebook, page):
|
||||
self.__tabs.append(page)
|
||||
notebook.insert_page(page, page.get_tab_widget())
|
||||
page.add_db_signal_callback(self._add_db_signal)
|
||||
page.label.set_use_underline(True)
|
||||
return page
|
||||
|
||||
@ -151,11 +169,31 @@ class EditPrimary(ManagedWindow.ManagedWindow):
|
||||
section))
|
||||
|
||||
def _do_close(self, *obj):
|
||||
for key in self.signal_keys:
|
||||
self.db.disconnect(key)
|
||||
self._cleanup_db_connects()
|
||||
self._cleanup_on_exit()
|
||||
ManagedWindow.ManagedWindow.close(self)
|
||||
|
||||
def _cleanup_db_connects(self):
|
||||
"""
|
||||
All connects that happened to signals of the db must be removed on
|
||||
closed. This implies two things:
|
||||
1. The connects on the main view must be disconnected
|
||||
2. Connects done in subelements must be disconnected
|
||||
"""
|
||||
#cleanup callbackmanager of this editor
|
||||
self._cleanup_callbacks()
|
||||
for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]:
|
||||
tab._cleanup_callbacks()
|
||||
|
||||
def check_for_close(self, handles):
|
||||
"""
|
||||
Callback method for delete signals.
|
||||
If there is a delete signal of the primary object we are editing, the
|
||||
editor (and all child windows spawned) should be closed
|
||||
"""
|
||||
if self.obj.get_handle() in handles:
|
||||
self._do_close()
|
||||
|
||||
def close(self, *obj):
|
||||
"""If the data has changed, give the user a chance to cancel
|
||||
the close window"""
|
||||
|
@ -36,6 +36,7 @@ import gtk
|
||||
import ManagedWindow
|
||||
from DisplayTabs import GrampsTab
|
||||
import Config
|
||||
from gui.dbguielement import DbGUIElement
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
@ -85,7 +86,7 @@ class RefTab(GrampsTab):
|
||||
# EditReference class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class EditReference(ManagedWindow.ManagedWindow):
|
||||
class EditReference(ManagedWindow.ManagedWindow, DbGUIElement):
|
||||
|
||||
def __init__(self, state, uistate, track, source, source_ref, update):
|
||||
self.db = state.db
|
||||
@ -95,10 +96,11 @@ class EditReference(ManagedWindow.ManagedWindow):
|
||||
self.source = source
|
||||
self.source_added = False
|
||||
self.update = update
|
||||
self.signal_keys = []
|
||||
self.warn_box = None
|
||||
self.__tabs = []
|
||||
|
||||
ManagedWindow.ManagedWindow.__init__(self, uistate, track, source_ref)
|
||||
DbGUIElement.__init__(self, self.db)
|
||||
|
||||
self._local_init()
|
||||
self._set_size()
|
||||
@ -155,14 +157,11 @@ class EditReference(ManagedWindow.ManagedWindow):
|
||||
notebook.set_current_page(page_no)
|
||||
|
||||
def _add_tab(self, notebook,page):
|
||||
self.__tabs.append(page)
|
||||
notebook.insert_page(page, page.get_tab_widget())
|
||||
page.add_db_signal_callback(self._add_db_signal)
|
||||
page.label.set_use_underline(True)
|
||||
return page
|
||||
|
||||
def _add_db_signal(self, name, callback):
|
||||
self.signal_keys.append(self.db.connect(name,callback))
|
||||
|
||||
def _connect_signals(self):
|
||||
pass
|
||||
|
||||
@ -190,6 +189,15 @@ class EditReference(ManagedWindow.ManagedWindow):
|
||||
self._cleanup_on_exit()
|
||||
self.close(obj)
|
||||
|
||||
def check_for_close(self, handles):
|
||||
"""
|
||||
Callback method for delete signals.
|
||||
If there is a delete signal of the primary object we are editing, the
|
||||
editor (and all child windows spawned) should be closed
|
||||
"""
|
||||
if self.source.get_handle() in handles:
|
||||
self.close()
|
||||
|
||||
def define_help_button(self, button, webpage='', section=''):
|
||||
import GrampsDisplay
|
||||
button.connect('clicked', lambda x: GrampsDisplay.help(webpage,
|
||||
@ -200,6 +208,17 @@ class EditReference(ManagedWindow.ManagedWindow):
|
||||
pass
|
||||
|
||||
def close(self,*obj):
|
||||
for key in self.signal_keys:
|
||||
self.db.disconnect(key)
|
||||
self._cleanup_db_connects()
|
||||
ManagedWindow.ManagedWindow.close(self)
|
||||
|
||||
def _cleanup_db_connects(self):
|
||||
"""
|
||||
All connects that happened to signals of the db must be removed on
|
||||
closed. This implies two things:
|
||||
1. The connects on the main view must be disconnected
|
||||
2. Connects done in subelements must be disconnected
|
||||
"""
|
||||
#cleanup callbackmanager of this editor
|
||||
self._cleanup_callbacks()
|
||||
for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]:
|
||||
tab._cleanup_callbacks()
|
||||
|
@ -83,7 +83,15 @@ class EditRepoRef(EditReference):
|
||||
def _connect_signals(self):
|
||||
self.define_ok_button(self.top.get_object('ok'),self.ok_clicked)
|
||||
self.define_cancel_button(self.top.get_object('cancel'))
|
||||
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('repository-rebuild', self.close)
|
||||
self._add_db_signal('repository-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
self.callno = MonitoredEntry(
|
||||
self.top.get_object("call_number"),
|
||||
|
@ -148,6 +148,14 @@ class EditRepository(EditPrimary):
|
||||
self.define_cancel_button(self.glade.get_object('cancel'))
|
||||
self.define_ok_button(self.glade.get_object('ok'), self.save)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('repository-rebuild', self._do_close)
|
||||
self._add_db_signal('repository-delete', self.check_for_close)
|
||||
|
||||
def save(self, *obj):
|
||||
self.ok_button.set_sensitive(False)
|
||||
if self.object_is_empty():
|
||||
|
@ -24,8 +24,9 @@
|
||||
import ManagedWindow
|
||||
import GrampsDisplay
|
||||
import Config
|
||||
from gui.dbguielement import DbGUIElement
|
||||
|
||||
class EditSecondary(ManagedWindow.ManagedWindow):
|
||||
class EditSecondary(ManagedWindow.ManagedWindow, DbGUIElement):
|
||||
|
||||
def __init__(self, state, uistate, track, obj, callback=None):
|
||||
"""Create an edit window. Associates a person with the window."""
|
||||
@ -35,9 +36,10 @@ class EditSecondary(ManagedWindow.ManagedWindow):
|
||||
self.uistate = uistate
|
||||
self.db = state.db
|
||||
self.callback = callback
|
||||
self.signal_keys = []
|
||||
self.__tabs = []
|
||||
|
||||
ManagedWindow.ManagedWindow.__init__(self, uistate, track, obj)
|
||||
DbGUIElement.__init__(self, self.db)
|
||||
|
||||
self._local_init()
|
||||
self._set_size()
|
||||
@ -60,9 +62,6 @@ class EditSecondary(ManagedWindow.ManagedWindow):
|
||||
"""
|
||||
pass
|
||||
|
||||
def _add_db_signal(self, name, callback):
|
||||
self.signal_keys.append(self.db.connect(name,callback))
|
||||
|
||||
def _connect_signals(self):
|
||||
pass
|
||||
|
||||
@ -101,8 +100,8 @@ class EditSecondary(ManagedWindow.ManagedWindow):
|
||||
notebook.set_current_page(page_no)
|
||||
|
||||
def _add_tab(self, notebook,page):
|
||||
self.__tabs.append(page)
|
||||
notebook.insert_page(page, page.get_tab_widget())
|
||||
page.add_db_signal_callback(self._add_db_signal)
|
||||
page.label.set_use_underline(True)
|
||||
return page
|
||||
|
||||
@ -121,7 +120,18 @@ class EditSecondary(ManagedWindow.ManagedWindow):
|
||||
section))
|
||||
|
||||
def close(self,*obj):
|
||||
for key in self.signal_keys:
|
||||
self.db.disconnect(key)
|
||||
self._cleanup_db_connects()
|
||||
self._cleanup_on_exit()
|
||||
ManagedWindow.ManagedWindow.close(self)
|
||||
|
||||
def _cleanup_db_connects(self):
|
||||
"""
|
||||
All connects that happened to signals of the db must be removed on
|
||||
closed. This implies two things:
|
||||
1. The connects on the main view must be disconnected
|
||||
2. Connects done in subelements must be disconnected
|
||||
"""
|
||||
#cleanup callbackmanager of this editor
|
||||
self._cleanup_callbacks()
|
||||
for tab in [tab for tab in self.__tabs if hasattr(tab, 'callman')]:
|
||||
tab._cleanup_callbacks()
|
||||
|
@ -92,6 +92,14 @@ class EditSource(EditPrimary):
|
||||
self.define_cancel_button(self.glade.get_object('cancel'))
|
||||
self.define_help_button(self.glade.get_object('help'))
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('source-rebuild', self._do_close)
|
||||
self._add_db_signal('source-delete', self.check_for_close)
|
||||
|
||||
def _setup_fields(self):
|
||||
self.author = MonitoredEntry(self.glade.get_object("author"),
|
||||
self.obj.set_author, self.obj.get_author,
|
||||
|
@ -86,6 +86,16 @@ class EditSourceRef(EditReference):
|
||||
self.define_cancel_button(self.top.get_object('cancel'))
|
||||
self.define_help_button(self.top.get_object("help"))
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Connect any signals that need to be connected.
|
||||
Called by the init routine of the base class (_EditPrimary).
|
||||
"""
|
||||
self._add_db_signal('source-rebuild', self.close)
|
||||
self._add_db_signal('source-delete', self.check_for_close)
|
||||
#note: at the moment, a source cannot be updated while an editor with
|
||||
# that source shown is open. So no need to connect to source-update
|
||||
|
||||
def _setup_fields(self):
|
||||
self.ref_privacy = PrivacyButton(
|
||||
self.top.get_object('privacy'), self.source_ref, self.db.readonly)
|
||||
|
@ -417,6 +417,10 @@ class GrampsDbBase(Callback):
|
||||
"""
|
||||
Notify clients that the data has changed significantly, and that all
|
||||
internal data dependent on the database should be rebuilt.
|
||||
Note that all rebuild signals on all objects are emitted at the same
|
||||
time. It is correct to assume that this is always the case.
|
||||
TODO: it might be better to replace these rebuild signals by one single
|
||||
database-rebuild signal.
|
||||
"""
|
||||
self.emit('person-rebuild')
|
||||
self.emit('family-rebuild')
|
||||
|
@ -9,6 +9,7 @@ pkgdata_PYTHON = \
|
||||
__init__.py \
|
||||
dbutils.py \
|
||||
callback.py \
|
||||
callman.py \
|
||||
progressmon.py \
|
||||
longop.py
|
||||
|
||||
|
431
src/gen/utils/callman.py
Normal file
431
src/gen/utils/callman.py
Normal file
@ -0,0 +1,431 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2009 Benny Malengier
|
||||
#
|
||||
# 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$
|
||||
|
||||
"""
|
||||
Module providing support for callback handling in the GUI
|
||||
* track object handles
|
||||
* register new handles
|
||||
* manage callback functions
|
||||
"""
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Python modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# Constants
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
PERSONKEY = 'person'
|
||||
FAMILYKEY = 'family'
|
||||
EVENTKEY = 'event'
|
||||
PLACEKEY = 'place'
|
||||
MEDIAKEY = 'media'
|
||||
SOURCEKEY = 'source'
|
||||
REPOKEY = 'repository'
|
||||
NOTEKEY = 'note'
|
||||
|
||||
ADD = '-add'
|
||||
UPDATE = '-update'
|
||||
DELETE = '-delete'
|
||||
REBUILD = '-rebuild'
|
||||
|
||||
KEYS = [PERSONKEY, FAMILYKEY, EVENTKEY, PLACEKEY, MEDIAKEY, SOURCEKEY,
|
||||
REPOKEY, NOTEKEY]
|
||||
|
||||
METHODS = [ADD, UPDATE, DELETE, REBUILD]
|
||||
METHODS_LIST = [ADD, UPDATE, DELETE]
|
||||
METHODS_NONE = [REBUILD]
|
||||
|
||||
PERSONCLASS = 'Person'
|
||||
FAMILYCLASS = 'Family'
|
||||
EVENTCLASS = 'Event'
|
||||
PLACECLASS = 'Place'
|
||||
MEDIACLASS = 'MediaObject'
|
||||
SOURCECLASS = 'Source'
|
||||
REPOCLASS = 'Repository'
|
||||
NOTECLASS = 'Note'
|
||||
|
||||
CLASS2KEY = {
|
||||
PERSONCLASS: PERSONKEY,
|
||||
FAMILYCLASS: FAMILYKEY,
|
||||
EVENTCLASS: EVENTKEY,
|
||||
PLACECLASS: PLACEKEY,
|
||||
MEDIACLASS: MEDIAKEY,
|
||||
SOURCECLASS: SOURCEKEY,
|
||||
REPOCLASS: REPOKEY,
|
||||
NOTECLASS: NOTEKEY
|
||||
}
|
||||
|
||||
def _return(*args):
|
||||
"""
|
||||
Function that does nothing with the arguments
|
||||
"""
|
||||
return True
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# CallbackManager class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
class CallbackManager(object):
|
||||
"""
|
||||
Manage callback handling from GUI to the db.
|
||||
It is unique to a db and some GUI element. When a db is changed, one should
|
||||
destroy the CallbackManager and set up a new one (or delete the GUI element
|
||||
as it shows info from a previous db).
|
||||
|
||||
Track changes to your relevant objects, calling callback functions as
|
||||
needed.
|
||||
"""
|
||||
def __init__(self, database):
|
||||
"""
|
||||
:param database: database to which to connect the callbacks of this
|
||||
CallbackManager object
|
||||
:type database: a class:`~gen.db.base.GrampsDbBase` object
|
||||
"""
|
||||
#no handles to track
|
||||
self.database = database
|
||||
self.__handles = {
|
||||
PERSONKEY: [],
|
||||
FAMILYKEY: [],
|
||||
EVENTKEY: [],
|
||||
PLACEKEY: [],
|
||||
MEDIAKEY: [],
|
||||
SOURCEKEY: [],
|
||||
REPOKEY: [],
|
||||
NOTEKEY: [],
|
||||
}
|
||||
#no custom callbacks to do
|
||||
self.custom_signal_keys = []
|
||||
#set up callbacks to do nothing
|
||||
self.__callbacks = {}
|
||||
self.__init_callbacks()
|
||||
|
||||
def __init_callbacks(self):
|
||||
"""
|
||||
set up callbacks to do nothing
|
||||
"""
|
||||
self.__callbacks = {}
|
||||
for key in KEYS:
|
||||
for method in METHODS:
|
||||
self.__callbacks[key+method] = [_return, None]
|
||||
|
||||
def disconnect_all(self):
|
||||
"""
|
||||
Disconnect from all signals from the database
|
||||
This method should always be called before a the callback methods
|
||||
become invalid.
|
||||
"""
|
||||
for key in self.custom_signal_keys:
|
||||
self.database.disconnect(key)
|
||||
self.custom_signal_keys = []
|
||||
for key, value in self.__callbacks.iteritems():
|
||||
if not value[1] is None:
|
||||
self.database.disconnect(value[1])
|
||||
self.__init_callbacks()
|
||||
|
||||
def register_obj(self, baseobj, directonly=False):
|
||||
"""
|
||||
Convenience method, will register all directly and not directly
|
||||
referenced prim objects connected to baseobj with the CallbackManager
|
||||
If directonly is True, only directly registered objects will be
|
||||
registered.
|
||||
Note that baseobj is not registered itself as it can be a sec obj.
|
||||
"""
|
||||
if directonly:
|
||||
self.register_handles(directhandledict(baseobj))
|
||||
else:
|
||||
self.register_handles(handledict(baseobj))
|
||||
|
||||
def register_handles(self, ahandledict):
|
||||
"""
|
||||
Register handles that need to be tracked by the manager.
|
||||
This function can be called several times, adding to existing
|
||||
registered handles.
|
||||
|
||||
:param ahandledict: a dictionary with key one of the KEYS,
|
||||
and value a list of handles to track
|
||||
"""
|
||||
for key in KEYS:
|
||||
handles = ahandledict.get(key)
|
||||
if handles:
|
||||
self.__handles[key] = list(
|
||||
set(self.__handles[key]).union(handles))
|
||||
|
||||
def unregister_handles(self, ahandledict):
|
||||
"""
|
||||
All handles in handledict are no longer tracked
|
||||
|
||||
:param handledict: a dictionary with key one of the KEYS,
|
||||
and value a list of handles to track
|
||||
"""
|
||||
for key in KEYS:
|
||||
handles = ahandledict.get(key)
|
||||
if handles:
|
||||
for handle in handles:
|
||||
self.__handles[key].remove(handle)
|
||||
|
||||
def unregister_all(self):
|
||||
"""
|
||||
Unregister all handles that are registered
|
||||
"""
|
||||
self.__handles = {
|
||||
PERSONKEY: [],
|
||||
FAMILYKEY: [],
|
||||
EVENTKEY: [],
|
||||
PLACEKEY: [],
|
||||
MEDIAKEY: [],
|
||||
SOURCEKEY: [],
|
||||
REPOKEY: [],
|
||||
NOTEKEY: [],
|
||||
}
|
||||
|
||||
def register_callbacks(self, callbackdict):
|
||||
"""
|
||||
register callback functions that need to be called for a specific
|
||||
db action. This function can be called several times, adding to and if
|
||||
needed overwriting, existing callbacks.
|
||||
No db connects are done. If a signal already is connected to the db,
|
||||
it is removed from the connect list of the db.
|
||||
|
||||
:param callbackdict: a dictionary with key one of KEYS+METHODS, or one
|
||||
of KEYS, and value a function to be called when signal is raised.
|
||||
"""
|
||||
for key in KEYS:
|
||||
function = callbackdict.get(key)
|
||||
if function:
|
||||
for method in METHODS:
|
||||
self.__add_callback(key+method, function)
|
||||
for method in METHODS:
|
||||
function = callbackdict.get(key+method)
|
||||
if function:
|
||||
self.__add_callback(key+method, function)
|
||||
|
||||
def connect_all(self, keys=None):
|
||||
"""
|
||||
Convenience function, connects all database signals related to the
|
||||
primary objects given in keys to the callbacks attached to self.
|
||||
Note that only those callbacks registered with register_callbacks will
|
||||
effectively result in an action, so one can connect to all keys
|
||||
even if not all keys have a registered callback.
|
||||
|
||||
:param keys: list of keys of primary objects for which to connect the
|
||||
signals, default is no connects being done. One can enable signal
|
||||
activity to needed objects by passing a list, eg
|
||||
keys=[callman.SOURCEKEY, callman.PLACEKEY], or to all with
|
||||
keys=callman.KEYS
|
||||
"""
|
||||
if keys is None:
|
||||
return
|
||||
for key in keys:
|
||||
for method in METHODS:
|
||||
signal = key + method
|
||||
self.__do_unconnect(signal)
|
||||
self.__callbacks[signal][1] = self.database.connect(
|
||||
signal,
|
||||
self.__callbackcreator(key, signal))
|
||||
|
||||
def __do_callback(self, signal, *arg):
|
||||
"""
|
||||
Execute a specific callback. This is only actually done if one of the
|
||||
registered handles is involved.
|
||||
Arg must conform to the requirements of the signal emitter.
|
||||
For a GrampsDbBase that is that arg must be not given (rebuild
|
||||
methods), or arg[0] must be the list of handles affected.
|
||||
"""
|
||||
key = signal.split('-')[0]
|
||||
if arg:
|
||||
handles = arg[0]
|
||||
affected = list(set(self.__handles[key]).intersection(handles))
|
||||
if affected:
|
||||
self.__callbacks[signal][0](affected)
|
||||
else:
|
||||
affected = self.__handles[key]
|
||||
if affected:
|
||||
self.__callbacks[signal][0]()
|
||||
|
||||
def __add_callback(self, signal, callback):
|
||||
"""
|
||||
Add a callback to a signal. There can be only one callback per signal
|
||||
that is managed, so if there is a previous one, it is removed
|
||||
"""
|
||||
self.__do_unconnect(signal)
|
||||
self.__callbacks[signal] = [callback, None]
|
||||
|
||||
def __do_unconnect(self, signal):
|
||||
"""
|
||||
unconnect a signal from the database if it is already connected
|
||||
"""
|
||||
oldcall, oldconnectkey = self.__callbacks[signal]
|
||||
if not oldconnectkey is None:
|
||||
self.database.disconnect(oldconnectkey)
|
||||
|
||||
def add_db_signal(self, name, callback):
|
||||
"""
|
||||
Do a custom db connect signal outside of the primary object ones
|
||||
managed automatically.
|
||||
"""
|
||||
self.custom_signal_keys.append(self.database.connect(name, callback))
|
||||
|
||||
def __callbackcreator(self, key, signal):
|
||||
"""
|
||||
helper function, a lambda function needs a string to be defined
|
||||
explicitly. This function creates the correct lambda function to use
|
||||
as callback based on the key/signal one needs to connect to.
|
||||
AttributeError is raised for unknown key or signal.
|
||||
"""
|
||||
if key == PERSONKEY:
|
||||
if signal == 'person-update':
|
||||
return lambda arg: self.__do_callback('person-update', *(arg,))
|
||||
elif signal == 'person-add':
|
||||
return lambda arg: self.__do_callback('person-add', *(arg,))
|
||||
elif signal == 'person-delete':
|
||||
return lambda arg: self.__do_callback('person-delete', *(arg,))
|
||||
elif signal == 'person-rebuild':
|
||||
return lambda *arg: self.__do_callback('person-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == FAMILYKEY:
|
||||
if signal == 'family-update':
|
||||
return lambda arg: self.__do_callback('family-update', *(arg,))
|
||||
elif signal == 'family-add':
|
||||
return lambda arg: self.__do_callback('family-add', *(arg,))
|
||||
elif signal == 'family-delete':
|
||||
return lambda arg: self.__do_callback('family-delete', *(arg,))
|
||||
elif signal == 'family-rebuild':
|
||||
return lambda *arg: self.__do_callback('family-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == EVENTKEY:
|
||||
if signal == 'event-update':
|
||||
return lambda arg: self.__do_callback('event-update', *(arg,))
|
||||
elif signal == 'event-add':
|
||||
return lambda arg: self.__do_callback('event-add', *(arg,))
|
||||
elif signal == 'event-delete':
|
||||
return lambda arg: self.__do_callback('event-delete', *(arg,))
|
||||
elif signal == 'event-rebuild':
|
||||
return lambda *arg: self.__do_callback('event-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == PLACEKEY:
|
||||
if signal == 'place-update':
|
||||
return lambda arg: self.__do_callback('place-update', *(arg,))
|
||||
elif signal == 'place-add':
|
||||
return lambda arg: self.__do_callback('place-add', *(arg,))
|
||||
elif signal == 'place-delete':
|
||||
return lambda arg: self.__do_callback('place-delete', *(arg,))
|
||||
elif signal == 'place-rebuild':
|
||||
return lambda *arg: self.__do_callback('place-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == SOURCEKEY:
|
||||
if signal == 'source-update':
|
||||
return lambda arg: self.__do_callback('source-update', *(arg,))
|
||||
elif signal == 'source-add':
|
||||
return lambda arg: self.__do_callback('source-add', *(arg,))
|
||||
elif signal == 'source-delete':
|
||||
return lambda arg: self.__do_callback('source-delete', *(arg,))
|
||||
elif signal == 'source-rebuild':
|
||||
return lambda *arg: self.__do_callback('source-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == REPOKEY:
|
||||
if signal == 'repository-update':
|
||||
return lambda arg: self.__do_callback('repository-update',
|
||||
*(arg,))
|
||||
elif signal == 'repository-add':
|
||||
return lambda arg: self.__do_callback('repository-add',
|
||||
*(arg,))
|
||||
elif signal == 'repository-delete':
|
||||
return lambda arg: self.__do_callback('repository-delete',
|
||||
*(arg,))
|
||||
elif signal == 'repository-rebuild':
|
||||
return lambda *arg: self.__do_callback('repository-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == MEDIAKEY:
|
||||
if signal == 'media-update':
|
||||
return lambda arg: self.__do_callback('media-update', *(arg,))
|
||||
elif signal == 'media-add':
|
||||
return lambda arg: self.__do_callback('media-add', *(arg,))
|
||||
elif signal == 'media-delete':
|
||||
return lambda arg: self.__do_callback('media-delete', *(arg,))
|
||||
elif signal == 'media-rebuild':
|
||||
return lambda *arg: self.__do_callback('media-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
elif key == NOTEKEY:
|
||||
if signal == 'note-update':
|
||||
return lambda arg: self.__do_callback('note-update', *(arg,))
|
||||
elif signal == 'note-add':
|
||||
return lambda arg: self.__do_callback('note-add', *(arg,))
|
||||
elif signal == 'note-delete':
|
||||
return lambda arg: self.__do_callback('note-delete', *(arg,))
|
||||
elif signal == 'note-rebuild':
|
||||
return lambda *arg: self.__do_callback('note-rebuild')
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
else:
|
||||
raise AttributeError, 'Signal ' + signal + 'not supported.'
|
||||
|
||||
def directhandledict(baseobj):
|
||||
"""
|
||||
Build a handledict from baseobj with all directly referenced objects
|
||||
"""
|
||||
handles = {
|
||||
PERSONKEY: [],
|
||||
FAMILYKEY: [],
|
||||
EVENTKEY: [],
|
||||
PLACEKEY: [],
|
||||
MEDIAKEY: [],
|
||||
SOURCEKEY: [],
|
||||
REPOKEY: [],
|
||||
NOTEKEY: [],
|
||||
}
|
||||
for classn, handle in baseobj.get_referenced_handles():
|
||||
handles[CLASS2KEY[classn]].append(handle)
|
||||
return handles
|
||||
|
||||
def handledict(baseobj):
|
||||
"""
|
||||
Build a handledict from baseobj with all directly and not directly
|
||||
referenced base obj that are present
|
||||
"""
|
||||
handles = {
|
||||
PERSONKEY: [],
|
||||
FAMILYKEY: [],
|
||||
EVENTKEY: [],
|
||||
PLACEKEY: [],
|
||||
MEDIAKEY: [],
|
||||
SOURCEKEY: [],
|
||||
REPOKEY: [],
|
||||
NOTEKEY: [],
|
||||
}
|
||||
for classn, handle in baseobj.get_referenced_handles_recursively():
|
||||
handles[CLASS2KEY[classn]].append(handle)
|
||||
return handles
|
||||
|
@ -10,6 +10,7 @@ pkgdatadir = $(datadir)/@PACKAGE@/gui
|
||||
|
||||
pkgdata_PYTHON = \
|
||||
__init__.py \
|
||||
dbguielement.py \
|
||||
dbloader.py \
|
||||
dbman.py \
|
||||
grampsgui.py \
|
||||
|
89
src/gui/dbguielement.py
Normal file
89
src/gui/dbguielement.py
Normal file
@ -0,0 +1,89 @@
|
||||
#
|
||||
# Gramps - a GTK+/GNOME based genealogy program
|
||||
#
|
||||
# Copyright (C) 2009 Benny Malengier
|
||||
#
|
||||
# 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$
|
||||
|
||||
"""
|
||||
Group common stuff GRAMPS GUI elements must be able to do when tracking a DB:
|
||||
* connect to db signals
|
||||
* listen to db changes to update themself on relevant changes
|
||||
* determine if the GUI has become out of sync with the db
|
||||
"""
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GRAMPS modules
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
from gen.utils.callman import CallbackManager
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
#
|
||||
# GUIElement class
|
||||
#
|
||||
#-------------------------------------------------------------------------
|
||||
class DbGUIElement(object):
|
||||
"""
|
||||
Group common stuff GRAMPS GUI elements must be able to do when tracking
|
||||
a DB:
|
||||
* connect to db signals
|
||||
* listen to db changes to update themself on relevant changes
|
||||
* determine if the GUI has become out of sync with the db
|
||||
Most interaction with the DB should be done via the callman attribute.
|
||||
On initialization, the method _connect_db_signals is called. Inheriting
|
||||
objects are advised to group the setup of the callman attribute here.
|
||||
|
||||
.. attribute callman : a `~gen.utils.callman.CallbackManager` object, to
|
||||
be used to track specific changes in the db and set up callbacks
|
||||
"""
|
||||
def __init__(self, database):
|
||||
self.callman = CallbackManager(database)
|
||||
self._connect_db_signals()
|
||||
|
||||
def _add_db_signal(self, name, callback):
|
||||
"""
|
||||
Convenience function to add a custom db signal. The attributes are just
|
||||
passed to the callman object.
|
||||
For primary objects, use the register method of the callman attribute.
|
||||
|
||||
:param name: name of the signal to connect to
|
||||
:type name: string
|
||||
:param callback: function to call when signal is emitted
|
||||
:type callback: a funtion or method with the correct signature for the
|
||||
signal
|
||||
"""
|
||||
self.callman.add_db_signal(name, callback)
|
||||
|
||||
def _connect_db_signals(self):
|
||||
"""
|
||||
Convenience method that is called on initialization of DbGUIElement.
|
||||
Use this to group setup of the callman attribute
|
||||
"""
|
||||
pass
|
||||
|
||||
def _cleanup_callbacks(self):
|
||||
"""
|
||||
Remove all db callbacks.
|
||||
This is done automatically on destruction of the object, but is
|
||||
normally needed earlier, calling this method does so.
|
||||
"""
|
||||
database = self.callman.database
|
||||
self.callman.disconnect_all()
|
||||
self.callman = CallbackManager(database)
|
Loading…
Reference in New Issue
Block a user