From 80908fc6f70c00b1d4aa5b5a7dd516d4de0f7316 Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 10 Aug 2020 10:27:25 -0500 Subject: [PATCH 1/6] Rework primary object Deletes in views --- gramps/gen/db/base.py | 24 ++-- gramps/gen/lib/notebase.py | 28 ++++ gramps/gen/lib/primaryobj.py | 4 + gramps/gui/editors/__init__.py | 14 +- gramps/gui/editors/editcitation.py | 55 -------- gramps/gui/editors/editevent.py | 34 ----- gramps/gui/editors/editmedia.py | 60 -------- gramps/gui/editors/editnote.py | 68 --------- gramps/gui/editors/editplace.py | 41 ------ gramps/gui/editors/editrepository.py | 20 --- gramps/gui/editors/editsource.py | 78 ----------- gramps/gui/views/listview.py | 179 +++++++++++++++++------- gramps/plugins/lib/libpersonview.py | 93 ++---------- gramps/plugins/lib/libplaceview.py | 33 ++--- gramps/plugins/lib/libsourceview.py | 104 ++++++++++++++ gramps/plugins/view/citationlistview.py | 15 +- gramps/plugins/view/citationtreeview.py | 31 +--- gramps/plugins/view/eventview.py | 93 +----------- gramps/plugins/view/familyview.py | 50 +------ gramps/plugins/view/mediaview.py | 18 +-- gramps/plugins/view/noteview.py | 14 +- gramps/plugins/view/repoview.py | 19 +-- gramps/plugins/view/sourceview.py | 18 +-- 23 files changed, 338 insertions(+), 755 deletions(-) create mode 100644 gramps/plugins/lib/libsourceview.py diff --git a/gramps/gen/db/base.py b/gramps/gen/db/base.py index 66f586653..7fc9705c1 100644 --- a/gramps/gen/db/base.py +++ b/gramps/gen/db/base.py @@ -1894,14 +1894,10 @@ class DbWriteBase(DbReadBase): handle = person.get_handle() - person_list = [ - item[1] for item in - self.find_backlink_handles(handle, ['Person'])] - - for phandle in person_list: - prsn = self.get_person_from_handle(phandle) - prsn.remove_handle_references('Person', [handle]) - self.commit_person(prsn, trans) + for obj_type, ohandle in self.find_backlink_handles(handle): + obj = self.method("get_%s_from_handle", obj_type)(ohandle) + obj.remove_handle_references('Person', [handle]) + self.method("commit_%s", obj_type)(obj, trans) self.remove_person(handle, trans) def remove_family_relationships(self, family_handle, trans=None): @@ -1919,13 +1915,11 @@ class DbWriteBase(DbReadBase): """ Remove a family and all that references it; trans is compulsory. """ - person_list = [item[1] for item in - self.find_backlink_handles(family_handle, ['Person'])] - for phandle in person_list: - person = self.get_person_from_handle(phandle) - if person: - person.remove_handle_references('Family', [family_handle]) - self.commit_person(person, trans) + for obj_type, ohandle in self.find_backlink_handles(family_handle): + obj = self.method("get_%s_from_handle", obj_type)(ohandle) + if obj: + obj.remove_handle_references('Family', [family_handle]) + self.method("commit_%s", obj_type)(obj, trans) self.remove_family(family_handle, trans) def remove_parent_from_family(self, person_handle, family_handle, diff --git a/gramps/gen/lib/notebase.py b/gramps/gen/lib/notebase.py index 6bfb51de7..e9fe54b48 100644 --- a/gramps/gen/lib/notebase.py +++ b/gramps/gen/lib/notebase.py @@ -22,6 +22,15 @@ """ NoteBase class for Gramps. """ +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import logging + +LOG = logging.getLogger(".note") + #------------------------------------------------------------------------- # @@ -132,6 +141,25 @@ class NoteBase: return False + def remove_note_references(self, handle_list): + """ + Remove the specified handles from the list of note handles, and all + secondary child objects. + + :param citation_handle_list: The list of note handles to be removed + :type handle: list + """ + LOG.debug('enter remove_note handle: %s self: %s note_list: %s', + handle_list, self, self.note_list) + for handle in handle_list: + if handle in self.note_list: + LOG.debug('remove handle %s from note_list %s', + handle, self.note_list) + self.note_list.remove(handle) + LOG.debug('get_note_child_list %s', self.get_note_child_list()) + for item in self.get_note_child_list(): + item.remove_note_references(handle_list) + def set_note_list(self, note_list): """ Assign the passed list to be object's list of :class:`~.note.Note` diff --git a/gramps/gen/lib/primaryobj.py b/gramps/gen/lib/primaryobj.py index 59a1a1342..863f8939d 100644 --- a/gramps/gen/lib/primaryobj.py +++ b/gramps/gen/lib/primaryobj.py @@ -39,8 +39,10 @@ from .tableobj import TableObject from .privacybase import PrivacyBase from .citationbase import CitationBase from .mediabase import MediaBase +from .notebase import NoteBase from .tagbase import TagBase + #------------------------------------------------------------------------- # # Basic Primary Object class @@ -274,6 +276,8 @@ class PrimaryObject(BasicPrimaryObject): self.remove_citation_references(handle_list) elif classname == 'Media' and isinstance(self, MediaBase): self.remove_media_references(handle_list) + elif classname == 'Note' and isinstance(self, NoteBase): + self.remove_note_references(handle_list) else: self._remove_handle_references(classname, handle_list) diff --git a/gramps/gui/editors/__init__.py b/gramps/gui/editors/__init__.py index f1f4ec5ca..fba6db615 100644 --- a/gramps/gui/editors/__init__.py +++ b/gramps/gui/editors/__init__.py @@ -23,25 +23,25 @@ from .editaddress import EditAddress from .editattribute import EditAttribute, EditSrcAttribute from .editchildref import EditChildRef -from .editcitation import EditCitation, DeleteCitationQuery +from .editcitation import EditCitation from .editdate import EditDate -from .editevent import EditEvent, DeleteEventQuery +from .editevent import EditEvent from .editeventref import EditEventRef from .editfamily import EditFamily from .editldsord import EditLdsOrd, EditFamilyLdsOrd from .editlocation import EditLocation -from .editmedia import EditMedia, DeleteMediaQuery +from .editmedia import EditMedia from .editmediaref import EditMediaRef from .editname import EditName -from .editnote import EditNote, DeleteNoteQuery +from .editnote import EditNote from .editperson import EditPerson from .editpersonref import EditPersonRef -from .editplace import EditPlace, DeletePlaceQuery +from .editplace import EditPlace from .editplacename import EditPlaceName from .editplaceref import EditPlaceRef -from .editrepository import EditRepository, DeleteRepositoryQuery +from .editrepository import EditRepository from .editreporef import EditRepoRef -from .editsource import EditSource, DeleteSrcQuery +from .editsource import EditSource from .edittaglist import EditTagList from .editurl import EditUrl from .editlink import EditLink diff --git a/gramps/gui/editors/editcitation.py b/gramps/gui/editors/editcitation.py index d691e1819..11e0b2341 100644 --- a/gramps/gui/editors/editcitation.py +++ b/gramps/gui/editors/editcitation.py @@ -364,58 +364,3 @@ class EditCitation(EditPrimary): else: cmp_obj = self.empty_object() return cmp_obj.serialize(True)[1:] != self.obj.serialize()[1:] - -class DeleteCitationQuery: - def __init__(self, dbstate, uistate, citation, the_lists): - self.citation = citation - self.db = dbstate.db - self.uistate = uistate - self.the_lists = the_lists - - def query_response(self): - with DbTxn(_("Delete Citation (%s)") % self.citation.get_page(), - self.db) as trans: - self.db.disable_signals() - - (person_list, family_list, event_list, place_list, source_list, - media_list, repo_list) = self.the_lists - - ctn_handle_list = [self.citation.get_handle()] - - for handle in person_list: - person = self.db.get_person_from_handle(handle) - person.remove_citation_references(ctn_handle_list) - self.db.commit_person(person, trans) - - for handle in family_list: - family = self.db.get_family_from_handle(handle) - family.remove_citation_references(ctn_handle_list) - self.db.commit_family(family, trans) - - for handle in event_list: - event = self.db.get_event_from_handle(handle) - event.remove_citation_references(ctn_handle_list) - self.db.commit_event(event, trans) - - for handle in place_list: - place = self.db.get_place_from_handle(handle) - place.remove_citation_references(ctn_handle_list) - self.db.commit_place(place, trans) - - for handle in source_list: - source = self.db.get_source_from_handle(handle) - source.remove_citation_references(ctn_handle_list) - self.db.commit_source(source, trans) - - for handle in media_list: - media = self.db.get_media_from_handle(handle) - media.remove_citation_references(ctn_handle_list) - self.db.commit_media(media, trans) - - for handle in repo_list: - repo = self.db.get_repository_from_handle(handle) - repo.remove_citation_references(ctn_handle_list) - self.db.commit_repository(repo, trans) - - self.db.enable_signals() - self.db.remove_citation(self.citation.get_handle(), trans) diff --git a/gramps/gui/editors/editevent.py b/gramps/gui/editors/editevent.py index fa0a5eb93..01e0186d2 100644 --- a/gramps/gui/editors/editevent.py +++ b/gramps/gui/editors/editevent.py @@ -319,37 +319,3 @@ class EditEvent(EditPrimary): self.top.get_object("place").set_markup( self.place_field.EMPTY_TEXT) self.place_field.set_button(False) - - -#------------------------------------------------------------------------- -# -# Delete Query class -# -#------------------------------------------------------------------------- -class DeleteEventQuery: - def __init__(self, dbstate, uistate, event, person_list, family_list): - self.event = event - self.db = dbstate.db - self.uistate = uistate - self.person_list = person_list - self.family_list = family_list - - def query_response(self): - with DbTxn(_("Delete Event (%s)") % self.event.get_gramps_id(), - self.db) as trans: - self.db.disable_signals() - - ev_handle_list = [self.event.get_handle()] - - for handle in self.person_list: - person = self.db.get_person_from_handle(handle) - person.remove_handle_references('Event', ev_handle_list) - self.db.commit_person(person, trans) - - for handle in self.family_list: - family = self.db.get_family_from_handle(handle) - family.remove_handle_references('Event', ev_handle_list) - self.db.commit_family(family, trans) - - self.db.enable_signals() - self.db.remove_event(self.event.get_handle(), trans) diff --git a/gramps/gui/editors/editmedia.py b/gramps/gui/editors/editmedia.py index 933fd3213..f874f699f 100644 --- a/gramps/gui/editors/editmedia.py +++ b/gramps/gui/editors/editmedia.py @@ -359,63 +359,3 @@ class EditMedia(EditPrimary): else: cmp_obj = self.empty_object() return cmp_obj.serialize(True)[1:] != self.obj.serialize()[1:] - -class DeleteMediaQuery: - - def __init__(self, dbstate, uistate, media_handle, the_lists): - self.db = dbstate.db - self.uistate = uistate - self.media_handle = media_handle - self.the_lists = the_lists - - def query_response(self): - with DbTxn(_("Remove Media Object"), self.db) as trans: - self.db.disable_signals() - - (person_list, family_list, event_list, - place_list, source_list, citation_list) = self.the_lists - - for handle in person_list: - person = self.db.get_person_from_handle(handle) - new_list = [photo for photo in person.get_media_list() - if photo.get_reference_handle() != self.media_handle] - person.set_media_list(new_list) - self.db.commit_person(person, trans) - - for handle in family_list: - family = self.db.get_family_from_handle(handle) - new_list = [photo for photo in family.get_media_list() - if photo.get_reference_handle() != self.media_handle] - family.set_media_list(new_list) - self.db.commit_family(family, trans) - - for handle in event_list: - event = self.db.get_event_from_handle(handle) - new_list = [photo for photo in event.get_media_list() - if photo.get_reference_handle() != self.media_handle] - event.set_media_list(new_list) - self.db.commit_event(event, trans) - - for handle in place_list: - place = self.db.get_place_from_handle(handle) - new_list = [photo for photo in place.get_media_list() - if photo.get_reference_handle() != self.media_handle] - place.set_media_list(new_list) - self.db.commit_place(place, trans) - - for handle in source_list: - source = self.db.get_source_from_handle(handle) - new_list = [photo for photo in source.get_media_list() - if photo.get_reference_handle() != self.media_handle] - source.set_media_list(new_list) - self.db.commit_source(source, trans) - - for handle in citation_list: - citation = self.db.get_citation_from_handle(handle) - new_list = [photo for photo in citation.get_media_list() - if photo.get_reference_handle() != self.media_handle] - citation.set_media_list(new_list) - self.db.commit_citation(citation, trans) - - self.db.enable_signals() - self.db.remove_media(self.media_handle, trans) diff --git a/gramps/gui/editors/editnote.py b/gramps/gui/editors/editnote.py index 81c5a208b..104771388 100644 --- a/gramps/gui/editors/editnote.py +++ b/gramps/gui/editors/editnote.py @@ -349,71 +349,3 @@ class EditNote(EditPrimary): self._do_close() if self.callback: self.callback(self.obj.get_handle()) - -class DeleteNoteQuery: - def __init__(self, dbstate, uistate, note, the_lists): - self.note = note - self.db = dbstate.db - self.uistate = uistate - self.the_lists = the_lists - - def query_response(self): - with DbTxn(_("Delete Note (%s)") % self.note.get_gramps_id(), - self.db) as trans: - self.db.disable_signals() - - (person_list, family_list, event_list, place_list, source_list, - citation_list, media_list, repo_list) = self.the_lists - - note_handle = self.note.get_handle() - - for handle in person_list: - person = self.db.get_person_from_handle(handle) - if person: - person.remove_note(note_handle) - self.db.commit_person(person, trans) - - for handle in family_list: - family = self.db.get_family_from_handle(handle) - if family: - family.remove_note(note_handle) - self.db.commit_family(family, trans) - - for handle in event_list: - event = self.db.get_event_from_handle(handle) - if event: - event.remove_note(note_handle) - self.db.commit_event(event, trans) - - for handle in place_list: - place = self.db.get_place_from_handle(handle) - if place: - place.remove_note(note_handle) - self.db.commit_place(place, trans) - - for handle in source_list: - source = self.db.get_source_from_handle(handle) - if source: - source.remove_note(note_handle) - self.db.commit_source(source, trans) - - for handle in citation_list: - citation = self.db.get_citation_from_handle(handle) - if citation: - citation.remove_note(note_handle) - self.db.commit_citation(citation, trans) - - for handle in media_list: - media = self.db.get_media_from_handle(handle) - if media: - media.remove_note(note_handle) - self.db.commit_media(media, trans) - - for handle in repo_list: - repo = self.db.get_repository_from_handle(handle) - if repo: - repo.remove_note(note_handle) - self.db.commit_repository(repo, trans) - - self.db.enable_signals() - self.db.remove_note(note_handle, trans) diff --git a/gramps/gui/editors/editplace.py b/gramps/gui/editors/editplace.py index d141bca62..8c685748c 100644 --- a/gramps/gui/editors/editplace.py +++ b/gramps/gui/editors/editplace.py @@ -379,44 +379,3 @@ class EditPlace(EditPrimary): self._do_close() if self.callback: self.callback(self.obj) - -#------------------------------------------------------------------------- -# -# DeletePlaceQuery -# -#------------------------------------------------------------------------- -class DeletePlaceQuery: - - def __init__(self, dbstate, uistate, place, person_list, family_list, - event_list): - self.db = dbstate.db - self.uistate = uistate - self.obj = place - self.person_list = person_list - self.family_list = family_list - self.event_list = event_list - - def query_response(self): - place_title = place_displayer.display(self.db, self.obj) - with DbTxn(_("Delete Place (%s)") % place_title, self.db) as trans: - self.db.disable_signals() - - place_handle = self.obj.get_handle() - - for handle in self.person_list: - person = self.db.get_person_from_handle(handle) - person.remove_handle_references('Place', place_handle) - self.db.commit_person(person, trans) - - for handle in self.family_list: - family = self.db.get_family_from_handle(handle) - family.remove_handle_references('Place', place_handle) - self.db.commit_family(family, trans) - - for handle in self.event_list: - event = self.db.get_event_from_handle(handle) - event.remove_handle_references('Place', place_handle) - self.db.commit_event(event, trans) - - self.db.enable_signals() - self.db.remove_place(place_handle, trans) diff --git a/gramps/gui/editors/editrepository.py b/gramps/gui/editors/editrepository.py index f95fccafa..1c8268063 100644 --- a/gramps/gui/editors/editrepository.py +++ b/gramps/gui/editors/editrepository.py @@ -210,23 +210,3 @@ class EditRepository(EditPrimary): self._do_close() if self.callback: self.callback(self.obj) - -class DeleteRepositoryQuery: - def __init__(self, dbstate, uistate, repository, sources): - self.obj = repository - self.db = dbstate.db - self.uistate = uistate - self.sources = sources - - def query_response(self): - with DbTxn(_("Delete Repository (%s)") % self.obj.get_name(), - self.db) as trans: - - repos_handle_list = [self.obj.get_handle()] - - for handle in self.sources: - source = self.db.get_source_from_handle(handle) - source.remove_repo_references(repos_handle_list) - self.db.commit_source(source, trans) - - self.db.remove_repository(self.obj.get_handle(), trans) diff --git a/gramps/gui/editors/editsource.py b/gramps/gui/editors/editsource.py index 9c14af25d..86e57aabe 100644 --- a/gramps/gui/editors/editsource.py +++ b/gramps/gui/editors/editsource.py @@ -231,81 +231,3 @@ class EditSource(EditPrimary): self._do_close() if self.callback: self.callback(self.obj) - -class DeleteSrcQuery: - def __init__(self, dbstate, uistate, source, the_lists): - self.source = source - self.db = dbstate.db - self.uistate = uistate - self.the_lists = the_lists - - def query_response(self): - with DbTxn(_("Delete Source (%s)") % self.source.get_title(), - self.db) as trans: - self.db.disable_signals() - - # we can have: - # object(CitationBase) -> Citation(source_handle) -> Source - # We first have to remove the CitationBase references to the - # Citation. Then we remove the Citations. (We don't need to - # remove the source_handle references to the Source, because we are - # removing the whole Citation). Then we can remove the Source - - (citation_list, citation_referents_list) = self.the_lists - # citation_list is a tuple of lists. Only the first, for Citations, - # exists. - citation_list = citation_list[0] - - # (1) delete the references to the citation - for (citation_handle, refs) in citation_referents_list: - LOG.debug('delete citation %s references %s' % - (citation_handle, refs)) - (person_list, family_list, event_list, place_list, source_list, - media_list, repo_list) = refs - - ctn_handle_list = [citation_handle] - - for handle in person_list: - person = self.db.get_person_from_handle(handle) - person.remove_citation_references(ctn_handle_list) - self.db.commit_person(person, trans) - - for handle in family_list: - family = self.db.get_family_from_handle(handle) - family.remove_citation_references(ctn_handle_list) - self.db.commit_family(family, trans) - - for handle in event_list: - event = self.db.get_event_from_handle(handle) - event.remove_citation_references(ctn_handle_list) - self.db.commit_event(event, trans) - - for handle in place_list: - place = self.db.get_place_from_handle(handle) - place.remove_citation_references(ctn_handle_list) - self.db.commit_place(place, trans) - - for handle in source_list: - source = self.db.get_source_from_handle(handle) - source.remove_citation_references(ctn_handle_list) - self.db.commit_source(source, trans) - - for handle in media_list: - media = self.db.get_media_from_handle(handle) - media.remove_citation_references(ctn_handle_list) - self.db.commit_media(media, trans) - - for handle in repo_list: - repo = self.db.get_repository_from_handle(handle) - repo.remove_citation_references(ctn_handle_list) - self.db.commit_repository(repo, trans) - - # (2) delete the actual citations - LOG.debug('remove the actual citations %s' % citation_list) - for citation_handle in citation_list: - LOG.debug("remove_citation %s" % citation_handle) - self.db.remove_citation(citation_handle, trans) - - # (3) delete the source - self.db.enable_signals() - self.db.remove_source(self.source.get_handle(), trans) diff --git a/gramps/gui/views/listview.py b/gramps/gui/views/listview.py index b44804f02..7ea0dacda 100644 --- a/gramps/gui/views/listview.py +++ b/gramps/gui/views/listview.py @@ -59,6 +59,7 @@ from .navigationview import NavigationView from ..uimanager import ActionGroup from ..columnorder import ColumnOrder from gramps.gen.config import config +from gramps.gen.db import DbTxn from gramps.gen.errors import WindowActiveError, FilterError, HandleError from ..filters import SearchBar from ..widgets.menuitem import add_menuitem @@ -66,7 +67,8 @@ from gramps.gen.const import CUSTOM_FILTERS from gramps.gen.utils.debug import profile from gramps.gen.utils.string import data_recover_msg from gramps.gen.plug import CATEGORY_QR_PERSON -from ..dialog import QuestionDialog, QuestionDialog3, ErrorDialog +from ..dialog import (QuestionDialog, QuestionDialog3, ErrorDialog, + MultiSelectDialog) from ..editors import FilterEditor from ..ddtargets import DdTargets from ..plug.quick import create_quickreport_menu, create_web_connect_menu @@ -532,57 +534,138 @@ class ListView(NavigationView): def get_column_widths(self): return [column.get_width() for column in self.columns] - def remove_selected_objects(self): + #################################################################### + # Object Delete functions + #################################################################### + # def remove(self): # this is over-ridden in each view + # """ + # must return the tuple of (object type and handle) for each + # selected item + # """ + # handles = self.selected_handles() + # ht_list = [('Person', hndl) for hndl in handles] + # self.remove_selected_objects(ht_list) + + def remove_selected_objects(self, ht_list=None): """ Function to remove selected objects """ - prompt = True - if len(self.selected_handles()) > 1: - ques = QuestionDialog3( - _("Multiple Selection Delete"), - _("More than one item has been selected for deletion. " - "Select the option indicating how to delete the items:"), - _("Delete All"), - _("Confirm Each Delete"), - parent=self.uistate.window) - res = ques.run() - if res == -1: # Cancel - return - else: - prompt = not res # we prompt on 'Confirm Each Delete' + if len(ht_list) == 1: + obj = self.dbstate.db.method( + "get_%s_from_handle", ht_list[0][0])(ht_list[0][1]) + msg1 = self._message1_format(obj) + msg2 = self._message2_format(obj) + msg2 = "%s %s" % (msg2, data_recover_msg) + QuestionDialog(msg1, + msg2, + _('_Delete'), + lambda: self.delete_object_response(obj, parent=self.uistate.window), + parent=self.uistate.window) + else: + MultiSelectDialog(self._message1_format, + self._message2_format, + ht_list, + lambda x: self.dbstate.db.method( + "get_%s_from_handle", x[0])(x[1]), + yes_func=self.delete_object_response, + multi_yes_func=self.delete_multi_object_response, + parent=self.uistate.window) - if not prompt: - self.uistate.set_busy_cursor(True) + def _message1_format(self, obj): + """ + Header format for remove dialogs. + """ + return _('Delete {type} [{gid}]?').format( + type=_(obj.__class__.__name__), gid=obj.gramps_id) - for handle in self.selected_handles(): - (query, is_used, object) = self.remove_object_from_handle(handle) - if prompt: - if is_used: - msg = _('This item is currently being used. ' - 'Deleting it will remove it from the database and ' - 'from all other items that reference it.') - else: - msg = _('Deleting item will remove it from the database.') + def _message2_format(self, _obj): + """ + Detailed message format for the remove dialogs. + """ + return _('Deleting item will remove it from the database.') - msg += ' ' + data_recover_msg - #descr = object.get_description() - #if descr == "": - descr = object.get_gramps_id() - ques = QuestionDialog3(_('Delete %s?') % descr, msg, - _('_Yes'), _('_No'), - parent=self.uistate.window) - res = ques.run() - if res == -1: # Cancel - return - elif res: # If true, perfom the delete - self.uistate.set_busy_cursor(True) - query.query_response() - self.uistate.set_busy_cursor(False) - else: - query.query_response() + def _message3_format(self, obj): + """ + Transaction label format + """ + return "%s %s [%s]" % (_("Delete"), _(obj.__class__.__name__), + obj.get_gramps_id()) - if not prompt: - self.uistate.set_busy_cursor(False) + def delete_object_response(self, obj, parent=None): + """ + Delete the object from the database. + """ + with DbTxn(self._message3_format(obj), self.dbstate.db) as trans: + #self.db.disable_signals() + self.remove_object_from_handle( + obj.__class__.__name__, obj.handle, trans, in_use_prompt=True, parent=parent) + #self.dbstate.db.enable_signals() + self.uistate.set_busy_cursor(False) + + def delete_multi_object_response(self, ht_list=None, parent=None): + """ + Deletes multiple objects from the database. + """ + # set the busy cursor, so the user knows that we are working + self.uistate.set_busy_cursor(True) + self.uistate.progress.show() + self.uistate.push_message(self.dbstate, _("Processing...")) + hndl_cnt = len(ht_list) / 100 + _db = self.dbstate.db + _db.disable_signals() + + # create the transaction + with DbTxn('', _db) as trans: + for (indx, item) in enumerate(ht_list): + result = self.remove_object_from_handle( + *item, trans, in_use_prompt=False, parent=parent) + self.uistate.pulse_progressbar(indx / hndl_cnt) + if result == -1: + break + trans.set_description(_("Multiple Selection Delete")) + + _db.enable_signals() + _db.request_rebuild() + self.uistate.progress.hide() + self.uistate.set_busy_cursor(False) + + def remove_object_from_handle(self, obj_type, handle, + trans, in_use_prompt=False, parent=None): + """ + deletes a single object from database + """ + obj = self.dbstate.db.method("get_%s_from_handle", obj_type)(handle) + bl_list = list(self.dbstate.db.find_backlink_handles(handle)) + if in_use_prompt: + res = self._in_use_prompt(obj, bl_list, parent=parent) + if res != 1: # Cancel or No + return res + # perfom the cleanup + for ref_type, ref_hndl in bl_list: + ref_obj = self.dbstate.db.method( + "get_%s_from_handle", ref_type)(ref_hndl) + ref_obj.remove_handle_references(obj_type, [handle]) + self.dbstate.db.method("commit_%s", ref_type)(ref_obj, trans) + self.dbstate.db.method("remove_%s", obj_type)( + obj.get_handle(), trans) + + def _in_use_prompt(self, obj, bl_list, parent=None): + """ + Prompt user if he wants to continue becasue in use + """ + if bl_list: + msg = _('This item is currently being used. ' + 'Deleting it will remove it from the database and ' + 'from all other items that reference it.') + else: + msg = _('Deleting item will remove it from the database.') + + msg += ' ' + data_recover_msg + descr = obj.get_gramps_id() + ques = QuestionDialog3(_('Delete %s?') % descr, msg, + _('_Yes'), _('_No'), + parent=parent) + return ques.run() def blist(self, store, path, iter_, sel_list): '''GtkTreeSelectionForeachFunc @@ -1188,12 +1271,6 @@ class ListView(NavigationView): Template function to allow the merger of two objects. """ - @abstractmethod - def remove_object_from_handle(self, handle): - """ - Template function to allow the removal of an object by its handle - """ - def open_all_nodes(self, *obj): """ Method for Treeviews to open all groups diff --git a/gramps/plugins/lib/libpersonview.py b/gramps/plugins/lib/libpersonview.py index 23748411d..0c6ad005e 100644 --- a/gramps/plugins/lib/libpersonview.py +++ b/gramps/plugins/lib/libpersonview.py @@ -46,12 +46,10 @@ _LOG = logging.getLogger(".gui.personview") # #------------------------------------------------------------------------- from gramps.gen.lib import Person, Surname -from gramps.gen.db import DbTxn from gramps.gui.views.listview import ListView, TEXT, MARKUP, ICON from gramps.gui.uimanager import ActionGroup -from gramps.gen.utils.string import data_recover_msg from gramps.gen.display.name import displayer as name_displayer -from gramps.gui.dialog import ErrorDialog, MultiSelectDialog, QuestionDialog +from gramps.gui.dialog import ErrorDialog from gramps.gen.errors import WindowActiveError from gramps.gui.views.bookmarks import PersonBookmarks from gramps.gen.config import config @@ -443,27 +441,8 @@ class BasePersonView(ListView): Remove a person from the database. """ handles = self.selected_handles() - if len(handles) == 1: - person = self._lookup_person(handles[0]) - msg1 = self._message1_format(person) - msg2 = self._message2_format(person) - msg2 = "%s %s" % (msg2, data_recover_msg) - # This gets person to delete self.active_person: - QuestionDialog(msg1, - msg2, - _('_Delete Person'), - self.delete_person_response, - parent=self.uistate.window) - else: - # Ask to delete; option to cancel, delete rest - # This gets person to delete from parameter - MultiSelectDialog(self._message1_format, - self._message2_format, - handles, - self._lookup_person, - yes_func=self.delete_person_response, - multi_yes_func=self.delete_multi_person_response, - parent=self.uistate.window) + ht_list = [('Person', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def _message1_format(self, person): return _('Delete %s?') % (name_displayer.display(person) + @@ -473,65 +452,19 @@ class BasePersonView(ListView): return _('Deleting the person will remove the person ' 'from the database.') - def _lookup_person(self, handle): + def _message3_format(self, person): """ - Get the next person from handle. + Transaction label format + """ + return _("Delete Person (%s)") % name_displayer.display(person) + + def remove_object_from_handle(self, _obj_type, handle, + trans, in_use_prompt=False, parent=None): + """ + deletes a single object from database """ person = self.dbstate.db.get_person_from_handle(handle) - self.active_person = person - return person - - def delete_person_response(self, person=None): - """ - Deletes the person from the database. - """ - # set the busy cursor, so the user knows that we are working - self.uistate.set_busy_cursor(True) - - # create the transaction - with DbTxn('', self.dbstate.db) as trans: - - # create name to save - person = self.active_person - active_name = _("Delete Person (%s)") % name_displayer.display(person) - - # delete the person from the database - # Above will emit person-delete, which removes the person via - # callback to the model, so row delete is signaled - self.dbstate.db.delete_person_from_database(person, trans) - trans.set_description(active_name) - - self.uistate.set_busy_cursor(False) - - def delete_multi_person_response(self, handles=None): - """ - Deletes multiple persons from the database. - """ - # set the busy cursor, so the user knows that we are working - self.uistate.set_busy_cursor(True) - self.uistate.progress.show() - self.uistate.push_message(self.dbstate, _("Processing...")) - hndl_cnt = len(handles) / 100 - self.dbstate.db.disable_signals() - - # create the transaction - with DbTxn('', self.dbstate.db) as trans: - for (indx, handle) in enumerate(handles): - person = self.dbstate.db.get_person_from_handle(handle) - self.dbstate.db.delete_person_from_database(person, trans) - self.uistate.pulse_progressbar(indx / hndl_cnt) - trans.set_description(_("Multiple Selection Delete")) - - self.dbstate.db.enable_signals() - self.dbstate.db.request_rebuild() - self.uistate.progress.hide() - self.uistate.set_busy_cursor(False) - - def remove_object_from_handle(self, handle): - """ - The remove_selected_objects method is not called in this view. - """ - pass + self.dbstate.db.delete_person_from_database(person, trans) def define_actions(self): """ diff --git a/gramps/plugins/lib/libplaceview.py b/gramps/plugins/lib/libplaceview.py index 9401d35bf..4a34ba495 100644 --- a/gramps/plugins/lib/libplaceview.py +++ b/gramps/plugins/lib/libplaceview.py @@ -45,7 +45,7 @@ from gramps.gen.config import config from gramps.gui.dialog import ErrorDialog from gramps.gui.pluginmanager import GuiPluginManager from gramps.gui.ddtargets import DdTargets -from gramps.gui.editors import EditPlace, DeletePlaceQuery +from gramps.gui.editors import EditPlace from gramps.gui.filters.sidebar import PlaceSidebarFilter from gramps.gui.merge import MergePlace from gramps.gen.plug import CATEGORY_QR_PLACE @@ -503,34 +503,17 @@ class PlaceBaseView(ListView): pass def remove(self, *obj): + ht_list = [] for handle in self.selected_handles(): - for link in self.dbstate.db.find_backlink_handles(handle,['Place']): + for _link in self.dbstate.db.find_backlink_handles(handle, + ['Place']): msg = _("Cannot delete place.") - msg2 = _("This place is currently referenced by another place. " - "First remove the places it contains.") + msg2 = _("This place is currently referenced by another place." + " First remove the places it contains.") ErrorDialog(msg, msg2, parent=self.uistate.window) return - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - person_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Person'])] - - family_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Family'])] - - event_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle,['Event'])] - - object = self.dbstate.db.get_place_from_handle(handle) - query = DeletePlaceQuery(self.dbstate, self.uistate, object, - person_list, family_list, event_list) - - is_used = len(person_list) + len(family_list) + len(event_list) > 0 - return (query, is_used, object) + ht_list.append(('Place', handle)) + self.remove_selected_objects(ht_list) def edit(self, *obj): for handle in self.selected_handles(): diff --git a/gramps/plugins/lib/libsourceview.py b/gramps/plugins/lib/libsourceview.py new file mode 100644 index 000000000..331a04800 --- /dev/null +++ b/gramps/plugins/lib/libsourceview.py @@ -0,0 +1,104 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2020 Paul Culley +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +""" +Library common to SourceView and CitationTreeView +""" +from gramps.gen.errors import HandleError + + +#------------------------------------------------------------------------- +# +# SourceLibView +# +#------------------------------------------------------------------------- +class LibSourceView(): + """ + This contains the common delete related methods for the views. + It was written specifically for CitationTreeView, but works for SourceView + as well; there just will never be an citation handles in selection. + + This must be placed before Listview in the MRO to properly override the + included methods. For example: + class SourceView(LibSourceView, ListView) + """ + + def remove(self, *obj): + """ + Method called when deleting source(s) or citations from the views. + """ + ht_list = [] + handles = self.selected_handles() + for hndl in handles: + if self.dbstate.db.has_source_handle(hndl): + ht_list.append(('Source', hndl)) + else: + ht_list.append(('Citation', hndl)) + self.remove_selected_objects(ht_list) + + def remove_object_from_handle(self, obj_type, handle, + trans, in_use_prompt=False, parent=None): + """ + deletes a single object from database + """ + try: # need this in case user selects both source and its Citations + obj = self.dbstate.db.method( + "get_%s_from_handle", obj_type)(handle) + except HandleError: + return + bl_list = list(self.dbstate.db.find_backlink_handles(handle)) + if in_use_prompt: + res = self._in_use_prompt(obj, bl_list, parent=parent) + if res != 1: # Cancel or No + return res + # perfom the cleanup + if obj_type == 'Source': + # we need to delete all back linked citations, so sort these out + cit_list = [] + nbl_list = [] + for item in bl_list: + if item[0] == 'Citation': + cit_list.append(item[1]) + else: # save any other back links for later. + nbl_list.append(item) + # now lets go through citations and clean up their back-refs + hndl_cnt = len(cit_list) / 100 + for indx, cit_hndl in enumerate(cit_list): + # the following introduces another pass with the progressbar + # to keep the user from wondering what is happening if there + # are a lot of citations on the source + self.uistate.pulse_progressbar(indx / hndl_cnt) + cit_bl_list = list(self.dbstate.db.find_backlink_handles( + cit_hndl)) + for ref_type, ref_hndl in cit_bl_list: + ref_obj = self.dbstate.db.method( + "get_%s_from_handle", ref_type)(ref_hndl) + ref_obj.remove_handle_references(obj_type, [cit_hndl]) + self.dbstate.db.method("commit_%s", ref_type)( + ref_obj, trans) + # and delete the citation + self.dbstate.db.remove_citation(cit_hndl, trans) + bl_list = nbl_list # to clean up any other back refs to source + for ref_type, ref_hndl in bl_list: + ref_obj = self.dbstate.db.method( + "get_%s_from_handle", ref_type)(ref_hndl) + ref_obj.remove_handle_references(obj_type, [handle]) + self.dbstate.db.method("commit_%s", ref_type)(ref_obj, trans) + # and delete the source + self.dbstate.db.remove_source(handle, trans) diff --git a/gramps/plugins/view/citationlistview.py b/gramps/plugins/view/citationlistview.py index 44188981b..dd1b3e3ea 100644 --- a/gramps/plugins/view/citationlistview.py +++ b/gramps/plugins/view/citationlistview.py @@ -47,12 +47,11 @@ from gramps.gui.views.treemodels.citationlistmodel import CitationListModel from gramps.gen.plug import CATEGORY_QR_CITATION from gramps.gen.lib import Citation, Source from gramps.gui.views.listview import ListView, TEXT, MARKUP, ICON -from gramps.gen.utils.db import get_citation_referents from gramps.gui.views.bookmarks import CitationBookmarks from gramps.gen.errors import WindowActiveError from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import ErrorDialog -from gramps.gui.editors import EditCitation, DeleteCitationQuery +from gramps.gui.editors import EditCitation from gramps.gui.filters.sidebar import CitationSidebarFilter from gramps.gui.merge import MergeCitation @@ -367,15 +366,9 @@ class CitationListView(ListView): pass def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - the_lists = get_citation_referents(handle, self.dbstate.db) - object = self.dbstate.db.get_citation_from_handle(handle) - query = DeleteCitationQuery(self.dbstate, self.uistate, object, - the_lists) - is_used = any(the_lists) - return (query, is_used, object) + handles = self.selected_handles() + ht_list = [('Citation', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def edit(self, *obj): """ diff --git a/gramps/plugins/view/citationtreeview.py b/gramps/plugins/view/citationtreeview.py index ddb4248b3..8f71535b3 100644 --- a/gramps/plugins/view/citationtreeview.py +++ b/gramps/plugins/view/citationtreeview.py @@ -48,16 +48,14 @@ from gramps.gui.views.treemodels.citationtreemodel import CitationTreeModel from gramps.gen.plug import CATEGORY_QR_SOURCE_OR_CITATION from gramps.gen.lib import Citation, Source from gramps.gui.views.listview import ListView -from gramps.gen.utils.db import (get_source_and_citation_referents, - get_citation_referents) from gramps.gui.views.bookmarks import CitationBookmarks from gramps.gen.errors import WindowActiveError, HandleError from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import ErrorDialog -from gramps.gui.editors import EditCitation, DeleteCitationQuery, EditSource, \ - DeleteSrcQuery +from gramps.gui.editors import EditCitation, EditSource from gramps.gui.filters.sidebar import SourceSidebarFilter from gramps.gui.merge import MergeCitation, MergeSource +from gramps.plugins.lib.libsourceview import LibSourceView #------------------------------------------------------------------------- # @@ -67,12 +65,13 @@ from gramps.gui.merge import MergeCitation, MergeSource from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext + #------------------------------------------------------------------------- # # PlaceTreeView # #------------------------------------------------------------------------- -class CitationTreeView(ListView): +class CitationTreeView(LibSourceView, ListView): """ A hierarchical view of sources with citations below them. """ @@ -590,28 +589,6 @@ class CitationTreeView(ListView): WarningDialog(_("Cannot share this reference"), self.__blocked_text(), parent=self.uistate.window) -# - def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - # The handle will either be a Source handle or a Citation handle - source, citation = self.get_source_or_citation(handle) - if citation: - the_lists = get_citation_referents(handle, self.dbstate.db) - query = DeleteCitationQuery(self.dbstate, self.uistate, citation, - the_lists) - is_used = any(the_lists) - return (query, is_used, citation) - else: - the_lists = get_source_and_citation_referents(handle, - self.dbstate.db) - LOG.debug('the_lists %s' % [the_lists]) - - query = DeleteSrcQuery(self.dbstate, self.uistate, source, - the_lists) - is_used = any(the_lists) - return (query, is_used, source) def edit(self, *obj): """ diff --git a/gramps/plugins/view/eventview.py b/gramps/plugins/view/eventview.py index 935618de3..2622b45c3 100644 --- a/gramps/plugins/view/eventview.py +++ b/gramps/plugins/view/eventview.py @@ -40,14 +40,12 @@ _LOG = logging.getLogger(".plugins.eventview") from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext -from gramps.gui.dialog import ErrorDialog, MultiSelectDialog, QuestionDialog -from gramps.gen.db import DbTxn +from gramps.gui.dialog import ErrorDialog from gramps.gen.errors import WindowActiveError from gramps.gen.lib import Event -from gramps.gen.utils.string import data_recover_msg from gramps.gui.ddtargets import DdTargets -from gramps.gui.editors import EditEvent, DeleteEventQuery +from gramps.gui.editors import EditEvent from gramps.gui.filters.sidebar import EventSidebarFilter from gramps.gui.merge import MergeEvent from gramps.gen.plug import CATEGORY_QR_EVENT @@ -376,24 +374,8 @@ class EventView(ListView): Method called when deleting event(s) from the event view. """ handles = self.selected_handles() - if len(handles) == 1: - event = self.dbstate.db.get_event_from_handle(handles[0]) - msg1 = self._message1_format(event) - msg2 = self._message2_format(event) - msg2 = "%s %s" % (msg2, data_recover_msg) - QuestionDialog(msg1, - msg2, - _('_Delete Event'), - lambda: self.delete_event_response(event), - parent=self.uistate.window) - else: - MultiSelectDialog(self._message1_format, - self._message2_format, - handles, - self.dbstate.db.get_event_from_handle, - yes_func=self.delete_event_response, - multi_yes_func=self.delete_multi_event_response, - parent=self.uistate.window) + ht_list = [('Event', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def _message1_format(self, event): """ @@ -402,73 +384,6 @@ class EventView(ListView): return _('Delete {type} [{gid}]?').format(type=str(event.type), gid=event.gramps_id) - def _message2_format(self, event): - """ - Detailed message format for the remove dialogs. - """ - return _('Deleting item will remove it from the database.') - - def delete_event_response(self, event): - """ - Delete the event from the database. - """ - person_list = [item[1] for item in - self.dbstate.db.find_backlink_handles(event.handle, ['Person'])] - family_list = [item[1] for item in - self.dbstate.db.find_backlink_handles(event.handle, ['Family'])] - - query = DeleteEventQuery(self.dbstate, self.uistate, event, - person_list, family_list) - query.query_response() - - def delete_multi_event_response(self, handles=None): - """ - Deletes multiple events from the database. - """ - # set the busy cursor, so the user knows that we are working - self.uistate.set_busy_cursor(True) - self.uistate.progress.show() - self.uistate.push_message(self.dbstate, _("Processing...")) - hndl_cnt = len(handles) / 100 - _db = self.dbstate.db - _db.disable_signals() - - # create the transaction - with DbTxn('', _db) as trans: - for (indx, handle) in enumerate(handles): - ev_handle_list = [handle] - - person_list = [ - item[1] for item in - _db.find_backlink_handles(handle, ['Person'])] - for hndl in person_list: - person = _db.get_person_from_handle(hndl) - person.remove_handle_references('Event', ev_handle_list) - _db.commit_person(person, trans) - - family_list = [ - item[1] for item in - _db.find_backlink_handles(handle, ['Family'])] - for hndl in family_list: - family = _db.get_family_from_handle(hndl) - family.remove_handle_references('Event', ev_handle_list) - _db.commit_family(family, trans) - - _db.remove_event(handle, trans) - self.uistate.pulse_progressbar(indx / hndl_cnt) - trans.set_description(_("Multiple Selection Delete")) - - _db.enable_signals() - _db.request_rebuild() - self.uistate.progress.hide() - self.uistate.set_busy_cursor(False) - - def remove_object_from_handle(self, handle): - """ - The remove_selected_objects method is not called in this view. - """ - pass - def edit(self, *obj): for handle in self.selected_handles(): event = self.dbstate.db.get_event_from_handle(handle) diff --git a/gramps/plugins/view/familyview.py b/gramps/plugins/view/familyview.py index 9ac8529c8..61ffc56b8 100644 --- a/gramps/plugins/view/familyview.py +++ b/gramps/plugins/view/familyview.py @@ -359,26 +359,9 @@ class FamilyView(ListView): """ Method called when deleting a family from a family view. """ - from gramps.gui.dialog import QuestionDialog, MultiSelectDialog - from gramps.gen.utils.string import data_recover_msg handles = self.selected_handles() - if len(handles) == 1: - family = self.dbstate.db.get_family_from_handle(handles[0]) - msg1 = self._message1_format(family) - msg2 = self._message2_format(family) - msg2 = "%s %s" % (msg2, data_recover_msg) - QuestionDialog(msg1, - msg2, - _('_Delete Family'), - lambda: self.delete_family_response(family), - parent=self.uistate.window) - else: - MultiSelectDialog(self._message1_format, - self._message2_format, - handles, - self.dbstate.db.get_family_from_handle, - yes_func=self.delete_family_response, - parent=self.uistate.window) + ht_list = [('Family', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def _message1_format(self, family): """ @@ -387,32 +370,13 @@ class FamilyView(ListView): return _('Delete %s?') % (_('family') + (" [%s]" % family.gramps_id)) - def _message2_format(self, family): + def remove_object_from_handle(self, _obj_type, handle, + trans, in_use_prompt=False, parent=None): """ - Detailed message format for the remove dialogs. + deletes a single object from database """ - return _('Deleting item will remove it from the database.') - - def delete_family_response(self, family): - """ - Deletes the family from the database. Callback to remove - dialogs. - """ - from gramps.gen.db import DbTxn - # set the busy cursor, so the user knows that we are working - self.uistate.set_busy_cursor(True) - # create the transaction - with DbTxn('', self.dbstate.db) as trans: - gramps_id = family.gramps_id - self.dbstate.db.remove_family_relationships(family.handle, trans) - trans.set_description(_("Family [%s]") % gramps_id) - self.uistate.set_busy_cursor(False) - - def remove_object_from_handle(self, handle): - """ - The remove_selected_objects method is not called in this view. - """ - pass + family = self.dbstate.db.get_family_from_handle(handle) + self.dbstate.db.remove_family_relationships(handle, trans) def edit(self, *obj): for handle in self.selected_handles(): diff --git a/gramps/plugins/view/mediaview.py b/gramps/plugins/view/mediaview.py index 582321e05..7744ef72b 100644 --- a/gramps/plugins/view/mediaview.py +++ b/gramps/plugins/view/mediaview.py @@ -54,12 +54,11 @@ from gramps.gui.views.treemodels import MediaModel from gramps.gen.config import config from gramps.gen.utils.file import (media_path, relative_path, media_path_full, create_checksum) -from gramps.gen.utils.db import get_media_referents from gramps.gui.views.bookmarks import MediaBookmarks from gramps.gen.mime import get_type, is_valid_type from gramps.gen.lib import Media from gramps.gen.db import DbTxn -from gramps.gui.editors import EditMedia, DeleteMediaQuery +from gramps.gui.editors import EditMedia from gramps.gen.errors import WindowActiveError from gramps.gui.filters.sidebar import MediaSidebarFilter from gramps.gui.merge import MergeMedia @@ -456,18 +455,9 @@ class MediaView(ListView): pass def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - """ - Remove the selected objects from the database after getting - user verification. - """ - the_lists = get_media_referents(handle, self.dbstate.db) - object = self.dbstate.db.get_media_from_handle(handle) - query = DeleteMediaQuery(self.dbstate, self.uistate, handle, the_lists) - is_used = any(the_lists) - return (query, is_used, object) + handles = self.selected_handles() + ht_list = [('Media', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def edit(self, *obj): """ diff --git a/gramps/plugins/view/noteview.py b/gramps/plugins/view/noteview.py index b4d24f266..9e0e3f2a2 100644 --- a/gramps/plugins/view/noteview.py +++ b/gramps/plugins/view/noteview.py @@ -46,7 +46,6 @@ from gi.repository import Gtk #------------------------------------------------------------------------- from gramps.gui.views.listview import ListView, TEXT, MARKUP, ICON from gramps.gui.views.treemodels import NoteModel -from gramps.gen.utils.db import get_note_referents from gramps.gen.errors import WindowActiveError from gramps.gui.views.bookmarks import NoteBookmarks from gramps.gen.config import config @@ -54,7 +53,7 @@ from gramps.gen.lib import Note from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import ErrorDialog from gramps.gui.filters.sidebar import NoteSidebarFilter -from gramps.gui.editors import EditNote, DeleteNoteQuery +from gramps.gui.editors import EditNote from gramps.gui.merge import MergeNote from gramps.gen.plug import CATEGORY_QR_NOTE @@ -330,14 +329,9 @@ class NoteView(ListView): pass def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - the_lists = get_note_referents(handle, self.dbstate.db) - object = self.dbstate.db.get_note_from_handle(handle) - query = DeleteNoteQuery(self.dbstate, self.uistate, object, the_lists) - is_used = any(the_lists) - return (query, is_used, object) + handles = self.selected_handles() + ht_list = [('Note', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def edit(self, *obj): for handle in self.selected_handles(): diff --git a/gramps/plugins/view/repoview.py b/gramps/plugins/view/repoview.py index bc95c456d..3183707ff 100644 --- a/gramps/plugins/view/repoview.py +++ b/gramps/plugins/view/repoview.py @@ -40,7 +40,7 @@ from gramps.gui.views.treemodels import RepositoryModel from gramps.gui.views.bookmarks import RepoBookmarks from gramps.gen.errors import WindowActiveError from gramps.gen.config import config -from gramps.gui.editors import EditRepository, DeleteRepositoryQuery +from gramps.gui.editors import EditRepository from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import ErrorDialog from gramps.gui.filters.sidebar import RepoSidebarFilter @@ -333,17 +333,12 @@ class RepositoryView(ListView): EditRepository(self.dbstate, self.uistate, [], Repository()) def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - source_list = [ - item[1] for item in - self.dbstate.db.find_backlink_handles(handle, ['Source'])] - object = self.dbstate.db.get_repository_from_handle(handle) - query = DeleteRepositoryQuery(self.dbstate, self.uistate, object, - source_list) - is_used = len(source_list) > 0 - return (query, is_used, object) + """ + Method called when deleting repo(s) from the repo view. + """ + handles = self.selected_handles() + ht_list = [('Repository', hndl) for hndl in handles] + self.remove_selected_objects(ht_list) def edit(self, *obj): for handle in self.selected_handles(): diff --git a/gramps/plugins/view/sourceview.py b/gramps/plugins/view/sourceview.py index 081302bff..06526771e 100644 --- a/gramps/plugins/view/sourceview.py +++ b/gramps/plugins/view/sourceview.py @@ -41,15 +41,15 @@ from gramps.gen.lib import Source from gramps.gen.config import config from gramps.gui.views.listview import ListView, TEXT, MARKUP, ICON from gramps.gui.views.treemodels import SourceModel -from gramps.gen.utils.db import get_source_and_citation_referents from gramps.gui.views.bookmarks import SourceBookmarks from gramps.gen.errors import WindowActiveError from gramps.gui.ddtargets import DdTargets from gramps.gui.dialog import ErrorDialog -from gramps.gui.editors import EditSource, DeleteSrcQuery +from gramps.gui.editors import EditSource from gramps.gui.filters.sidebar import SourceSidebarFilter from gramps.gui.merge import MergeSource from gramps.gen.plug import CATEGORY_QR_SOURCE +from gramps.plugins.lib.libsourceview import LibSourceView #------------------------------------------------------------------------- # @@ -65,7 +65,7 @@ _ = glocale.translation.sgettext # SourceView # #------------------------------------------------------------------------- -class SourceView(ListView): +class SourceView(LibSourceView, ListView): """ sources listview class """ COL_TITLE = 0 @@ -318,18 +318,6 @@ class SourceView(ListView): def add(self, *obj): EditSource(self.dbstate, self.uistate, [], Source()) - def remove(self, *obj): - self.remove_selected_objects() - - def remove_object_from_handle(self, handle): - the_lists = get_source_and_citation_referents(handle, self.dbstate.db) - LOG.debug('the_lists %s' % [the_lists]) - - object = self.dbstate.db.get_source_from_handle(handle) - query = DeleteSrcQuery(self.dbstate, self.uistate, object, the_lists) - is_used = any(the_lists) - return (query, is_used, object) - def edit(self, *obj): for handle in self.selected_handles(): source = self.dbstate.db.get_source_from_handle(handle) From 11d5a64643fc7fbc524c2d8d96ccf0e5c7598d1a Mon Sep 17 00:00:00 2001 From: prculley Date: Sat, 8 Aug 2020 11:37:00 -0500 Subject: [PATCH 2/6] Fix MultiSelectDialog for two issues; 1) it would rerun an 'ok' after a 'no' on same object if the user selected 'Use answer for rest'. 2) Dialogs called within an operation would have wrong parent. --- gramps/gui/dialog.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/gramps/gui/dialog.py b/gramps/gui/dialog.py index 638e11245..e14145a8b 100644 --- a/gramps/gui/dialog.py +++ b/gramps/gui/dialog.py @@ -437,11 +437,29 @@ class MissingMediaDialog: parent=self.top) return True + class MultiSelectDialog: + """ + Allows a Yes, No, Cancel dialog that includes a checkbox + with "Use this answer for the rest of the items" that works with 'Yes' + """ def __init__(self, msg1_func, msg2_func, items, lookup, cancel_func=None, no_func=None, yes_func=None, multi_yes_func=None, parent=None): """ + msg1_func a function to display big bold text at top + msg2_func a function to display normal text at center of dialog + items a list of objects (often handles) passed to lookup + Must be a list, not a generator + lookup A function that looks up an object to pass to yes/no/multi_func + cancel_func A function or None called on cancel. + yes_func A function or None called on Yes. + no_func A function or None called on No. + multi_yes_func A function or None called on Yes with + "use this answer for rest" checked. + The above xxx_func calls will have parameters: + object from lookup function + "parent=" set to this dialog for transient parent purposes. """ self.xml = Glade(toplevel='multiselectdialog') @@ -468,7 +486,7 @@ class MultiSelectDialog: self.top.connect('delete_event', self.warn) default_action = 0 - for selected in items: + for indx, selected in enumerate(items): item = self.lookup(selected) if default_action == 0: msg1 = self.msg1_func(item) @@ -491,22 +509,22 @@ class MultiSelectDialog: if check_button.get_active(): # run the multiple yes if 'do remainder' is checked if multi_yes_func and response == 3: - multi_yes_func(items) + multi_yes_func(items[indx:], parent=self.top) break default_action = response else: response = default_action - ### Now do it - if response == 1: # Cancel + # Now do it + if response == 1: # Cancel if self.cancel_func: - self.cancel_func(item) + self.cancel_func(item, parent=self.top) break - elif response == 2: # No + elif response == 2: # No if self.no_func: - self.no_func(item) - elif response == 3: # Yes + self.no_func(item, parent=self.top) + elif response == 3: # Yes if self.yes_func: - self.yes_func(item) + self.yes_func(item, parent=self.top) self.top.destroy() if parent and parent_modal: parent.set_modal(True) From f57c38e220d5431a2a93c83d843f4a429c7cb73a Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 10 Aug 2020 11:27:46 -0500 Subject: [PATCH 3/6] Support Note LINKS as backlinks --- gramps/gen/lib/note.py | 9 ++++++++- gramps/gui/editors/displaytabs/backrefmodel.py | 9 +++++++++ gramps/gui/utils.py | 11 ++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/gramps/gen/lib/note.py b/gramps/gen/lib/note.py index 05a4d7506..5161891fd 100644 --- a/gramps/gen/lib/note.py +++ b/gramps/gen/lib/note.py @@ -161,7 +161,14 @@ class Note(BasicPrimaryObject): :returns: List of (classname, handle) tuples for referenced objects. :rtype: list """ - return self.get_referenced_tag_handles() + reflist = [] + for dom, obj, prop, hndl in self.get_links(): + if dom != "gramps" or prop != "handle": + continue + else: + reflist.append((obj, hndl)) + reflist.extend(self.get_referenced_tag_handles()) + return reflist def merge(self, acquisition): """ diff --git a/gramps/gui/editors/displaytabs/backrefmodel.py b/gramps/gui/editors/displaytabs/backrefmodel.py index a8044493f..a3a922497 100644 --- a/gramps/gui/editors/displaytabs/backrefmodel.py +++ b/gramps/gui/editors/displaytabs/backrefmodel.py @@ -134,6 +134,15 @@ class BackRefModel(Gtk.ListStore): name = p.get_name() gid = p.gramps_id handle = p.handle + elif dtype == 'Note': + p = self.db.get_note_from_handle(ref[1]) + if not p: + continue + name = " ".join(p.get().split()) + if len(name) > 80: + name = name[:80] + "..." + gid = p.gramps_id + handle = p.handle else: p = self.db.get_media_from_handle(ref[1]) if not p: diff --git a/gramps/gui/utils.py b/gramps/gui/utils.py index d6346fb9d..ba5315924 100644 --- a/gramps/gui/utils.py +++ b/gramps/gui/utils.py @@ -569,12 +569,14 @@ def get_link_color(context): return rgb_to_hex((col.red, col.green, col.blue)) + def edit_object(dbstate, uistate, reftype, ref): """ Invokes the appropriate editor for an object type and given handle. """ from .editors import (EditEvent, EditPerson, EditFamily, EditSource, - EditPlace, EditMedia, EditRepository, EditCitation) + EditPlace, EditMedia, EditRepository, EditCitation, + EditNote) if reftype == 'Person': try: @@ -642,6 +644,13 @@ def edit_object(dbstate, uistate, reftype, ref): EditRepository(dbstate, uistate, [], repo) except WindowActiveError: pass + elif reftype == 'Note': + try: + note = dbstate.db.get_note_from_handle(ref) + EditNote(dbstate, uistate, [], note) + except WindowActiveError: + pass + #------------------------------------------------------------------------- # From 1d2919634493d4ce8304b2eeb6123d5e034a51cb Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 10 Aug 2020 11:29:53 -0500 Subject: [PATCH 4/6] Note LINK support for deletes of other objects --- gramps/gen/lib/note.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/gramps/gen/lib/note.py b/gramps/gen/lib/note.py index 5161891fd..71280e5f0 100644 --- a/gramps/gen/lib/note.py +++ b/gramps/gen/lib/note.py @@ -170,6 +170,29 @@ class Note(BasicPrimaryObject): reflist.extend(self.get_referenced_tag_handles()) return reflist + def remove_handle_references(self, classname, handle_list): + """ + Remove all references in this object to object handles in the list. + + :param classname: The name of the primary object class. + :type classname: str + :param handle_list: The list of handles to be removed. + :type handle_list: str + + If the link is in the styled text, we just remove the style for that + link. + """ + tags = [] + for styledtext_tag in self.text.get_tags(): + if(styledtext_tag.name == StyledTextTagType.LINK and + styledtext_tag.value.startswith("gramps://")): + obj, prop, value = styledtext_tag.value[9:].split("/", 2) + if obj == classname and prop == 'handle' and \ + value in handle_list: + continue + tags.append(styledtext_tag) + self.text.set_tags(tags) + def merge(self, acquisition): """ Merge the content of acquisition into this note. From 18c61c3e1beaa7153af20ecf3363dfc6c806d0ce Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 10 Aug 2020 11:30:56 -0500 Subject: [PATCH 5/6] Note LINK support for merge of other objects --- gramps/gen/lib/note.py | 42 ++++++++++++++++++++++++ gramps/gen/merge/mergecitationquery.py | 10 ++++-- gramps/gen/merge/mergeeventquery.py | 8 ++++- gramps/gen/merge/mergefamilyquery.py | 19 +++++++---- gramps/gen/merge/mergemediaquery.py | 8 ++++- gramps/gen/merge/mergenotequery.py | 8 ++++- gramps/gen/merge/mergepersonquery.py | 32 ++++++++++-------- gramps/gen/merge/mergeplacequery.py | 8 ++++- gramps/gen/merge/mergerepositoryquery.py | 8 ++++- gramps/gen/merge/mergesourcequery.py | 9 +++-- 10 files changed, 122 insertions(+), 30 deletions(-) diff --git a/gramps/gen/lib/note.py b/gramps/gen/lib/note.py index 71280e5f0..9f1ae33a6 100644 --- a/gramps/gen/lib/note.py +++ b/gramps/gen/lib/note.py @@ -170,6 +170,28 @@ class Note(BasicPrimaryObject): reflist.extend(self.get_referenced_tag_handles()) return reflist + def has_handle_reference(self, classname, handle): + """ + Return True if the object has reference to a given handle of given + primary object type. + + :param classname: The name of the primary object class. + :type classname: str + :param handle: The handle to be checked. + :type handle: str + + :returns: + Returns whether the object has reference to this handle of + this object type. + + :rtype: bool + """ + for dom, obj, prop, hndl in self.get_links(): + if dom == "gramps" and prop == "handle" and \ + obj == classname and hndl == handle: + return True + return False + def remove_handle_references(self, classname, handle_list): """ Remove all references in this object to object handles in the list. @@ -193,6 +215,26 @@ class Note(BasicPrimaryObject): tags.append(styledtext_tag) self.text.set_tags(tags) + def replace_handle_reference(self, classname, old_handle, new_handle): + """ + Replace all references to old handle with those to the new handle. + + :param classname: The name of the primary object class. + :type classname: str + :param old_handle: The handle to be replaced. + :type old_handle: str + :param new_handle: The handle to replace the old one with. + :type new_handle: str + """ + for styledtext_tag in self.text.get_tags(): + if(styledtext_tag.name == StyledTextTagType.LINK and + styledtext_tag.value.startswith("gramps://")): + obj, prop, value = styledtext_tag.value[9:].split("/", 2) + if(obj == classname and prop == 'handle' and + value == old_handle): + styledtext_tag.value = styledtext_tag.value.replace( + old_handle, new_handle) + def merge(self, acquisition): """ Merge the content of acquisition into this note. diff --git a/gramps/gen/merge/mergecitationquery.py b/gramps/gen/merge/mergecitationquery.py index bd7e610e1..3e04af79b 100644 --- a/gramps/gen/merge/mergecitationquery.py +++ b/gramps/gen/merge/mergecitationquery.py @@ -28,8 +28,8 @@ Provide merge capabilities for citations. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import (Person, Family, Event, Place, - Media, Repository, Citation, Source) +from ..lib import (Person, Family, Event, Place, Media, Repository, + Citation, Source, Note) from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -105,6 +105,12 @@ class MergeCitationQuery: source.replace_citation_references(old_handle, new_handle) self.database.commit_source(source, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Citation', old_handle)) + note.replace_handle_reference( + 'Citation', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type %s that has " "a citation reference." % class_name) diff --git a/gramps/gen/merge/mergeeventquery.py b/gramps/gen/merge/mergeeventquery.py index 28250424e..ba32f64f2 100644 --- a/gramps/gen/merge/mergeeventquery.py +++ b/gramps/gen/merge/mergeeventquery.py @@ -27,7 +27,7 @@ Provide merge capabilities for events. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import Person, Family +from ..lib import Person, Family, Note from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -88,6 +88,12 @@ class MergeEventQuery: family.replace_handle_reference("Event", old_handle, new_handle) self.database.commit_family(family, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Event', old_handle)) + note.replace_handle_reference( + 'Event', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type %s that has " "an event reference." % class_name) diff --git a/gramps/gen/merge/mergefamilyquery.py b/gramps/gen/merge/mergefamilyquery.py index 4dc8aff93..7659c92d9 100644 --- a/gramps/gen/merge/mergefamilyquery.py +++ b/gramps/gen/merge/mergefamilyquery.py @@ -194,12 +194,17 @@ class MergeFamilyQuery: if phoenix_mother: phoenix_mother.remove_family_handle(old_handle) self.database.commit_person(phoenix_mother, trans) - # replace the family in lds ordinances - for (dummy, person_handle) in self.database.find_backlink_handles( - old_handle, ['Person']): - if person_handle in (self.titanic_fh, self.titanic_mh): + # replace the family in lds ordinances and notes + for (ref_obj, ref_handle) in self.database.find_backlink_handles( + old_handle, ['Person', 'Note']): + if ref_handle in (self.titanic_fh, self.titanic_mh): continue - person = self.database.get_person_from_handle(person_handle) - person.replace_handle_reference('Family', old_handle,new_handle) - self.database.commit_person(person, trans) + obj = self.database.method( + "get_%s_from_handle", ref_obj)(ref_handle) + assert obj.has_handle_reference('Family', old_handle) + obj.replace_handle_reference( + 'Family', old_handle, new_handle) + if ref_handle != old_handle: + self.database.method("commit_%s", ref_obj)(obj, trans) + self.database.remove_family(old_handle, trans) diff --git a/gramps/gen/merge/mergemediaquery.py b/gramps/gen/merge/mergemediaquery.py index b03e983b6..e153c784e 100644 --- a/gramps/gen/merge/mergemediaquery.py +++ b/gramps/gen/merge/mergemediaquery.py @@ -27,7 +27,7 @@ Provide merge capabilities for media objects. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import Person, Family, Event, Source, Citation, Place +from ..lib import Person, Family, Event, Source, Citation, Place, Note from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -90,6 +90,12 @@ class MergeMediaQuery: assert(place.has_media_reference(old_handle)) place.replace_media_references(old_handle, new_handle) self.database.commit_place(place, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Media', old_handle)) + note.replace_handle_reference( + 'Media', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type % s that has " "a media object reference." % class_name) diff --git a/gramps/gen/merge/mergenotequery.py b/gramps/gen/merge/mergenotequery.py index 3261ccb09..86e097ce8 100644 --- a/gramps/gen/merge/mergenotequery.py +++ b/gramps/gen/merge/mergenotequery.py @@ -28,7 +28,7 @@ Provide merge capabilities for notes. # #------------------------------------------------------------------------- from ..lib import (Person, Family, Event, Place, Source, Citation, Repository, - Media) + Media, Note) from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -99,6 +99,12 @@ class MergeNoteQuery: assert(repo.has_note_reference(old_handle)) repo.replace_note_references(old_handle, new_handle) self.database.commit_repository(repo, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Note', old_handle)) + note.replace_handle_reference( + 'Note', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter object of type %s that has " "a note reference." % class_name) diff --git a/gramps/gen/merge/mergepersonquery.py b/gramps/gen/merge/mergepersonquery.py index 93f134945..dfb0b8028 100644 --- a/gramps/gen/merge/mergepersonquery.py +++ b/gramps/gen/merge/mergepersonquery.py @@ -100,14 +100,16 @@ class MergePersonQuery: spouse.remove_family_handle(family_handle) self.database.commit_person(spouse, trans) # replace the family in lds ordinances - for (dummy, person_handle) in self.database.find_backlink_handles( - family_handle, ['Person']): - if person_handle == old_handle: + for (ref_obj, ref_handle) in self.database.find_backlink_handles( + family_handle, ['Person', 'Note']): + if ref_handle == old_handle: continue - person = self.database.get_person_from_handle(person_handle) - person.replace_handle_reference('Family', family_handle, - main_family_handle) - self.database.commit_person(person, trans) + obj = self.database.method( + "get_%s_from_handle", ref_obj)(ref_handle) + assert obj.has_handle_reference('Family', family_handle) + obj.replace_handle_reference('Family', family_handle, + main_family_handle) + self.database.method("commit_%s", ref_obj)(obj, trans) self.database.remove_family(family_handle, trans) self.database.commit_family(main_family, trans) @@ -133,13 +135,15 @@ class MergePersonQuery: self.phoenix.merge(self.titanic) self.database.commit_person(self.phoenix, trans) - for (dummy, person_handle) in self.database.find_backlink_handles( - old_handle, ['Person']): - person = self.database.get_person_from_handle(person_handle) - assert person.has_handle_reference('Person', old_handle) - person.replace_handle_reference('Person', old_handle, new_handle) - if person_handle != old_handle: - self.database.commit_person(person, trans) + for (ref_obj, handle) in self.database.find_backlink_handles( + old_handle, ['Person', 'Note']): + obj = self.database.method( + "get_%s_from_handle", ref_obj)(handle) + assert obj.has_handle_reference('Person', old_handle) + obj.replace_handle_reference( + 'Person', old_handle, new_handle) + if handle != old_handle: + self.database.method("commit_%s", ref_obj)(obj, trans) for family_handle in self.phoenix.get_parent_family_handle_list(): family = self.database.get_family_from_handle(family_handle) diff --git a/gramps/gen/merge/mergeplacequery.py b/gramps/gen/merge/mergeplacequery.py index 95f89677c..c168b58b6 100644 --- a/gramps/gen/merge/mergeplacequery.py +++ b/gramps/gen/merge/mergeplacequery.py @@ -28,7 +28,7 @@ Provide merge capabilities for places. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import Person, Family, Event, Place +from ..lib import Person, Family, Event, Place, Note from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -85,6 +85,12 @@ class MergePlaceQuery: place.replace_handle_reference('Place', old_handle, new_handle) self.database.commit_place(place, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Place', old_handle)) + note.replace_handle_reference('Place', old_handle, + new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type %s that has " "a place reference." % class_name) diff --git a/gramps/gen/merge/mergerepositoryquery.py b/gramps/gen/merge/mergerepositoryquery.py index dc6c5100e..57a6f6b6d 100644 --- a/gramps/gen/merge/mergerepositoryquery.py +++ b/gramps/gen/merge/mergerepositoryquery.py @@ -27,7 +27,7 @@ Provide merge capabilities for repositories. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import Source +from ..lib import Source, Note from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -65,6 +65,12 @@ class MergeRepositoryQuery: assert source.has_handle_reference('Repository', old_handle) source.replace_repo_references(old_handle, new_handle) self.database.commit_source(source, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Repository', old_handle)) + note.replace_handle_reference( + 'Repository', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type %s that has " "a repository reference." % class_name) diff --git a/gramps/gen/merge/mergesourcequery.py b/gramps/gen/merge/mergesourcequery.py index 89d95b604..fb6c4085b 100644 --- a/gramps/gen/merge/mergesourcequery.py +++ b/gramps/gen/merge/mergesourcequery.py @@ -29,8 +29,7 @@ Provide merge capabilities for sources. # Gramps modules # #------------------------------------------------------------------------- -from ..lib import (Person, Family, Event, Place, Source, Repository, - Media, Citation) +from ..lib import (Citation, Note) from ..db import DbTxn from ..const import GRAMPS_LOCALE as glocale _ = glocale.translation.sgettext @@ -68,6 +67,12 @@ class MergeSourceQuery: assert(citation.get_reference_handle() == old_handle) citation.set_reference_handle(new_handle) self.database.commit_citation(citation, trans) + elif class_name == Note.__name__: + note = self.database.get_note_from_handle(handle) + assert(note.has_handle_reference('Source', old_handle)) + note.replace_handle_reference( + 'Source', old_handle, new_handle) + self.database.commit_note(note, trans) else: raise MergeError("Encounter an object of type %s that has " "a source reference." % class_name) From e468072694cd21f777f3b2d517360b955c00525a Mon Sep 17 00:00:00 2001 From: prculley Date: Mon, 10 Aug 2020 10:20:21 -0500 Subject: [PATCH 6/6] Tests update for Note rework --- .../filters/rules/test/media_rules_test.py | 3 +- gramps/gen/lib/test/merge_test.py | 52 ++++++ gramps/gen/merge/test/merge_ref_test.py | 173 +++++++++++++++++- 3 files changed, 225 insertions(+), 3 deletions(-) diff --git a/gramps/gen/filters/rules/test/media_rules_test.py b/gramps/gen/filters/rules/test/media_rules_test.py index 395496109..5d2ddd338 100644 --- a/gramps/gen/filters/rules/test/media_rules_test.py +++ b/gramps/gen/filters/rules/test/media_rules_test.py @@ -114,7 +114,8 @@ class BaseTest(unittest.TestCase): """ rule = HasReferenceCountOf(['greater than', '1']) self.assertEqual(self.filter_with_rule(rule), set([ - '238CGQ939HG18SS5MG', 'b39fe1cfc1305ac4a21'])) + '238CGQ939HG18SS5MG', 'b39fe1cfc1305ac4a21', + 'Y3ARGQWE088EQRTTDH'])) def test_hassourcecount(self): """ diff --git a/gramps/gen/lib/test/merge_test.py b/gramps/gen/lib/test/merge_test.py index 69997e36d..ff6faf89d 100644 --- a/gramps/gen/lib/test/merge_test.py +++ b/gramps/gen/lib/test/merge_test.py @@ -967,6 +967,49 @@ class NoteCheck(unittest.TestCase, PrivacyBaseTest): self.titanic = Note("hello world") self.ref_obj = Note("hello world") + def test_note_replace_handle_reference(self): + ptag = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Event/handle/e0000", + ranges=[0, 3]) + self.phoenix.text.set_tags([ptag]) + rtag = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Event/handle/e0001", + ranges=[0, 3]) + self.ref_obj.text.set_tags([rtag]) + self.phoenix.replace_handle_reference('Event', 'e0000', 'e0001') + self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) + + def test_note_has_handle_reference(self): + ptag = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Event/handle/e0000", + ranges=[0, 3]) + self.phoenix.text.set_tags([ptag]) + self.assertTrue(self.phoenix.has_handle_reference('Event', 'e0000')) + self.assertFalse(self.phoenix.has_handle_reference('Event', 'e0001')) + + def test_note_get_referenced_handles(self): + tag0 = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Event/handle/e0000", + ranges=[0, 2]) + tag1 = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Person/handle/i0001", + ranges=[2, 3]) + self.phoenix.text.set_tags([tag0, tag1]) + self.phoenix.add_tag("t1234") + tag_list = self.phoenix.get_referenced_handles() + self.assertEqual(tag_list, [('Event', 'e0000'), ('Person', 'i0001'), + ('Tag', 't1234')]) + self.assertFalse(self.phoenix.has_handle_reference('Event', 'e0001')) + + def test_note_remove_handle_references(self): + ptag = StyledTextTag(name=StyledTextTagType.LINK, + value="gramps://Event/handle/e0000", + ranges=[0, 3]) + self.phoenix.text.set_tags([ptag]) + self.phoenix.remove_handle_references('Event', ['e0000']) + self.assertEqual(self.phoenix.serialize(), self.ref_obj.serialize()) + + class NoteBaseCheck(unittest.TestCase): def setUp(self): self.phoenix = NoteBase() @@ -984,6 +1027,7 @@ class NoteBaseCheck(unittest.TestCase): def test_different(self): ref_note_list = NoteBase(self.phoenix) note = Note("note other") + note.set_handle('654321') self.titanic.add_note(note.get_handle()) ref_note_list.add_note(note.get_handle()) self.phoenix._merge_note_list(self.titanic) @@ -1018,6 +1062,14 @@ class NoteBaseCheck(unittest.TestCase): self.phoenix.replace_note_references('','') self.assertEqual(self.phoenix.serialize(), ref_note_list.serialize()) + def test_remove_note_references(self): + note = Note("note other") + note.set_handle('654321') + self.phoenix.add_note(note.get_handle()) + self.phoenix.remove_note_references(['123456', '654321']) + ref_note_list = NoteBase() + self.assertEqual(self.phoenix.serialize(), ref_note_list.serialize()) + class PersonCheck(unittest.TestCase, PrivacyBaseTest, MediaBaseTest, AttrBaseTest, NoteBaseTest, CitationBaseTest): def setUp(self): diff --git a/gramps/gen/merge/test/merge_ref_test.py b/gramps/gen/merge/test/merge_ref_test.py index 265c91a1a..590356c66 100644 --- a/gramps/gen/merge/test/merge_ref_test.py +++ b/gramps/gen/merge/test/merge_ref_test.py @@ -258,6 +258,7 @@ class PersonCheck(BaseMergeCheck): Source 1 + @@ -278,9 +279,40 @@ class PersonCheck(BaseMergeCheck): + + + New York Public Library + Library + + + Aunt Martha's Attic + Collection + + - Note 0 + Note 0. + + + + + + + Note 1 @@ -291,7 +323,8 @@ class PersonCheck(BaseMergeCheck): encoding='utf-8')) def test_event_merge(self): - """Merge two events""" + """Merge two events. Also checks that Event link in note is updated. + """ expect = ET.fromstring(self.basedoc, parser=self.parser) eventref = expect.xpath("//g:person[@handle='_i0001']/g:eventref", namespaces={"g": NS_G})[0] @@ -299,6 +332,8 @@ class PersonCheck(BaseMergeCheck): event = expect.xpath("//g:event[@handle='_e0001']", namespaces={"g": NS_G})[0] event.getparent().remove(event) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[1] + notetag.attrib['value'] = "gramps://Event/handle/e0000" self.do_case('E0000', 'E0001', self.basedoc, expect) #print(str(ET.tostring(expect, pretty_print=True), 'utf-8')) @@ -313,6 +348,8 @@ class PersonCheck(BaseMergeCheck): placeobj.getparent().remove(placeobj) placeobj = expect.xpath("//g:placeobj[@handle='_p0000']", namespaces={"g": NS_G})[0] + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[4] + notetag.attrib['value'] = "gramps://Place/handle/p0000" ET.SubElement(placeobj, NSP + 'pname', value='Place 1') self.do_case('P0000', 'P0001', self.basedoc, expect) @@ -325,6 +362,8 @@ class PersonCheck(BaseMergeCheck): citation = expect.xpath("//g:citation[@handle='_c0001']", namespaces={"g": NS_G})[0] citation.getparent().remove(citation) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Citation/handle/c0000" self.do_case('C0000', 'C0001', self.basedoc, expect) def test_media_merge(self): @@ -336,6 +375,8 @@ class PersonCheck(BaseMergeCheck): object_ = expect.xpath("//g:object[@handle='_o0001']", namespaces={"g": NS_G})[0] object_.getparent().remove(object_) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[2] + notetag.attrib['value'] = "gramps://Media/handle/o0000" self.do_case('O0000', 'O0001', self.basedoc, expect) def test_note_merge(self): @@ -346,9 +387,24 @@ class PersonCheck(BaseMergeCheck): noteref.attrib['hlink'] = '_n0000' note = expect.xpath("//g:note[@handle='_n0001']", namespaces={"g": NS_G})[0] + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[3] + notetag.attrib['value'] = "gramps://Note/handle/n0000" note.getparent().remove(note) self.do_case('N0000', 'N0001', self.basedoc, expect) + def test_repository_merge(self): + """Merge two repository objects""" + expect = ET.fromstring(self.basedoc, parser=self.parser) + reporef = expect.xpath("//g:source[@handle='_s0001']/g:reporef", + namespaces={"g": NS_G})[0] + reporef.attrib['hlink'] = '_r0000' + object_ = expect.xpath("//g:repository[@handle='_r0001']", + namespaces={"g": NS_G})[0] + object_.getparent().remove(object_) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[5] + notetag.attrib['value'] = "gramps://Repository/handle/r0000" + self.do_case('R0000', 'R0001', self.basedoc, expect) + #------------------------------------------------------------------------- # @@ -1226,6 +1282,14 @@ class PersonPersonCheck(BaseMergeCheck): + + + Note 0. + + + """ self.basedoc = bytes(bytearray(self.base_str + base_str, encoding='utf-8')) @@ -1256,6 +1320,8 @@ class PersonPersonCheck(BaseMergeCheck): person = expect.xpath("//g:person[@handle='_i0001']", namespaces={"g": NS_G})[0] person.getparent().remove(person) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" input_doc = ET.tostring(input_ctxt) self.do_case('I0000', 'I0001', input_doc, expect) @@ -1286,6 +1352,8 @@ class PersonPersonCheck(BaseMergeCheck): person = expect.xpath("//g:person[@handle='_i0001']", namespaces={"g": NS_G})[0] person.getparent().remove(person) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" input_doc = ET.tostring(input_ctxt) self.do_case('I0000', 'I0001', input_doc, expect) @@ -1309,6 +1377,8 @@ class PersonPersonCheck(BaseMergeCheck): person = expect.xpath("//g:person[@handle='_i0001']", namespaces={"g": NS_G})[0] person.getparent().remove(person) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" input_doc = ET.tostring(input_ctxt) self.do_case('I0000', 'I0001', input_doc, expect) @@ -1422,6 +1492,26 @@ class FamilyPersonCheck(BaseMergeCheck): + + + Note 0 + + + + Note 1 + + + + Note 0 + + + """ self.basedoc = bytes(bytearray(self.base_str + base_str, encoding='utf-8')) @@ -1439,6 +1529,9 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) self.do_case('I0000', 'I0002', self.basedoc, expect) @@ -1467,6 +1560,9 @@ class FamilyPersonCheck(BaseMergeCheck): father = expect.xpath("//g:family[@handle='_f0001']/g:father", namespaces={"g": NS_G})[0] father.attrib['hlink'] = '_i0000' + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) input_doc = ET.tostring(input_ctxt) self.do_case('I0000', 'I0002', input_doc, expect) @@ -1501,6 +1597,9 @@ class FamilyPersonCheck(BaseMergeCheck): ET.SubElement(family, NSP + 'rel', type='Married') ET.SubElement(family, NSP + 'father', hlink='_i0000') ET.SubElement(family, NSP + 'mother', hlink='_i0003') + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) persons = input_ctxt.xpath("//g:person", @@ -1547,7 +1646,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) @@ -1583,6 +1688,12 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" persons[2].getparent().remove(persons[2]) family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] @@ -1615,6 +1726,12 @@ class FamilyPersonCheck(BaseMergeCheck): namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements persons[3].getparent().remove(persons[3]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" + notetag = expect.xpath("//g:note[@handle='_n0003']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0001" family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) @@ -1655,7 +1772,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" families = expect.xpath("//g:family", namespaces={"g": NS_G}) families[1].getparent().remove(families[1]) @@ -1695,7 +1818,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" families = expect.xpath("//g:family", namespaces={"g": NS_G}) families[1].getparent().remove(families[1]) @@ -1733,7 +1862,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" families = expect.xpath("//g:family", namespaces={"g": NS_G}) families[1].getparent().remove(families[1]) @@ -1776,7 +1911,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" families = expect.xpath("//g:family", namespaces={"g": NS_G}) families[1].getparent().remove(families[1]) @@ -1817,7 +1958,13 @@ class FamilyPersonCheck(BaseMergeCheck): parentref = expect.xpath("//g:person[@handle='_i0000']/g:parentin", namespaces={"g": NS_G})[0] attr.addnext(parentref) # restore order of elements + notetag = expect.xpath("//g:note[@handle='_n0000']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Person/handle/i0000" persons[2].getparent().remove(persons[2]) + notetag = expect.xpath("//g:note[@handle='_n0001']/g:style", + namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" families = expect.xpath("//g:family", namespaces={"g": NS_G}) families[1].getparent().remove(families[1]) @@ -1879,6 +2026,14 @@ class FamilyMergeCheck(BaseMergeCheck): + + + Note 0. + + + """ self.basedoc = bytes(bytearray(self.base_str + base_str, encoding='utf-8')) @@ -1974,6 +2129,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" self.do_family_case('F0000', 'F0001', 'i0000', 'i0001', self.basedoc, expect) @@ -2004,6 +2161,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" father = expect.xpath("//g:family[@handle='_f0000']/g:father", namespaces={"g": NS_G})[0] father.attrib['hlink'] = '_i0002' @@ -2036,6 +2195,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" input_doc = ET.tostring(input_ctxt) self.do_family_case('F0000', 'F0001', 'i0000', 'i0001', input_doc, expect) @@ -2068,6 +2229,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" family = expect.xpath("//g:family[@handle='_f0000']", namespaces={"g": NS_G})[0] mother = ET.SubElement(family, NSP + 'mother', hlink='_i0003') @@ -2115,6 +2278,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" childof = expect.xpath("//g:person[@handle='_i0004']/g:childof", namespaces={"g": NS_G})[0] childof.attrib['hlink'] = '_f0000' @@ -2167,6 +2332,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" childof = expect.xpath("//g:person[@handle='_i0004']/g:childof", namespaces={"g": NS_G})[1] childof.getparent().remove(childof) @@ -2214,6 +2381,8 @@ class FamilyMergeCheck(BaseMergeCheck): family = expect.xpath("//g:family[@handle='_f0001']", namespaces={"g": NS_G})[0] family.getparent().remove(family) + notetag = expect.xpath("//g:style", namespaces={"g": NS_G})[0] + notetag.attrib['value'] = "gramps://Family/handle/f0000" sealedto = expect.xpath("//g:sealed_to", namespaces={"g": NS_G})[0] sealedto.attrib['hlink'] = '_f0000'