From ebb72b680397049e6aaf1c7ee6f49f493fce157a Mon Sep 17 00:00:00 2001 From: Christopher Horn Date: Sun, 2 Apr 2023 14:40:46 -0400 Subject: [PATCH] Tree metadata support --- data/gramps_canonicalize.xsl | 8 +- data/grampsxml.dtd | 39 +- data/grampsxml.rng | 41 +- example/gramps/example.gramps | 21 +- gramps/gen/db/base.py | 54 ++ gramps/gen/db/dummydb.py | 28 + gramps/gen/db/generic.py | 159 ++++- gramps/gen/lib/__init__.py | 1 + gramps/gen/lib/researcher.py | 72 ++- gramps/gen/lib/tree.py | 178 ++++++ gramps/gen/merge/test/merge_ref_test.py | 2 + gramps/gen/proxy/proxybase.py | 30 + gramps/gui/glade/catalog/grampswidgets.py | 4 + gramps/gui/glade/catalog/grampswidgets.xml | 5 + gramps/gui/widgets/__init__.py | 1 + gramps/gui/widgets/multientry.py | 134 ++++ gramps/plugins/export/exportgedcom.py | 6 +- gramps/plugins/export/exportxml.py | 62 +- gramps/plugins/gramplet/navhistorygramplet.py | 130 ++++ gramps/plugins/importer/importxml.py | 89 +-- .../plugins/importer/test/importvcard_test.py | 12 +- .../importer/test/importvcard_test.py.lxml | 574 ++++++++++++++++++ gramps/plugins/lib/libgrampsxml.py | 2 +- gramps/plugins/tool/edittree.glade | 272 +++++++++ gramps/plugins/tool/edittree.py | 200 ++++++ gramps/plugins/tool/ownereditor.glade | 430 +++++++++---- gramps/plugins/tool/ownereditor.py | 183 ++++-- gramps/plugins/tool/tools.gpr.py | 22 + 28 files changed, 2478 insertions(+), 281 deletions(-) create mode 100644 gramps/gen/lib/tree.py create mode 100644 gramps/gui/widgets/multientry.py create mode 100644 gramps/plugins/gramplet/navhistorygramplet.py create mode 100644 gramps/plugins/importer/test/importvcard_test.py.lxml create mode 100644 gramps/plugins/tool/edittree.glade create mode 100644 gramps/plugins/tool/edittree.py diff --git a/data/gramps_canonicalize.xsl b/data/gramps_canonicalize.xsl index abb410269..f100eb869 100644 --- a/data/gramps_canonicalize.xsl +++ b/data/gramps_canonicalize.xsl @@ -22,7 +22,7 @@ --> + xmlns:g="http://gramps-project.org/xml/1.7.2/"> - + + + + + + + + + + + + + + + + + + + + + diff --git a/data/grampsxml.rng b/data/grampsxml.rng index 87f44263d..b267274c9 100644 --- a/data/grampsxml.rng +++ b/data/grampsxml.rng @@ -41,14 +41,25 @@ - - - - - - - + + + + + + + + + + + + + + + + + + @@ -135,6 +146,22 @@ + + + + + + + + + + + + + + + + diff --git a/example/gramps/example.gramps b/example/gramps/example.gramps index 4a956588f..d78249357 100644 --- a/example/gramps/example.gramps +++ b/example/gramps/example.gramps @@ -1,10 +1,21 @@ - - + +
- - + + + 64c83c93 + 1690844342.4027495 + + + example_gramps + 2001-2006 Donald Allingham, 2007-2023 The Gramps Developers + Creative Commons Attribution-ShareAlike 2.5 + An example tree for use in producing documentation and with a lot of additional test data to help facilitate unit tests. + Donald Allingham, The Gramps Developers + + Alex Roitman,,, {GRAMPS_RESOURCES}/doc/gramps/example/gramps diff --git a/gramps/gen/db/base.py b/gramps/gen/db/base.py index ac561cd9c..2e6265316 100644 --- a/gramps/gen/db/base.py +++ b/gramps/gen/db/base.py @@ -1424,6 +1424,30 @@ class DbReadBase: """ raise NotImplementedError + def get_last_transaction_time(self): + """ + Return timestamp of last database transaction. + """ + raise NotImplementedError + + def get_tree(self): + """ + Return the tree. + """ + raise NotImplementedError + + def get_researcher_handle(self): + """ + Return the researcher handle. + """ + raise NotImplementedError + + def get_researcher_person(self): + """ + Return the researcher person object. + """ + raise NotImplementedError + def get_summary(self): """ Returns dictionary of summary item. @@ -2044,3 +2068,33 @@ class DbWriteBase(DbReadBase): person.birth_ref_index = birth_ref_index person.death_ref_index = death_ref_index + + def set_tree(self, tree): + """ + Set the tree information. + """ + raise NotImplementedError + + def set_tree_change(self, change): + """ + Set the tree last change value. + """ + raise NotImplementedError + + def set_researcher_changed(self, change): + """ + Set the researcher last change value. + """ + raise NotImplementedError + + def set_researcher_handle(self, handle): + """ + Set the researcher handle. + """ + raise NotImplementedError + + def refresh_cached_tree_name(self): + """ + Check and update tree name if changed. + """ + raise NotImplementedError diff --git a/gramps/gen/db/dummydb.py b/gramps/gen/db/dummydb.py index 649b6c0ff..9b3c0a2f9 100644 --- a/gramps/gen/db/dummydb.py +++ b/gramps/gen/db/dummydb.py @@ -1614,3 +1614,31 @@ class DummyDb( A name for this database on this computer. """ return "" + + def get_last_transaction_time(self): + """ + Timestamp of last database transaction. + """ + if not self.db_is_open: + LOG.debug("database is closed") + + def get_tree(self): + """ + Return the tree information. + """ + if not self.db_is_open: + LOG.debug("database is closed") + + def get_researcher_handle(self): + """ + Return the researcher handle. + """ + if not self.db_is_open: + LOG.debug("database is closed") + + def get_researcher_person(self): + """ + Return the researcher person object. + """ + if not self.db_is_open: + LOG.debug("database is closed") diff --git a/gramps/gen/db/generic.py b/gramps/gen/db/generic.py index fbed28b8b..4606faa50 100644 --- a/gramps/gen/db/generic.py +++ b/gramps/gen/db/generic.py @@ -74,21 +74,22 @@ from ..updatecallback import UpdateCallback from .bookmarks import DbBookmarks from ..utils.id import create_id -from ..lib.researcher import Researcher from ..lib import ( - Tag, - Media, - Person, - Family, - Source, Citation, Event, + Family, + GenderStats, + Media, + NameOriginType, + Note, + Person, Place, Repository, - Note, - NameOriginType, + Researcher, + Source, + Tag, + Tree ) -from ..lib.genderstats import GenderStats from ..config import config from ..const import GRAMPS_LOCALE as glocale @@ -352,7 +353,8 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): "tag", ] for op, signal in zip( - ["add", "update", "delete", "rebuild"], [(list,), (list,), (list,), None] + ["add", "update", "delete", "rebuild"], + [(list,), (list,), (list,), None], ) ) @@ -368,6 +370,12 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): # 4. Signal for change in person group name, parameters are __signals__["person-groupname-rebuild"] = (str, str) + # 5. Signal for change in tree metadata + __signals__["tree-data-changed"] = None + + # 6. Signal for change in either researcher metadata or handle + __signals__["researcher-changed"] = None + __callback_map = {} VERSION = (20, 0, 0) @@ -589,6 +597,10 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): self.surname_list = [] self.genderStats = GenderStats() # can pass in loaded stats as dict self.owner = Researcher() + self.owner_change = 0 + self.tree = Tree() + self.tree_change = 0 + self.db_last_transaction = 0.0 if directory: self.load(directory) @@ -653,6 +665,12 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): # Load metadata self.name_formats = self._get_metadata("name_formats") self.owner = self._get_metadata("researcher", default=Researcher()) + self.owner_change = self._get_metadata("researcher_change", default=0) + self.tree = self._get_metadata("tree", default=Tree()) + self.tree_change = self._get_metadata("tree_change", default=0) + self.db_last_transaction = self._get_metadata( + "db_last_transaction", default=0.0 + ) # Load bookmarks self.bookmarks.load(self._get_metadata("bookmarks")) @@ -712,6 +730,7 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): self.nmap_index = self._get_metadata("nmap_index", 0) self.db_is_open = True + self.refresh_cached_tree_name() # Check on db version to see if we need upgrade or too new dbversion = int(self._get_metadata("version", default="0")) @@ -721,7 +740,9 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): if not self.readonly and dbversion < self.VERSION[0]: LOG.debug( - "Schema upgrade required from %s to %s", dbversion, self.VERSION[0] + "Schema upgrade required from %s to %s", + dbversion, + self.VERSION[0], ) if force_schema_upgrade: self._gramps_upgrade(dbversion, directory, callback) @@ -735,6 +756,12 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): """ return DbGenericUndo(self, self.undolog) + def refresh_cached_tree_name(self): + name = self.get_dbname() + if self.tree.get_name() != name: + self.tree.set_name(name) + self._save_tree() + def _close(self): """ Close database backend. @@ -754,8 +781,8 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): Path(filename).touch() # Save metadata + self.refresh_cached_tree_name() self._set_metadata("name_formats", self.name_formats) - self._set_metadata("researcher", self.owner) # Bookmarks self._set_metadata("bookmarks", self.bookmarks.get()) @@ -827,7 +854,7 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): In DbGeneric, the database is in a text file at the path """ name = None - if self._directory: + if self._directory and self._directory != ":memory:": filepath = os.path.join(self._directory, "name.txt") try: with open(filepath, "r", encoding="utf8") as name_file: @@ -1056,7 +1083,16 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): self.nid2user_format = self.__id2user_format(self.note_prefix) def set_prefixes( - self, person, media, family, source, citation, place, event, repository, note + self, + person, + media, + family, + source, + citation, + place, + event, + repository, + note, ): self.set_person_id_prefix(person) self.set_media_id_prefix(media) @@ -1815,22 +1851,38 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): def add_person(self, person, trans, set_gid=True): return self._add_base( - person, trans, set_gid, self.find_next_person_gramps_id, self.commit_person + person, + trans, + set_gid, + self.find_next_person_gramps_id, + self.commit_person, ) def add_family(self, family, trans, set_gid=True): return self._add_base( - family, trans, set_gid, self.find_next_family_gramps_id, self.commit_family + family, + trans, + set_gid, + self.find_next_family_gramps_id, + self.commit_family, ) def add_event(self, event, trans, set_gid=True): return self._add_base( - event, trans, set_gid, self.find_next_event_gramps_id, self.commit_event + event, + trans, + set_gid, + self.find_next_event_gramps_id, + self.commit_event, ) def add_place(self, place, trans, set_gid=True): return self._add_base( - place, trans, set_gid, self.find_next_place_gramps_id, self.commit_place + place, + trans, + set_gid, + self.find_next_place_gramps_id, + self.commit_place, ) def add_repository(self, repository, trans, set_gid=True): @@ -1844,7 +1896,11 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): def add_source(self, source, trans, set_gid=True): return self._add_base( - source, trans, set_gid, self.find_next_source_gramps_id, self.commit_source + source, + trans, + set_gid, + self.find_next_source_gramps_id, + self.commit_source, ) def add_citation(self, citation, trans, set_gid=True): @@ -1858,12 +1914,20 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): def add_media(self, media, trans, set_gid=True): return self._add_base( - media, trans, set_gid, self.find_next_media_gramps_id, self.commit_media + media, + trans, + set_gid, + self.find_next_media_gramps_id, + self.commit_media, ) def add_note(self, note, trans, set_gid=True): return self._add_base( - note, trans, set_gid, self.find_next_note_gramps_id, self.commit_note + note, + trans, + set_gid, + self.find_next_note_gramps_id, + self.commit_note, ) def add_tag(self, tag, trans): @@ -2150,6 +2214,9 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): """ Post-transaction commit processing """ + self.db_last_transaction = time.time() + self._set_metadata("db_last_transaction", self.db_last_transaction) + # Reset callbacks if necessary if transaction.batch or not len(transaction): return @@ -2483,11 +2550,54 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): def save_gender_stats(self, gstats): raise NotImplementedError + def get_last_transaction_time(self): + return self.db_last_transaction + + def get_tree(self): + return self.tree + + def set_tree(self, tree, quiet=False): + self.tree = tree + self._save_tree(quiet) + + def _save_tree(self, quiet=False): + self._set_metadata("tree", self.tree) + self.set_tree_change() + if not quiet: + self.emit("tree-data-changed") + + def set_tree_change(self, change=int(time.time())): + self.tree_change = change + self._set_metadata("tree_change", change) + def get_researcher(self): return self.owner - def set_researcher(self, owner): + def set_researcher(self, owner, quiet=False): self.owner.set_from(owner) + self._set_metadata("researcher", self.owner) + self.set_researcher_change() + if not quiet: + self.emit("researcher-changed") + + def set_researcher_change(self, change=int(time.time())): + self.researcher_change = change + self._set_metadata("researcher_change", change) + + def get_researcher_handle(self): + return self._get_metadata("researcher_handle", default=None) + + def get_researcher_person(self): + handle = self.get_researcher_handle() + try: + return self.get_person_from_handle(handle) + except HandleError: + return None + + def set_researcher_handle(self, handle, quiet=False): + self._set_metadata("researcher_handle", handle) + if not quiet: + self.emit("researcher-changed") def request_rebuild(self): self.emit("person-rebuild") @@ -2572,7 +2682,10 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback): for surname in person.primary_name.surname_list if ( int(surname.origintype) - not in [NameOriginType.PATRONYMIC, NameOriginType.MATRONYMIC] + not in [ + NameOriginType.PATRONYMIC, + NameOriginType.MATRONYMIC, + ] ) ] order_by = " ".join(order_by_list) diff --git a/gramps/gen/lib/__init__.py b/gramps/gen/lib/__init__.py index d7a55da06..1b7876f15 100644 --- a/gramps/gen/lib/__init__.py +++ b/gramps/gen/lib/__init__.py @@ -60,6 +60,7 @@ from .tag import Tag # These are actually metadata from .genderstats import GenderStats from .researcher import Researcher +from .tree import Tree # Type classes from .grampstype import GrampsType diff --git a/gramps/gen/lib/researcher.py b/gramps/gen/lib/researcher.py index 7b5915041..a65d814da 100644 --- a/gramps/gen/lib/researcher.py +++ b/gramps/gen/lib/researcher.py @@ -29,21 +29,25 @@ Researcher information for Gramps. # # ------------------------------------------------------------------------- from .locationbase import LocationBase +from ..const import GRAMPS_LOCALE as glocale + +_ = glocale.translation.gettext # ------------------------------------------------------------------------- # -# +# Researcher class # # ------------------------------------------------------------------------- class Researcher(LocationBase): - """Contains the information about the owner of the database.""" + """ + Contains the information about the owner of the database. + """ def __init__(self, source=None): """ Initialize the Researcher object, copying from the source if provided. """ - LocationBase.__init__(self, source) if source: self.name = source.name @@ -60,41 +64,81 @@ class Researcher(LocationBase): """ return (LocationBase.serialize(self), self.name, self.addr, self.email) + @classmethod + def get_schema(cls): + """ + Returns the JSON Schema for this class. + + :returns: Returns a dict containing the schema. + :rtype: dict + """ + return { + "type": "object", + "title": _("Researcher"), + "properties": { + "_class": {"enum": [cls.__name__]}, + "name": {"type": "string", "title": _("Name")}, + "address": {"type": "string", "title": _("Address")}, + "email": {"type": "string", "title": _("Email")}, + "street": {"type": "string", "title": _("Street")}, + "locality": {"type": "string", "title": _("Locality")}, + "city": {"type": "string", "title": _("City")}, + "county": {"type": "string", "title": _("County")}, + "state": {"type": "string", "title": _("State")}, + "country": {"type": "string", "title": _("Country")}, + "postal": {"type": "string", "title": _("Postal Code")}, + "phone": {"type": "string", "title": _("Phone")}, + }, + } + def unserialize(self, data): """ Convert a serialized tuple of data to an object. """ (location, self.name, self.addr, self.email) = data LocationBase.unserialize(self, location) - return self def set_name(self, data): - """Set the database owner's name.""" + """ + Set the database owner's name. + """ self.name = data def get_name(self): - """Return the database owner's name.""" + """ + Return the database owner's name. + """ return self.name def set_address(self, data): - """Set the database owner's address.""" + """ + Set the database owner's address. + """ self.addr = data def get_address(self): - """Return the database owner's address.""" + """ + Return the database owner's address. + """ return self.addr def set_email(self, data): - """Set the database owner's email.""" + """ + Set the database owner's email. + """ self.email = data def get_email(self): - """Return the database owner's email.""" + """ + Return the database owner's email. + """ return self.email def set_from(self, other_researcher): - """Set all attributes from another instance.""" + """ + Set all attributes from another instance. + """ self.street = other_researcher.street self.locality = other_researcher.locality self.city = other_researcher.city @@ -109,6 +153,9 @@ class Researcher(LocationBase): self.email = other_researcher.email def get(self): + """ + Return all the attributes as a list. + """ return [ getattr(self, value) for value in [ @@ -125,6 +172,9 @@ class Researcher(LocationBase): ] def is_empty(self): + """ + Check if nothing has been set in the object. + """ for attr in [ "name", "addr", diff --git a/gramps/gen/lib/tree.py b/gramps/gen/lib/tree.py new file mode 100644 index 000000000..465009c45 --- /dev/null +++ b/gramps/gen/lib/tree.py @@ -0,0 +1,178 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2007 Donald N. Allingham +# Copyright (C) 2013 Doug Blank +# Copyright (C) 2022 Christopher Horn +# +# 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. +# + +""" +Tree information for Gramps. +""" + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from ..const import GRAMPS_LOCALE as glocale + +_ = glocale.translation.gettext + + +# ------------------------------------------------------------------------- +# +# Tree class +# +# ------------------------------------------------------------------------- +class Tree: + """ + Contains the tree related metadata. + """ + + def __init__(self, source=None): + """ + Initialize the Tree object, copying from the source if provided. + """ + self.name = "" + self.description = "" + self.copyright_used = "" + self.license_used = "" + self.contributors = "" + if source: + self.unserialize(source) + + def serialize(self): + """ + Convert the object to a serialized tuple of data. + """ + return ( + self.name, + self.description, + self.copyright_used, + self.license_used, + self.contributors, + ) + + @classmethod + def get_schema(cls): + """ + Returns the JSON Schema for this class. + + :returns: Returns a dict containing the schema. + :rtype: dict + """ + return { + "type": "object", + "title": _("Tree"), + "properties": { + "_class": {"enum": [cls.__name__]}, + "name": {"type": "string", "title": _("Tree name")}, + "description": {"type": "string", "title": _("Description")}, + "copyright": {"type": "string", "title": _("Copyright")}, + "license": {"type": "string", "title": _("License")}, + "contributors": {"type": "string", "title": _("Contributors")}, + }, + } + + def unserialize(self, data): + """ + Convert a serialized tuple of data to an object. + """ + ( + self.name, + self.description, + self.copyright_used, + self.license_used, + self.contributors, + ) = data + return self + + def get_text_data_list(self): + """ + Return the list of all textual attributes of the object. + + :returns: Returns the list of all textual attributes of the object. + :rtype: list + """ + return [ + self.name, + self.description, + self.copyright_used, + self.license_used, + self.contributors, + ] + + def get_name(self): + """ + Return the tree name. + """ + return self.name + + def set_name(self, data): + """ + Set the tree name. + """ + self.name = data + + def get_description(self): + """ + Return the description. + """ + return self.description + + def set_description(self, data): + """ + Set the description. + """ + self.description = data + + def get_copyright(self): + """ + Return the copyright. + """ + return self.copyright_used + + def set_copyright(self, data): + """ + Set the copyright. + """ + self.copyright_used = data + + def get_license(self): + """ + Return the license. + """ + return self.license_used + + def set_license(self, data): + """ + Set the license. + """ + self.license_used = data + + def get_contributors(self): + """ + Return the contributors. + """ + return self.contributors + + def set_contributors(self, data): + """ + Set the contributors. + """ + self.contributors = data diff --git a/gramps/gen/merge/test/merge_ref_test.py b/gramps/gen/merge/test/merge_ref_test.py index 5c3677230..c0e6c68f0 100644 --- a/gramps/gen/merge/test/merge_ref_test.py +++ b/gramps/gen/merge/test/merge_ref_test.py @@ -70,6 +70,8 @@ class BaseMergeCheck(unittest.TestCase):
+ \n + \n \n
""" % ( diff --git a/gramps/gen/proxy/proxybase.py b/gramps/gen/proxy/proxybase.py index 1178a36f7..26e3377fd 100644 --- a/gramps/gen/proxy/proxybase.py +++ b/gramps/gen/proxy/proxybase.py @@ -145,6 +145,36 @@ class ProxyDbBase(DbReadBase): """ return self.db.is_open() + def get_last_transaction_time(self): + """ + Return the last transaction timestamp. + """ + return self.db.get_last_transaction_time() + + def get_tree(self): + """ + Return the tree information. + """ + return self.db.get_tree() + + def get_researcher_handle(self): + """ + Return the researcher handle. + """ + return self.db.get_researcher_handle() + + def get_researcher_person(self): + """ + Return the researcher person. + """ + return self.db.get_researcher_person() + + def get_tree_metadata(self): + """ + Return the tree metadata. + """ + return self.db.get_tree_metadata() + def get_researcher(self): """returns the Researcher instance, providing information about the owner of the database""" diff --git a/gramps/gui/glade/catalog/grampswidgets.py b/gramps/gui/glade/catalog/grampswidgets.py index 0c672390e..39ddae242 100644 --- a/gramps/gui/glade/catalog/grampswidgets.py +++ b/gramps/gui/glade/catalog/grampswidgets.py @@ -41,3 +41,7 @@ class UndoableBuffer(Gtk.TextBuffer): class PersistentTreeView(Gtk.TreeView): __gtype_name__ = "PersistentTreeView" + + +class MultiLineEntry(Gtk.Box): + __gtype_name__ = "MultiLineEntry" diff --git a/gramps/gui/glade/catalog/grampswidgets.xml b/gramps/gui/glade/catalog/grampswidgets.xml index d5c97ad9a..4a31d4375 100644 --- a/gramps/gui/glade/catalog/grampswidgets.xml +++ b/gramps/gui/glade/catalog/grampswidgets.xml @@ -19,11 +19,16 @@ name="PersistentTreeView" title="Persistent TreeView" generic-name="resizable_treeview"/> + + diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index 7e87a3f7b..0f6e2d3f9 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -22,6 +22,7 @@ """Custom widgets.""" from .basicentry import * +from .multientry import * from .buttons import * from .dateentry import * from .expandcollapsearrow import * diff --git a/gramps/gui/widgets/multientry.py b/gramps/gui/widgets/multientry.py new file mode 100644 index 000000000..4ebe14449 --- /dev/null +++ b/gramps/gui/widgets/multientry.py @@ -0,0 +1,134 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2022 Christopher Horn +# +# 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. +# + +""" +Provide a basic multi-line text entry widget. +""" + +__all__ = ["MultiLineEntry"] + + +# ------------------------------------------------------------------------- +# +# GTK/Gnome modules +# +# ------------------------------------------------------------------------- +from gi.repository import GObject, Gtk + + +# ------------------------------------------------------------------------- +# +# MultiLineEntry class +# +# ------------------------------------------------------------------------- +class MultiLineEntry(Gtk.Box): + """ + Provide a simple multi-line text entry widget with a frame that + should look like a normal single line text entry widget. + """ + + __gtype_name__ = "MultiLineEntry" + + __gsignals__ = { + "changed": (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) + } + + def __init__(self, hexpand=True, vexpand=True, text=""): + super().__init__(self) + self.frame = Gtk.Frame(hexpand=hexpand, vexpand=vexpand) + css = ".frame { border-style: solid; border-radius: 5px; }" + self.provider = Gtk.CssProvider() + self.provider.load_from_data(css.encode("utf-8")) + context = self.frame.get_style_context() + context.add_provider(self.provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) + context.add_class("frame") + + self.buffer = Gtk.TextBuffer() + self.buffer.set_text(text) + self.buffer.connect("changed", self.changed) + self.view = Gtk.TextView( + buffer=self.buffer, hexpand=True, vexpand=True + ) + self.view.set_accepts_tab(False) + self.view.set_left_margin(6) + self.view.set_right_margin(6) + self.view.set_top_margin(6) + self.view.set_bottom_margin(6) + self.view.set_wrap_mode(Gtk.WrapMode.WORD) + self.connect("focus-in-event", self.focus_outer_set) + self.view.connect("focus-in-event", self.focus_inner_set) + self.view.connect("focus-out-event", self.focus_unset) + self.frame.add(self.view) + self.add(self.frame) + self.show() + + def changed(self, _cb_obj): + """ + Emit change signal in case being monitored. + """ + self.emit("changed") + + def set_editable(self, *args): + """ + Set text view edit state. + """ + self.view.set_editable(*args) + + def set_text(self, text): + """ + Set the text. + """ + self.buffer.set_text(text) + + def get_text(self): + """ + Get the text. + """ + start, end = self.buffer.get_bounds() + return self.buffer.get_text(start, end, False) + + def focus_outer_set(self, *_dummy_args): + """ + Pivot focus to the view. + """ + self.view.grab_focus() + + def focus_inner_set(self, *_dummy_args): + """ + Set border when in focus. + """ + css = ( + ".frame { border-style: solid; border-width: 2px; " + "border-radius: 5px; border-color: rgb(53,132,228); }" + ) + self.provider.load_from_data(css.encode("utf-8")) + self.frame.queue_draw() + self.buffer.place_cursor(self.buffer.get_start_iter()) + self.set_can_focus(False) + + def focus_unset(self, *_dummy_args): + """ + Reset border when releasing focus. + """ + css = ".frame { border-style: solid; border-radius: 5px; }" + self.provider.load_from_data(css.encode("utf-8")) + self.frame.queue_draw() + self.set_can_focus(True) + return False diff --git a/gramps/plugins/export/exportgedcom.py b/gramps/plugins/export/exportgedcom.py index 64902c538..dfd7e94ff 100644 --- a/gramps/plugins/export/exportgedcom.py +++ b/gramps/plugins/export/exportgedcom.py @@ -380,7 +380,11 @@ class GedcomWriter(UpdateCallback): self._writeln(2, "TIME", time_str) self._writeln(1, "SUBM", "@SUBM@") self._writeln(1, "FILE", filename, limit=255) - self._writeln(1, "COPR", "Copyright (c) %d %s." % (year, rname)) + tree = self.dbase.get_tree() + if tree.get_copyright(): + self._writeln(1, "COPR", tree.get_copyright()) + else: + self._writeln(1, "COPR", "Copyright (c) %d %s." % (year, rname)) self._writeln(1, "GEDC") self._writeln(2, "VERS", "5.5.1") self._writeln(2, "FORM", "LINEAGE-LINKED") diff --git a/gramps/plugins/export/exportxml.py b/gramps/plugins/export/exportxml.py index 9444bcaa3..50ab4f252 100644 --- a/gramps/plugins/export/exportxml.py +++ b/gramps/plugins/export/exportxml.py @@ -8,7 +8,7 @@ # Copyright (C) 2009 Douglas S. Blank # Copyright (C) 2010 Jakim Friant # Copyright (C) 2010-2011 Nick Hall -# Copyright (C) 2013 Benny Malengier +# Copyright (C) 2013 Benny Malengier # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -56,18 +56,18 @@ LOG = logging.getLogger(".WriteXML") # # ------------------------------------------------------------------------- from gramps.gen.const import GRAMPS_LOCALE as glocale - -_ = glocale.translation.gettext from gramps.gen.const import URL_HOMEPAGE +from gramps.gen.constfunc import win +from gramps.gen.db.exceptions import DbWriteFailure from gramps.gen.lib import Date, Person from gramps.gen.updatecallback import UpdateCallback -from gramps.gen.db.exceptions import DbWriteFailure -from gramps.version import VERSION -from gramps.gen.constfunc import win from gramps.gui.plug.export import WriterOptionBox, WriterOptionBoxWithCompression +from gramps.version import VERSION import gramps.plugins.lib.libgrampsxml as libgrampsxml -# ------------------------------------------------------------------------- +_ = glocale.translation.gettext + +#------------------------------------------------------------------------- # # Attempt to load the GZIP library. Some version of python do not seem # to be compiled with this available. @@ -109,7 +109,8 @@ class GrampsXmlWriter(UpdateCallback): Writes a database to the XML file. """ - def __init__(self, db, strip_photos=0, compress=1, version="unknown", user=None): + def __init__(self, db, strip_photos=0, compress=1, version="unknown", + user=None, export_note=""): """ Initialize, but does not write, an XML file. @@ -128,6 +129,7 @@ class GrampsXmlWriter(UpdateCallback): self.db = db self.strip_photos = strip_photos self.version = version + self.export_note = export_note self.status = None @@ -225,6 +227,9 @@ class GrampsXmlWriter(UpdateCallback): def write_xml_data(self): date = time.localtime(time.time()) owner = self.db.get_researcher() + owner_handle = self.db.get_researcher_handle() + self.db.refresh_cached_tree_name() + tree = self.db.get_tree() person_len = self.db.get_number_of_people() family_len = self.db.get_number_of_families() @@ -271,16 +276,37 @@ class GrampsXmlWriter(UpdateCallback): self.g.write(' \n") - self.g.write(" \n") - self.write_line("resname", owner.get_name(), 3) - self.write_line("resaddr", owner.get_address(), 3) - self.write_line("reslocality", owner.get_locality(), 3) - self.write_line("rescity", owner.get_city(), 3) - self.write_line("resstate", owner.get_state(), 3) - self.write_line("rescountry", owner.get_country(), 3) - self.write_line("respostal", owner.get_postal_code(), 3) - self.write_line("resphone", owner.get_phone(), 3) - self.write_line("resemail", owner.get_email(), 3) + self.g.write(" \n") + self.write_line("database-id", self.db.get_dbid(), 3) + self.write_line( + "last-transaction-timestamp", + self.db.get_last_transaction_time(), + 3 + ) + self.write_line("export-note", self.export_note, 3) + self.g.write(" \n") + + self.g.write(' \n' % self.db.tree_change) + self.write_line("name", tree.get_name(), 3) + self.write_line("copyright", tree.get_copyright(), 3) + self.write_line("license", tree.get_license(), 3) + self.write_line("description", tree.get_description(), 3) + self.write_line("contributors", tree.get_contributors(), 3) + self.g.write(" \n") + + self.g.write(' \n") + self.write_line("resname", owner.get_name(),3) + self.write_line("resaddr", owner.get_address(),3) + self.write_line("reslocality", owner.get_locality(),3) + self.write_line("rescity", owner.get_city(),3) + self.write_line("resstate", owner.get_state(),3) + self.write_line("rescountry", owner.get_country(),3) + self.write_line("respostal", owner.get_postal_code(),3) + self.write_line("resphone", owner.get_phone(),3) + self.write_line("resemail", owner.get_email(),3) self.g.write(" \n") self.write_metadata() self.g.write("
\n") diff --git a/gramps/plugins/gramplet/navhistorygramplet.py b/gramps/plugins/gramplet/navhistorygramplet.py new file mode 100644 index 000000000..7edad3f6a --- /dev/null +++ b/gramps/plugins/gramplet/navhistorygramplet.py @@ -0,0 +1,130 @@ +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2007-2009 Douglas S. Blank +# +# 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. + +#------------------------------------------------------------------------ +# +# Python modules +# +#------------------------------------------------------------------------ +import time + +#------------------------------------------------------------------------ +# +# Gramps modules +# +#------------------------------------------------------------------------ + +from gramps.gen.lib import Person, Family +from gramps.gen.db import PERSON_KEY, FAMILY_KEY, TXNDEL +from gramps.gen.plug import Gramplet +from gramps.gen.display.name import displayer as name_displayer +from gramps.gen.utils.db import family_name +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.sgettext + +#------------------------------------------------------------------------ +# +# NavHistoryGramplet class +# +#------------------------------------------------------------------------ +class NavHistoryGramplet(Gramplet): + def init(self): + self.set_tooltip(_("Click name to change active\nDouble-click name to edit")) + self.set_text(_("Log for this Session") + "\n") + self.gui.force_update = True # will always update, even if minimized + self.last_log = None + + def timestamp(self): + self.append_text(time.strftime("%Y-%m-%d %H:%M:%S ")) + + def db_changed(self): + self.timestamp() + self.append_text(_("Opened data base -----------\n")) + # List of translated strings used here (translated in self.log ). + _('Added'), _('Deleted'), _('Edited'), _('Selected') # Dead code for l10n + self.connect(self.dbstate.db, 'person-add', + lambda handles: self.log('Person', 'Added', handles)) + self.connect(self.dbstate.db, 'person-delete', + lambda handles: self.log('Person', 'Deleted', handles)) + self.connect(self.dbstate.db, 'person-update', + lambda handles: self.log('Person', 'Edited', handles)) + self.connect(self.dbstate.db, 'family-add', + lambda handles: self.log('Family', 'Added', handles)) + self.connect(self.dbstate.db, 'family-delete', + lambda handles: self.log('Family', 'Deleted', handles)) + self.connect(self.dbstate.db, 'family-update', + lambda handles: self.log('Family', 'Edited', handles)) + self.connect_signal('Person', self.active_changed) + self.connect_signal('Family', self.active_changed_family) + + def active_changed(self, handle): + if handle: + self.log('Person', 'Selected', [handle]) + + def active_changed_family(self, handle): + if handle: + self.log('Family', 'Selected', [handle]) + + def log(self, ltype, action, handles): + for handle in set(handles): + if self.last_log == (ltype, action, handle): + continue + self.last_log = (ltype, action, handle) + self.timestamp() + # Translators: needed for French, ignore otherwise + self.append_text(_("%s: ") % _(action)) + if action == 'Deleted': + transaction = self.dbstate.db.transaction + if ltype == 'Person': + name = 'a person' + if transaction is not None: + for i in transaction.get_recnos(reverse=True): + (obj_type, trans_type, hndl, old_data, dummy) = \ + transaction.get_record(i) + if isinstance(hndl, bytes): + hndl = str(hndl, "utf-8") + if (obj_type == PERSON_KEY and trans_type == TXNDEL + and hndl == handle): + person = Person() + person.unserialize(old_data) + name = name_displayer.display(person) + break + elif ltype == 'Family': + name = 'a family' + if transaction is not None: + for i in transaction.get_recnos(reverse=True): + (obj_type, trans_type, hndl, old_data, dummy) = \ + transaction.get_record(i) + if isinstance(hndl, bytes): + hndl = str(hndl, "utf-8") + if (obj_type == FAMILY_KEY and trans_type == TXNDEL + and hndl == handle): + family = Family() + family.unserialize(old_data) + name = family_name(family, self.dbstate.db, name) + break + self.append_text(name) + else: + if ltype == 'Person': + person = self.dbstate.db.get_person_from_handle(handle) + name = name_displayer.display(person) + elif ltype == 'Family': + family = self.dbstate.db.get_family_from_handle(handle) + name = family_name(family, self.dbstate.db, 'a family') + self.link(name, ltype, handle) + self.append_text("\n") diff --git a/gramps/plugins/importer/importxml.py b/gramps/plugins/importer/importxml.py index 6f26f1dab..2e48c4d81 100644 --- a/gramps/plugins/importer/importxml.py +++ b/gramps/plugins/importer/importxml.py @@ -89,11 +89,10 @@ from gramps.gen.lib import ( StyledTextTagType, Surname, Tag, + Tree, Url, ) from gramps.gen.db import DbTxn - -# from gramps.gen.db.write import CLASS_TO_KEY_MAP from gramps.gen.errors import GrampsImportError from gramps.gen.utils.id import create_id from gramps.gen.utils.db import family_name @@ -117,8 +116,6 @@ from gramps.gen.db.dbconst import ( from gramps.gen.updatecallback import UpdateCallback from gramps.version import VERSION from gramps.gen.config import config - -# import gramps.plugins.lib.libgrampsxml from gramps.plugins.lib import libgrampsxml from gramps.gen.plug.utils import version_str_to_tup from gramps.plugins.lib.libplaceimport import PlaceImport @@ -601,16 +598,12 @@ class GrampsParser(UpdateCallback): self.place_map = {} self.place_import = PlaceImport(self.db) - self.resname = "" - self.resaddr = "" - self.reslocality = "" - self.rescity = "" - self.resstate = "" - self.rescon = "" - self.respos = "" - self.resphone = "" - self.resemail = "" - + self.tree = Tree() + self.in_tree = False + self.tree_change = 0 + self.owner = Researcher() + self.owner_change = 0 + self.owner_handle = "" self.mediapath = "" self.pmap = {} @@ -632,7 +625,6 @@ class GrampsParser(UpdateCallback): self.surname = None self.surnamepat = None self.home = None - self.owner = Researcher() self.func_list = [None] * 50 self.func_index = 0 self.func = None @@ -759,7 +751,7 @@ class GrampsParser(UpdateCallback): "pos": (self.start_pos, None), "postal": (None, self.stop_postal), "range": (self.start_range, None), - "researcher": (None, self.stop_research), + "researcher": (self.start_research, None), "resname": (None, self.stop_resname), "resaddr": (None, self.stop_resaddr), "reslocality": (None, self.stop_reslocality), @@ -790,6 +782,10 @@ class GrampsParser(UpdateCallback): "repository": (self.start_repo, self.stop_repo), "reporef": (self.start_reporef, self.stop_reporef), "rname": (None, self.stop_rname), + "tree": (self.start_tree, self.stop_tree), + "copyright": (None, self.stop_tree_copyright), + "license": (None, self.stop_tree_license), + "contributors": (None, self.stop_tree_contributors), } self.grampsuri = re.compile( r"^gramps://(?P[A-Z][a-z]+)/" r"handle/(?P\w+)$" @@ -1070,7 +1066,12 @@ class GrampsParser(UpdateCallback): # If the database was originally empty we update the researcher from # the XML (or initialised to no researcher) if self.import_researcher: - self.db.set_researcher(self.owner) + self.db.set_researcher(self.owner, quiet=True) + self.db.set_researcher_change(self.owner_change) + self.db.set_researcher_handle(self.owner_handle, quiet=True) + self.db.set_tree(self.tree, quiet=True) + self.db.set_tree_change(self.tree_change) + if self.home is not None: person = self.db.get_person_from_handle(self.home) self.db.set_default_person_handle(person.handle) @@ -3055,7 +3056,10 @@ class GrampsParser(UpdateCallback): self.person = None def stop_description(self, tag): - self.event.set_description(tag) + if self.in_tree: + self.tree.set_description(tag) + else: + self.event.set_description(tag) def stop_cause(self, tag): # The old event's cause is now an attribute @@ -3291,43 +3295,54 @@ class GrampsParser(UpdateCallback): self.db.commit_note(self.note, self.trans, self.note.get_change_time()) self.note = None - def stop_research(self, tag): - self.owner.set_name(self.resname) - self.owner.set_address(self.resaddr) - self.owner.set_locality(self.reslocality) - self.owner.set_city(self.rescity) - self.owner.set_state(self.resstate) - self.owner.set_country(self.rescon) - self.owner.set_postal_code(self.respos) - self.owner.set_phone(self.resphone) - self.owner.set_email(self.resemail) + def start_tree(self, attrs): + self.tree.set_name(self.db.get_dbname()) + self.tree_change = int(attrs.get('change', self.change)) + self.in_tree = True + + def stop_tree_copyright(self, tag): + self.tree.set_copyright(tag) + + def stop_tree_license(self, tag): + self.tree.set_license(tag) + + def stop_tree_contributors(self, tag): + self.tree.set_contributors(tag) + + def stop_tree(self, tag): + self.in_tree = False + + def start_research(self, attrs): + self.owner_change = int(attrs.get('change', self.change)) + if 'handle' in attrs: + self.owner_handle = attrs['handle'].replace('_', '') def stop_resname(self, tag): - self.resname = tag + self.owner.set_name(tag) def stop_resaddr(self, tag): - self.resaddr = tag + self.owner.set_address(tag) def stop_reslocality(self, tag): - self.reslocality = tag + self.owner.set_locality(tag) def stop_rescity(self, tag): - self.rescity = tag + self.owner.set_city(tag) def stop_resstate(self, tag): - self.resstate = tag + self.owner.set_state(tag) def stop_rescountry(self, tag): - self.rescon = tag + self.owner.set_country(tag) def stop_respostal(self, tag): - self.respos = tag + self.owner.set_postal_code(tag) def stop_resphone(self, tag): - self.resphone = tag + self.owner.set_phone(tag) def stop_resemail(self, tag): - self.resemail = tag + self.owner.set_email(tag) def stop_mediapath(self, tag): self.mediapath = tag diff --git a/gramps/plugins/importer/test/importvcard_test.py b/gramps/plugins/importer/test/importvcard_test.py index 977e887a4..b0c388114 100644 --- a/gramps/plugins/importer/test/importvcard_test.py +++ b/gramps/plugins/importer/test/importvcard_test.py @@ -42,7 +42,12 @@ class VCardCheck(unittest.TestCase): self.header = """
- + + + + + +
""" % ( GRAMPS_XML_VERSION, date[0], @@ -84,6 +89,11 @@ class VCardCheck(unittest.TestCase): element.text = "" if element.tail is not None and not element.tail.strip(): element.tail = "" + if element.tag.split("}")[-1] in [ + "database-id", + "last-transaction-timestamp", + ]: + element.text = "" return ET.tostring(doc, encoding="utf-8") diff --git a/gramps/plugins/importer/test/importvcard_test.py.lxml b/gramps/plugins/importer/test/importvcard_test.py.lxml new file mode 100644 index 000000000..91c1c10e0 --- /dev/null +++ b/gramps/plugins/importer/test/importvcard_test.py.lxml @@ -0,0 +1,574 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2011 Michiel D. Nauta +# +# 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. +# + +""" +Unittest of import of VCard +""" + +# in case of a failing test, add True as last parameter to do_case to see the output. + +import unittest +import sys +import os +import time +import subprocess +#import xml.etree.ElementTree as ET +import lxml.etree as ET + +from gramps.gen.const import DATA_DIR +from gramps.plugins.lib.libgrampsxml import GRAMPS_XML_VERSION +from gramps.version import VERSION +from gramps.plugins.importer.importvcard import VCardParser, fitin, splitof_nameprefix + +class VCardCheck(unittest.TestCase): + def setUp(self): + self.parser = ET.XMLParser(remove_blank_text=True) + styledoc = ET.parse(os.path.join(DATA_DIR, "gramps_canonicalize.xsl"), + parser=self.parser) + self.transform = ET.XSLT(styledoc) + + date = time.localtime(time.time()) + self.header = """ +
+ + + + +
""" % (GRAMPS_XML_VERSION, date[0], date[1], date[2], VERSION) + + expect_str = self.header + """ + + + U + + Lastname + + + +
""" + +# namespace = "http://gramps-project.org/xml/%s/" % GRAMPS_XML_VERSION +# ET.register_namespace("", namespace) + + self.gramps = ET.XML(expect_str) + self.person = self.gramps[1][0] + self.name = self.person[1] + self.vcard = ["BEGIN:VCARD", "VERSION:3.0", "FN:Lastname", + "N:Lastname;;;;", "END:VCARD"] + + def canonicalize(self, doctxt): + """ + Return a canonicalized string representation + + :param doctxt: the text to bring in canonical form. + :type doctxt: either a string or an Xml document. + :returns: The text but in canonical form. + :rtype: string + """ +# if isinstance(doctxt, bytes): +# doc = ET.fromstring(doctxt, parser=self.parser) +# elif isinstance(doctxt, ET._Element): +# doc = doctxt +# else: +# raise TypeError + doc = doctxt + canonical_doc = self.transform(doc, replace_handles="ID") +# result = ET.tostring(canonical_doc, pretty_print=True) + result = ET.tostring(canonical_doc) + #print(str(result, 'utf-8')) + return result + +# def canonicalize(self, doc): +# handles = {} +# for element in doc.iter("*"): +# gramps_id = element.get('id') +# if gramps_id is not None: +# handles[element.get('handle')] = gramps_id +# element.set('handle', gramps_id) +# hlink = element.get('hlink') +# if hlink is not None: +# element.set('hlink', handles.get(hlink)) +# if element.get('change') is not None: +# del element.attrib['change'] +# if element.text is not None and not element.text.strip(): +# element.text = '' +# if element.tail is not None and not element.tail.strip(): +# element.tail = '' + +# return ET.tostring(doc, encoding='utf-8') + + def do_case(self, input_str, expect_doc, debug=True): + if debug: + print(input_str) + + gcmd = [sys.executable, 'Gramps.py', + '-d', '.Date', '-d', '.ImportVCard', + '--config=preferences.eprefix:DEFAULT', + '-i', '-', '-f', 'vcf', + '-e', '-', '-f', 'gramps'] + process = subprocess.Popen(gcmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=os.environ) + result_str, err_str = process.communicate(input_str.encode("utf-8")) + if debug: + print(err_str) + result_doc = ET.XML(result_str) + +# if debug: + if True: + print(self.canonicalize(result_doc)) + print(self.canonicalize(expect_doc)) + self.assertEqual(self.canonicalize(result_doc), + self.canonicalize(expect_doc)) + + def test_base(self): + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_splitof_nameprefix_noprefix(self): + self.assertEqual(splitof_nameprefix("Noprefix"), ('',"Noprefix")) + + def test_splitof_nameprefix_prefix(self): + self.assertEqual(splitof_nameprefix("van Prefix"), ('van',"Prefix")) + + def test_splitof_nameprefix_doublespace(self): + self.assertEqual(splitof_nameprefix("van Prefix"), ('van',"Prefix")) + + def test_fitin_regular(self): + self.assertEqual(fitin("Mr. Gaius Julius Caesar", + "Gaius Caesar", "Julius"), 6) + + def test_fitin_wrong_receiver(self): + self.assertEqual(fitin("A B C", "A D", "B"), -1) + + def test_fitin_wrong_element(self): + self.assertRaises(ValueError, fitin, "A B C", "A C", "D") + + def test_fitin_last_element(self): + self.assertRaises(IndexError, fitin, "A B C", "A B", "C") + + def test_name_value_split_begin_colon(self): + self.vcard.insert(4, ":email@example.com") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_name_value_split_quoted_colon(self): + self.vcard.insert(4, 'TEL;TYPE="FANCY:TYPE":01234-56789') + address = ET.SubElement(self.person, "address") + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_name_value_split_grouped(self): + self.vcard[1] = "group." + self.vcard[1] + self.vcard[3] = "group." + self.vcard[3] + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_unesc_string(self): + self.assertEqual(VCardParser.unesc("TEL:012\\\\345\\,67\\;89"), + "TEL:012\\345,67;89") + + def test_unesc_list(self): + self.assertEqual(VCardParser.unesc([r"Last\,name", r"First\;name"]), + ["Last,name", "First;name"]) + + def test_unesc_tuple(self): + self.assertRaises(TypeError, VCardParser.unesc, + (r"Last\,name", r"First\;name")) + + def test_count_escapes_null(self): + self.assertEqual(VCardParser.count_escapes("Lastname"), 0) + + def test_count_escapes_one(self): + self.assertEqual(VCardParser.count_escapes("Lastname\\"), 1) + + def test_count_escapes_two(self): + self.assertEqual(VCardParser.count_escapes(r"Lastname\\"), 2) + + def test_split_unescaped_regular(self): + self.assertEqual(VCardParser.split_unescaped("Lastname;Firstname", ';'), + ["Lastname", "Firstname"]) + + def test_split_unescaped_one(self): + self.assertEqual(VCardParser.split_unescaped("Lastname\\;Firstname", ';'), + ["Lastname\\;Firstname",]) + + def test_split_unescaped_two(self): + self.assertEqual(VCardParser.split_unescaped("Lastname\\\\;Firstname", ';'), + ["Lastname\\\\", "Firstname",]) + + def test_split_unescaped_three(self): + self.assertEqual(VCardParser.split_unescaped(r"Lastname\\\;Firstname", ';'), + [r"Lastname\\\;Firstname",]) + + def test_get_next_line_continuation(self): + self.vcard.insert(4, "TEL:01234-\r\n 56789") + address = ET.SubElement(self.person, "address") + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_get_next_line_cr(self): + self.vcard.insert(4, "TEL:01234-56789\r") + address = ET.SubElement(self.person, "address") + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_get_next_line_strip(self): + self.vcard.insert(4, "TEL:01234-56789 ") + address = ET.SubElement(self.person, "address") + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_get_next_line_none(self): + self.vcard.insert(4, "") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_parse_vCard_file_no_colon(self): + self.vcard.insert(4, "TEL;01234-56789") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_parse_vCard_file_lowercase(self): + self.vcard.insert(4, "tel:01234-56789") + address = ET.SubElement(self.person, "address") + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_parse_vCard_unknown_property(self): + self.vcard.insert(4, "TELEPHONE:01234-56789") + self.gramps = self.gramps + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_next_person_no_end(self): + self.vcard[4] = "BEGIN:VCARD" + self.vcard.extend(["VERSION:3.0", "FN:Another", "N:Another;;;;", "END:VCARD"]) + attribs = {'handle': 'I0001', 'id': 'I0001'} + person = ET.SubElement(self.gramps[1], "person", attribs) + ET.SubElement(person, 'gender').text = 'U' + name = ET.SubElement(person, 'name', {'type': 'Birth Name'}) + ET.SubElement(name, 'surname').text = 'Another' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_check_version(self): + self.vcard = ["BEGIN:VCARD", "VERSION:3.7", "FN:Another", + "N:Another;;;;", "END:VCARD"] + expected = ET.XML(self.header + "
") + self.do_case("\r\n".join(self.vcard), expected) + + def test_add_formatted_name_twice(self): + self.vcard[2] = "FN:Lastname B A" + self.vcard[3] = "N:Lastname;A;B;;" + self.vcard.insert(4, "FN:Lastname A B") + name = self.person[1] + first = ET.Element("first") + first.text = "B A" + name.insert(0, first) + call = ET.Element("call") + call.text = "A" + name.insert(1, call) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_parts_twice(self): + self.vcard.insert(4, "N:Another;;;;") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_regular(self): + self.vcard[2] = "FN:Mr. Firstname Middlename Lastname Jr." + self.vcard[3] = "N:Lastname;Firstname;Middlename;Mr.;Jr." + first = ET.Element('first') + first.text = 'Firstname Middlename' + self.name.insert(0, first) + ET.SubElement(self.name, 'suffix').text = 'Jr.' + ET.SubElement(self.name, 'title').text = 'Mr.' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_multisurname(self): + self.vcard[2] = "FN:Lastname Lastname2" + self.vcard[3] = "N:Lastname,Lastname2;;;;" + surname = ET.SubElement(self.name, 'surname') + surname.text = 'Lastname2' + surname.set('prim', '0') + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_prefixsurname(self): + self.vcard[2] = "FN:van d'Alembert" + self.vcard[3] = "N:van d'Alembert;;;;" + surname = self.name[0] + surname.text = 'Alembert' + surname.set('prefix', "van d'") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_only_surname(self): + self.vcard[3] = "N:Lastname" + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_upto_firstname(self): + self.vcard[2] = "FN:Firstname Lastname" + self.vcard[3] = "N:Lastname; Firstname;" + first = ET.Element('first') + first.text = 'Firstname' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_name_empty(self): + self.vcard[2] = "FN:Lastname" + self.vcard[3] = "N: " + self.gramps.remove(self.gramps[1]) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_regular(self): + self.vcard[2] = "FN:A B Lastname" + self.vcard[3] = "N:Lastname;A;B;;" + first = ET.Element('first') + first.text = 'A B' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_callname(self): + self.vcard[2] = "FN:A B Lastname" + self.vcard[3] = "N:Lastname;B;A;;" + first = ET.Element('first') + first.text = 'A B' + self.name.insert(0, first) + call = ET.Element('call') + call.text = 'B' + self.name.insert(1, call) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_incomplete_fn(self): + self.vcard[2] = "FN:A Lastname" + self.vcard[3] = "N:Lastname;A;B;;" + first = ET.Element('first') + first.text = 'A B' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_middle(self): + self.vcard[2] = "FN:A B C Lastname" + self.vcard[3] = "N:Lastname;B;A C;;" + first = ET.Element('first') + first.text = 'A B C' + self.name.insert(0, first) + call = ET.Element('call') + call.text = 'B' + self.name.insert(1, call) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_fn_not_given(self): + self.vcard[2] = "FN:B Lastname" + self.vcard[3] = "N:Lastname;A;B;;" + first = ET.Element('first') + first.text = 'A B' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_no_addnames(self): + self.vcard[2] = "FN:A Lastname" + self.vcard[3] = "N:Lastname;A;;;" + first = ET.Element('first') + first.text = 'A' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_no_givenname(self): + self.vcard[2] = "FN:A Lastname" + self.vcard[3] = "N:Lastname;;A;;" + first = ET.Element('first') + first.text = 'A' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_firstname_no_fn(self): + self.vcard[2] = "FN:" + self.vcard[3] = "N:Lastname;;A;;" + first = ET.Element('first') + first.text = 'A' + self.name.insert(0, first) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_nicknames_single(self): + self.vcard.insert(4, "NICKNAME:Ton") + attribs = {"alt": "1", "type": "Birth Name"} + name = ET.SubElement(self.person, 'name', attribs) + ET.SubElement(name, 'nick').text = "Ton" + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_nicknames_empty(self): + self.vcard.insert(4, "NICKNAME:") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_nicknames_multiple(self): + self.vcard.insert(4, r"NICKNAME:A,B\,C") + attribs = {"alt": "1", "type": "Birth Name"} + name = ET.SubElement(self.person, 'name', attribs) + ET.SubElement(name, 'nick').text = "A" + attribs = {"alt": "1", "type": "Birth Name"} + name = ET.SubElement(self.person, 'name', attribs) + ET.SubElement(name, 'nick').text = "B,C" + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_address_all(self): + self.vcard.insert(4, "ADR:box 1;bis;Broadway 11; New York; New York; NY1234; USA") + address = ET.SubElement(self.person, 'address') + ET.SubElement(address, 'street').text = 'box 1 bis Broadway 11' + ET.SubElement(address, 'city').text = 'New York' + ET.SubElement(address, 'state').text = 'New York' + ET.SubElement(address, 'country').text = 'USA' + ET.SubElement(address, 'postal').text = 'NY1234' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_address_too_many(self): + self.vcard.insert(4, r"ADR:;;Broadway 11; New\,York; ;; USA; Earth") + address = ET.SubElement(self.person, 'address') + ET.SubElement(address, 'street').text = 'Broadway 11' + ET.SubElement(address, 'city').text = 'New,York' + ET.SubElement(address, 'country').text = 'USA' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_address_too_few(self): + self.vcard.insert(4, "ADR:;;Broadway 11; New York") + address = ET.SubElement(self.person, 'address') + ET.SubElement(address, 'street').text = 'Broadway 11' + ET.SubElement(address, 'city').text = 'New York' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_address_empty(self): + self.vcard.insert(4, "ADR: ") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_phone_regular(self): + self.vcard.insert(4, "TEL:01234-56789") + address = ET.SubElement(self.person, 'address') + ET.SubElement(address, 'phone').text = '01234-56789' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_phone_empty(self): + self.vcard.insert(4, "TEL: ") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_birthday_regular(self): + self.vcard.insert(4, 'BDAY:2001-09-28') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'dateval', {'val': '2001-09-28'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_birthday_datetime(self): + self.vcard.insert(4, 'BDAY:2001-09-28T09:23:47Z') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'dateval', {'val': '2001-09-28'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_birthday_no_dash(self): + self.vcard.insert(4, 'BDAY:20010928') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'dateval', {'val': '2001-09-28'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_birthday_long_Feb_converted_to_datestr(self): + self.vcard.insert(4, 'BDAY:20010229') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'datestr', {'val': '20010229'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_birthday_invalid_format_converted_to_datestr(self): + self.vcard.insert(4, 'BDAY:unforgettable') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'datestr', {'val': 'unforgettable'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_birthday_one_dash(self): + self.vcard.insert(4, 'BDAY:2001-0928') + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Birth' + ET.SubElement(event, 'dateval', {'val': '2001-09-28'}) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_birthday_empty(self): + self.vcard.insert(4, "BDAY: ") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_occupation_regular(self): + self.vcard.insert(4, "ROLE:scarecrow") + attribs = {'hlink': 'E0000', 'role': 'Primary'} + eventref = ET.SubElement(self.person, 'eventref', attribs) + events = ET.Element('events') + self.gramps.insert(1, events) + attribs = {'handle': 'E0000', 'id': 'E0000'} + event = ET.SubElement(events, 'event', attribs) + ET.SubElement(event, 'type').text = 'Occupation' + ET.SubElement(event, 'description').text = 'scarecrow' + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_occupation_empty(self): + self.vcard.insert(4, "ROLE: ") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_url_regular(self): + self.vcard.insert(4, "URL:http://www.example.com") + attribs = {'href': 'http://www.example.com', 'type': 'Unknown'} + ET.SubElement(self.person, 'url', attribs) + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_url_empty(self): + self.vcard.insert(4, "URL: ") + self.do_case("\r\n".join(self.vcard), self.gramps) + + def test_add_email(self): + self.vcard.insert(4, "EMAIL:me@example.org") + attribs = {'href': 'me@example.org', 'type': 'E-mail'} + ET.SubElement(self.person, 'url', attribs) + self.do_case("\r\n".join(self.vcard), self.gramps) + + +if __name__ == "__main__": + unittest.main() diff --git a/gramps/plugins/lib/libgrampsxml.py b/gramps/plugins/lib/libgrampsxml.py index 2954cb71a..400d598cf 100644 --- a/gramps/plugins/lib/libgrampsxml.py +++ b/gramps/plugins/lib/libgrampsxml.py @@ -33,5 +33,5 @@ # Public Constants # # ------------------------------------------------------------------------ -GRAMPS_XML_VERSION_TUPLE = (1, 7, 1) # version for Gramps 4.2 +GRAMPS_XML_VERSION_TUPLE = (1, 7, 2) # version for Gramps 5.2 GRAMPS_XML_VERSION = ".".join(str(i) for i in GRAMPS_XML_VERSION_TUPLE) diff --git a/gramps/plugins/tool/edittree.glade b/gramps/plugins/tool/edittree.glade new file mode 100644 index 000000000..a34681b2b --- /dev/null +++ b/gramps/plugins/tool/edittree.glade @@ -0,0 +1,272 @@ + + + + + + + + False + center + + + True + False + + + True + False + 6 + vertical + + + True + False + + + False + False + 6 + 0 + + + + + + True + False + 6 + 6 + 6 + + + True + False + start + _Name: + True + + + 0 + 0 + + + + + True + False + start + _Copyright: + True + + + 0 + 1 + + + + + True + False + start + _License: + True + + + 0 + 2 + + + + + True + False + _Description: + True + + + 0 + 3 + + + + + True + False + _Contributors: + True + + + 0 + 4 + + + + + True + True + True + + + + 1 + 0 + + + + + True + True + True + + + + 1 + 1 + + + + + True + True + True + + + + 1 + 2 + + + + + description + True + True + True + True + + + 1 + 3 + + + + + contributors + True + True + True + True + + + 1 + 4 + + + + + + + + + + + + + + + + + + + + False + True + 1 + + + + + True + False + end + + + gtk-cancel + False + True + True + True + False + True + + + + False + False + 0 + + + + + gtk-ok + False + True + True + True + False + True + + + + False + False + 1 + + + + + gtk-help + False + True + True + True + False + True + + + + False + False + 2 + + + + + False + True + end + 2 + + + + + True + False + + + False + True + 3 + end + 3 + + + + + + + + + + + diff --git a/gramps/plugins/tool/edittree.py b/gramps/plugins/tool/edittree.py new file mode 100644 index 000000000..145b6f921 --- /dev/null +++ b/gramps/plugins/tool/edittree.py @@ -0,0 +1,200 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2007 Donald N. Allingham +# Copyright (C) 2008 Brian G. Matherly +# Copyright (C) 2010 Jakim Friant +# Copyright (C) 2010 Nick Hall +# Copyright (C) 2022 Christopher Horn +# +# 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. +# + +"""Tools/Database Processing/Edit Tree Information""" + +# ------------------------------------------------------------------------- +# +# Python modules +# +# ------------------------------------------------------------------------- +import os +from copy import copy + +# ------------------------------------------------------------------------- +# +# Gramps modules +# +# ------------------------------------------------------------------------- +from gramps.cli.clidbman import CLIDbManager +from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gen.const import URL_MANUAL_PAGE +from gramps.gen.recentfiles import rename_filename +from gramps.gui.dialog import ErrorDialog +from gramps.gui.display import display_help +from gramps.gui.glade import Glade +from gramps.gui.managedwindow import ManagedWindow +from gramps.gui.plug import tool +from gramps.gui.widgets import MonitoredEntry + +_ = glocale.translation.sgettext + + +# ------------------------------------------------------------------------- +# +# Constants +# +# ------------------------------------------------------------------------- +WIKI_HELP_PAGE = "%s_-_Tools" % URL_MANUAL_PAGE +WIKI_HELP_SEC = _("Edit_Tree_Information", "manual") + + +# ------------------------------------------------------------------------- +# +# TreeEditor +# +# ------------------------------------------------------------------------- +class TreeEditor(tool.Tool, ManagedWindow): + """ + Enables editing of tree metadata like copyright and description. + """ + + def __init__(self, dbstate, user, options_class, name, callback=None): + uistate = user.uistate + ManagedWindow.__init__(self, uistate, [], self.__class__) + tool.Tool.__init__(self, dbstate, options_class, name) + self.dbstate = dbstate + self.db.refresh_cached_tree_name() + self.tree = self.db.get_tree() + self.edit = copy(self.tree) + self.display() + + def display(self): + """ + Display the edit dialog. + """ + top_dialog = Glade() + + # Setup window + window = top_dialog.toplevel + self.set_window( + window, + top_dialog.get_object("title"), + _("Tree Information Editor"), + ) + self.setup_configs("interface.edit_tree", 500, 400) + + # Move help button to the left side + action_area = top_dialog.get_object("action_area") + help_button = top_dialog.get_object("help_button") + action_area.set_child_secondary(help_button, True) + + # Connect signals + top_dialog.connect_signals( + { + "on_ok_button_clicked": self.on_ok_button_clicked, + "on_cancel_button_clicked": self.close, + "on_help_button_clicked": on_help_button_clicked, + } + ) + + # Setup inputs + self.entries = [] + entry = [ + ("name", self.edit.set_name, self.edit.get_name), + ( + "description", + self.edit.set_description, + self.edit.get_description, + ), + ("copyright", self.edit.set_copyright, self.edit.get_copyright), + ("license", self.edit.set_license, self.edit.get_license), + ( + "contributors", + self.edit.set_contributors, + self.edit.get_contributors, + ), + ] + for (name, set_fn, get_fn) in entry: + self.entries.append( + MonitoredEntry( + top_dialog.get_object(name), + set_fn, + get_fn, + self.db.readonly, + ) + ) + self.show() + + def build_menu_names(self, obj): + """ + Return the menu names. + """ + return (_("Main window"), _("Edit tree information")) + + def on_ok_button_clicked(self, _cb_obj): + """ + Save the changes. + """ + name = self.edit.get_name().strip() + if name != self.tree.get_name(): + if self.rename_database(): + self.db.refresh_cached_tree_name() + self.uistate.window.set_title(name) + else: + if self.edit != self.tree: + self.db.set_tree(self.edit) + self.close() + + def rename_database(self): + """ + Renames the database by writing the new value to the name.txt file + """ + name = self.edit.get_name().strip() + dbman = CLIDbManager(self.dbstate) + for (tree_name, dummy_dir_name) in dbman.family_tree_list(): + if name == tree_name: + ErrorDialog( + _("Could not rename the Family Tree."), + _("Family Tree already exists, choose a unique name."), + parent=self, + ) + return False + + filename = os.path.join(self.db.get_save_path(), "name.txt") + old_text, new_text = dbman.rename_database(filename, name) + if old_text is not None: + rename_filename(old_text, new_text) + return True + + +def on_help_button_clicked(_cb_obj): + """ + Display the relevant portion of Gramps manual + """ + display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC) + + +# ------------------------------------------------------------------------- +# +# TreeEditorOptions (None at the moment) +# +# ------------------------------------------------------------------------- +class TreeEditorOptions(tool.ToolOptions): + """ + Defines options and provides handling interface. + """ + + def __init__(self, name, person_id=None): + tool.ToolOptions.__init__(self, name, person_id) diff --git a/gramps/plugins/tool/ownereditor.glade b/gramps/plugins/tool/ownereditor.glade index ae758afa9..2210dcc14 100644 --- a/gramps/plugins/tool/ownereditor.glade +++ b/gramps/plugins/tool/ownereditor.glade @@ -1,65 +1,65 @@ - + - False + False - Copy from DB to Preferences - False + Copy from DB to Preferences + False copy_from_db_to_preferences True - False - True - True - accelgroup1 + False + True + True + accelgroup1 - Copy from Preferences to DB - False + Copy from Preferences to DB + False copy_from_preferences_to_db True - False - True - True - accelgroup1 + False + True + True + accelgroup1 - False - center + False + center True - False + False True - False - 6 + False + 6 vertical True - False - end + False + end gtk-cancel - False + False True - True - True - False - True + True + True + False + True @@ -71,12 +71,12 @@ gtk-ok - False + False True - True - True - False - True + True + True + False + True @@ -88,12 +88,12 @@ gtk-help - False + False True - True - True - False - True + True + True + False + True @@ -106,27 +106,27 @@ False True - end + end 0 True - False + False False True 3 - end + end 1 True - False + False False @@ -136,258 +136,428 @@ + True - False - 6 - 6 - 6 + False + 6 + 6 + 6 True - False + False start _Name: - True - name + True + name - 0 - 0 + 0 + 0 True - False + False start _Street: - True - address + True + address - 0 - 1 + 0 + 1 True - False + False start _City: - True - city + True + city - 0 - 3 + 0 + 3 True - False + False start _State/County: - True - state + True + state - 0 - 4 + 0 + 4 True - False + False start _Country: - True - country + True + country - 0 - 5 + 0 + 5 True - False + False start _ZIP/Postal Code: - True - zip + True + zip - 0 - 6 + 0 + 6 True - False + False start _Phone: - True - phone + True + phone - 0 - 7 + 0 + 7 True - False + False start _Email: - True - email + True + email - 0 - 8 + 0 + 8 True - True + True True - + - 1 - 0 + 1 + 0 True - True + True True - + - 1 - 1 + 1 + 1 True - True + True True - + - 1 - 3 + 1 + 3 True - True + True True - + - 1 - 4 + 1 + 4 True - True + True True - + - 1 - 5 + 1 + 5 True - True + True True - + - 1 - 6 + 1 + 6 True - True + True True - + - 1 - 7 + 1 + 7 True - True + True True - + - 1 - 8 + 1 + 8 True - False + False start _Locality: - True - locality + True + locality - 0 - 2 + 0 + 2 True - True + True True - + - 1 - 2 + 1 + 2 + + + + + True + False + Right-click to copy from/to Researcher Preferences + + + 0 + 10 + 2 True - False - Right-click to copy from/to Researcher Preferences + False + start + _Person In Tree: + True - 0 - 9 - 2 + 0 + 9 + + + True + False + + + False + True + True + True + none + + + + True + False + list-remove + + + Remove + + + + + + + Person + + + + + False + False + end + 0 + + + + + False + True + True + True + none + + + + True + False + gtk-index + + + Selector + + + + + + + Person + + + + + False + False + 2 + end + 1 + + + + + False + True + True + True + none + + + + True + False + list-add + + + Add + + + + + + + Person + + + + + False + False + end + 2 + + + + + person_name + True + False + start + + + True + True + 2 + + + + + 1 + 9 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False diff --git a/gramps/plugins/tool/ownereditor.py b/gramps/plugins/tool/ownereditor.py index 06964934b..860882d56 100644 --- a/gramps/plugins/tool/ownereditor.py +++ b/gramps/plugins/tool/ownereditor.py @@ -5,6 +5,7 @@ # Copyright (C) 2008 Brian G. Matherly # Copyright (C) 2010 Jakim Friant # Copyright (C) 2010 Nick Hall +# Copyright (C) 2022 Christopher Horn # # 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 @@ -25,28 +26,27 @@ # ------------------------------------------------------------------------- # -# gnome/gtk +# Gramps modules # # ------------------------------------------------------------------------- -from gi.repository import Gtk - -# ------------------------------------------------------------------------- -# -# gramps modules -# -# ------------------------------------------------------------------------- -from gramps.gen.const import URL_MANUAL_PAGE from gramps.gen.config import config +from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gen.const import URL_MANUAL_PAGE +from gramps.gen.display.name import displayer as name_displayer +from gramps.gen.errors import WindowActiveError +from gramps.gen.lib import Person from gramps.gen.utils.config import get_researcher +from gramps.gui.dialog import QuestionDialog from gramps.gui.display import display_help -from gramps.gui.widgets import MonitoredEntry +from gramps.gui.editors import EditPerson +from gramps.gui.glade import Glade from gramps.gui.managedwindow import ManagedWindow from gramps.gui.plug import tool -from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gui.selectors import SelectorFactory +from gramps.gui.utils import is_right_click +from gramps.gui.widgets import MonitoredEntry _ = glocale.translation.sgettext -from gramps.gui.glade import Glade -from gramps.gui.utils import is_right_click # ------------------------------------------------------------------------- # @@ -58,7 +58,7 @@ WIKI_HELP_SEC = _("Edit_Database_Owner_Information", "manual") # ------------------------------------------------------------------------- # -# constants +# Constants # # ------------------------------------------------------------------------- config_keys = ( @@ -91,45 +91,49 @@ class OwnerEditor(tool.Tool, ManagedWindow): uistate = user.uistate ManagedWindow.__init__(self, uistate, [], self.__class__) tool.Tool.__init__(self, dbstate, options_class, name) - + self.dbstate = dbstate + self.person = self.db.get_researcher_person() self.display() def display(self): + """ + Display the edit dialog. + """ # get the main window from glade - topDialog = Glade() + top_dialog = Glade() # set gramps style title for the window - window = topDialog.toplevel + window = top_dialog.toplevel self.set_window( - window, topDialog.get_object("title"), _("Database Owner Editor") + window, top_dialog.get_object("title"), _("Database Owner Editor") ) self.setup_configs("interface.ownereditor", 500, 400) # move help button to the left side - action_area = topDialog.get_object("action_area") - help_button = topDialog.get_object("help_button") + action_area = top_dialog.get_object("action_area") + help_button = top_dialog.get_object("help_button") action_area.set_child_secondary(help_button, True) # connect signals - topDialog.connect_signals( + top_dialog.connect_signals( { + "on_select_button_clicked": self.on_select_button_clicked, + "on_add_button_clicked": self.on_add_button_clicked, + "on_remove_button_clicked": self.on_remove_button_clicked, "on_ok_button_clicked": self.on_ok_button_clicked, "on_cancel_button_clicked": self.close, - "on_help_button_clicked": self.on_help_button_clicked, + "on_help_button_clicked": on_help_button_clicked, "on_eventbox_button_press_event": self.on_button_press_event, "on_menu_activate": self.on_menu_activate, } ) # fetch the popup menu - self.menu = topDialog.get_object("popup_menu") + self.menu = top_dialog.get_object("popup_menu") self.track_ref_for_deletion("menu") - # topDialog.connect_signals({"on_menu_activate": self.on_menu_activate}) - # get current db owner and attach it to the entries of the window self.owner = self.db.get_researcher() - self.entries = [] entry = [ ("name", self.owner.set_name, self.owner.get_name), @@ -146,52 +150,153 @@ class OwnerEditor(tool.Tool, ManagedWindow): for name, set_fn, get_fn in entry: self.entries.append( MonitoredEntry( - topDialog.get_object(name), set_fn, get_fn, self.db.readonly + top_dialog.get_object(name), + set_fn, + get_fn, + self.db.readonly, ) ) + + self.person_label = top_dialog.get_object("person_name") + self.render_person() + # ok, let's see what we've done self.show() - def on_ok_button_clicked(self, obj): - """Update the current db's owner information from editor""" + def render_person(self): + """ + Render person name and gramps id. + """ + if self.person: + name = "%s [%s]" % ( + name_displayer.display(self.person), + self.person.gramps_id, + ) + self.person_label.set_label(name.strip()) + else: + self.person_label.set_label("") + + def on_select_button_clicked(self, _cb_obj): + """ + Select a person. + """ + person_selector = SelectorFactory("Person") + selector = person_selector( + self.dbstate, self.uistate, self.track, _("Select Researcher") + ) + person = selector.run() + if person: + self.person = person + self.render_person() + + def on_add_button_clicked(self, _cb_obj): + """ + Launch new person editor. + """ + try: + EditPerson( + self.dbstate, + self.uistate, + self.track, + Person(), + self.add_person, + ) + except WindowActiveError: + pass + + def on_remove_button_clicked(self, _cb_obj): + """ + Verify the remove. + """ + if self.person: + QuestionDialog( + _("Remove researcher association to %s?") + % name_displayer.display(self.person), + _( + "Removing the association only removes the reference " + "between the researcher and a person in the database, " + "it does not delete either." + ), + _("Remove Association"), + self.remove_association, + parent=self.window, + ) + + def remove_association(self): + """ + Remove the association. + """ + self.person = "" + self.render_person() + + def add_person(self, person): + """ + Save person for update. + """ + self.person = person + self.render_person() + + def on_ok_button_clicked(self, _cb_obj): + """ + Update the current db's owner information from editor. + """ self.db.set_researcher(self.owner) + if self.person: + self.db.set_researcher_handle(self.person.handle) + else: + self.db.set_researcher_handle("") self.menu.destroy() self.close() - def on_help_button_clicked(self, obj): - """Display the relevant portion of Gramps manual""" - display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC) - - def on_button_press_event(self, obj, event): - """Shows popup-menu for db <-> preferences copying""" + def on_button_press_event(self, _cb_obj, event): + """ + Shows popup-menu for db <-> preferences copying. + """ if is_right_click(event): self.menu.popup(None, None, None, None, 0, 0) def build_menu_names(self, obj): + """ + Return the menu names. + """ return (_("Main window"), _("Edit database owner information")) def on_menu_activate(self, menuitem): - """Copies the owner information from/to the preferences""" + """ + Copies the owner information from/to the preferences. + """ if menuitem.props.name == "copy_from_preferences_to_db": self.owner.set_from(get_researcher()) for entry in self.entries: entry.update() elif menuitem.props.name == "copy_from_db_to_preferences": - for i in range(len(config_keys)): - config.set(config_keys[i], self.owner.get()[i]) + for index, config_key in enumerate(config_keys): + config.set(config_key, self.owner.get()[index]) def clean_up(self): + """ + Cleanup. + """ self.menu.destroy() +def on_help_button_clicked(_cb_obj): + """ + Display the relevant portion of Gramps manual. + """ + display_help(webpage=WIKI_HELP_PAGE, section=WIKI_HELP_SEC) + + # ------------------------------------------------------------------------- # # OwnerEditorOptions (None at the moment) # # ------------------------------------------------------------------------- class OwnerEditorOptions(tool.ToolOptions): - """Defines options and provides handling interface.""" + """ + Defines options and provides handling interface. + """ def __init__(self, name, person_id=None): tool.ToolOptions.__init__(self, name, person_id) diff --git a/gramps/plugins/tool/tools.gpr.py b/gramps/plugins/tool/tools.gpr.py index 80affea38..021f47c69 100644 --- a/gramps/plugins/tool/tools.gpr.py +++ b/gramps/plugins/tool/tools.gpr.py @@ -230,6 +230,28 @@ register( # ------------------------------------------------------------------------ # +# Edit Tree Information +# +#------------------------------------------------------------------------ + +register(TOOL, +id = 'edittree', +name = _("Edit Tree Information"), +description = _("Enabled editing of tree information."), +version = '1.0', +gramps_target_version = MODULE_VERSION, +status = STABLE, +fname = 'edittree.py', +authors = ["Zsolt Foldvari", "Christopher Horn"], +authors_email = ["zfoldvar@users.sourceforge.net"], +category = TOOL_DBPROC, +toolclass = 'TreeEditor', +optionclass = 'TreeEditorOptions', +tool_modes = [TOOL_MODE_GUI] + ) + +#------------------------------------------------------------------------ +# # Edit Database Owner Information # # ------------------------------------------------------------------------