Tree metadata support

This commit is contained in:
Christopher Horn 2023-04-02 14:40:46 -04:00
parent 41720c5a7e
commit ebb72b6803
28 changed files with 2478 additions and 281 deletions

View File

@ -22,7 +22,7 @@
-->
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:g="http://gramps-project.org/xml/1.4.0/">
xmlns:g="http://gramps-project.org/xml/1.7.2/">
<!--
Transform a Gramps XML file into "canonical form", that is strip the
@ -33,8 +33,7 @@
<xsl:output method="xml"/>
<xsl:param name="replace_handles"/>
<xsl:key name="primary_obj" match="g:person|g:family|g:event|g:placeobj|g:source|g:repository|g:object|g:note|g:tag" use="@handle"/>
<xsl:key name="primary_obj" match="g:person|g:family|g:event|g:placeobj|g:source|g:repository|g:object|g:note|g:tag|g:researcher" use="@handle"/>
<xsl:template match="*|@*|text()">
<xsl:copy>
@ -45,7 +44,7 @@
<xsl:template match="@change">
</xsl:template>
<xsl:template match="g:researcher">
<xsl:template match="*[local-name()='provenance' or local-name()='tree' or local-name()='researcher']">
<xsl:copy/>
</xsl:template>
@ -57,6 +56,7 @@
</xsl:copy>
</xsl:template>
<xsl:param name="replace_handles"/>
<xsl:template match="@handle">
<xsl:choose>
<xsl:when test="$replace_handles='ID'">

View File

@ -67,12 +67,11 @@ DATABASE
<!-- ************************************************************
HEADER
A <header> consists of <created> (information about this
genealogical database) and <researcher> (information about the
person who created this genealogical database)
A <header> consists of <created> and several additional sections
with high level information about this genealogical database.
-->
<!ELEMENT header (created, researcher?, mediapath?)>
<!ELEMENT header (created, origin?, comment?, tree?, researcher?, mediapath?)>
<!ELEMENT created EMPTY>
<!ATTLIST created
@ -80,8 +79,40 @@ HEADER
version CDATA #REQUIRED
>
<!--
PROVENANCE identifies the database used to generate this export.
-->
<!ELEMENT provenance (database-id, last-transaction-timestamp, comment>
<!ELEMENT database-id (#PCDATA)>
<!ELEMENT last-transaction-timestamp (#PCDATA)>
<!ELEMENT export-note (#PCDATA)>
<!--
TREE contains information about the tree.
-->
<!ELEMENT tree (name, copyright?, license?, description?,
contributors?)>
<!ATTLIST tree
change CDATA #REQUIRED
>
<!ELEMENT name (#PCDATA)>
<!ELEMENT copyright (#PCDATA)>
<!ELEMENT license (#PCDATA)>
<!ELEMENT description (#PCDATA)>
<!ELEMENT contributors (#PCDATA)>
<!--
RESEARCHER
-->
<!ELEMENT researcher (resname?, resaddr?, reslocality?, rescity?, resstate?,
rescountry?, respostal?, resphone?, resemail?)>
<!ATTLIST researcher
change CDATA #REQUIRED
handle IDREF #IMPLIED
>
<!ELEMENT resname (#PCDATA)>
<!ELEMENT resaddr (#PCDATA)>
<!ELEMENT reslocality (#PCDATA)>

View File

@ -41,14 +41,25 @@
<attribute name="date"><data type="date"/></attribute>
<attribute name="version"><text/></attribute>
</element>
<element name="researcher">
<optional>
<ref name="researcher-content"/>
</optional>
</element>
<optional>
<element name="mediapath"><text/>
</element>
<element name="provenance">
<ref name="provenance-content"/>
</element>
<element name="tree">
<attribute name="change"><text/></attribute>
<ref name="tree-content"/>
</element>
<element name="researcher">
<attribute name="change"><text/></attribute>
<optional><attribute name="handle">
<data type="IDREF"/>
</attribute></optional>
<optional>
<ref name="researcher-content"/>
</optional>
</element>
<element name="mediapath"><text/>
</element>
</optional>
</element>
@ -135,6 +146,22 @@
</element></optional>
</element></start>
<define name="provenance-content">
<element name="database-id"><text/></element>
<element name="last-transaction-timestamp"><text/></element>
<element name="export-note"><text/></element>
</define>
<define name="tree-content">
<element name="name"><text/></element>
<optional>
<element name="copyright"><text/></element>
<element name="license"><text/></element>
<element name="description"><text/></element>
<element name="contributors"><text/></element>
</optional>
</define>
<define name="researcher-content">
<element name="resname"><text/></element>
<optional><element name="resaddr"><text/></element></optional>

View File

@ -1,10 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.1//EN"
"http://gramps-project.org/xml/1.7.1/grampsxml.dtd">
<database xmlns="http://gramps-project.org/xml/1.7.1/">
<!DOCTYPE database PUBLIC "-//Gramps//DTD Gramps XML 1.7.2//EN"
"http://gramps-project.org/xml/1.7.2/grampsxml.dtd">
<database xmlns="http://gramps-project.org/xml/1.7.2/">
<header>
<created date="2017-08-08" version="5.1.0"/>
<researcher>
<created date="2023-07-31" version="5.2.0"/>
<provenance>
<database-id>64c83c93</database-id>
<last-transaction-timestamp>1690844342.4027495</last-transaction-timestamp>
</provenance>
<tree change="1690843977">
<name>example_gramps</name>
<copyright>2001-2006 Donald Allingham, 2007-2023 The Gramps Developers</copyright>
<license>Creative Commons Attribution-ShareAlike 2.5</license>
<description>An example tree for use in producing documentation and with a lot of additional test data to help facilitate unit tests.</description>
<contributors>Donald Allingham, The Gramps Developers</contributors>
</tree>
<researcher change="0">
<resname>Alex Roitman,,,</resname>
</researcher>
<mediapath>{GRAMPS_RESOURCES}/doc/gramps/example/gramps</mediapath>

View File

@ -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

View File

@ -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")

View File

@ -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)

View File

@ -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

View File

@ -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",

178
gramps/gen/lib/tree.py Normal file
View File

@ -0,0 +1,178 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2000-2007 Donald N. Allingham
# Copyright (C) 2013 Doug Blank <doug.blank@gmail.com>
# 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

View File

@ -70,6 +70,8 @@ class BaseMergeCheck(unittest.TestCase):
<database xmlns="http://gramps-project.org/xml/%s/">
<header>
<created date="%04d-%02d-%02d" version="%s"/>
<provenance>\n </provenance>
<tree>\n </tree>
<researcher>\n </researcher>
</header>
""" % (

View File

@ -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"""

View File

@ -41,3 +41,7 @@ class UndoableBuffer(Gtk.TextBuffer):
class PersistentTreeView(Gtk.TreeView):
__gtype_name__ = "PersistentTreeView"
class MultiLineEntry(Gtk.Box):
__gtype_name__ = "MultiLineEntry"

View File

@ -19,11 +19,16 @@
name="PersistentTreeView"
title="Persistent TreeView"
generic-name="resizable_treeview"/>
<glade-widget-class
name="MultiLineEntry"
title="Multi-Line Text Entry"
generic-name="multi_line_entry"/>
</glade-widget-classes>
<glade-widget-group name="GrampsWidgets" title="Gramps Widgets">
<glade-widget-class-ref name="ValidatableMaskedEntry"/>
<glade-widget-class-ref name="UndoableEntry"/>
<glade-widget-class-ref name="StyledTextEditor"/>
<glade-widget-class-ref name="PersistentTreeView"/>
<glade-widget-class-ref name="MultiLineEntry"/>
</glade-widget-group>
</glade-catalog>

View File

@ -22,6 +22,7 @@
"""Custom widgets."""
from .basicentry import *
from .multientry import *
from .buttons import *
from .dateentry import *
from .expandcollapsearrow import *

View File

@ -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

View File

@ -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")

View File

@ -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(' <created date="%04d-%02d-%02d"' % date[:3])
self.g.write(' version="' + self.version + '"')
self.g.write("/>\n")
self.g.write(" <researcher>\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(" <provenance>\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(" </provenance>\n")
self.g.write(' <tree change="%s">\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(" </tree>\n")
self.g.write(' <researcher change="%s"' % self.db.owner_change)
if owner_handle:
self.g.write(' handle="_%s"' % owner_handle)
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(" </researcher>\n")
self.write_metadata()
self.g.write(" </header>\n")

View File

@ -0,0 +1,130 @@
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2007-2009 Douglas S. Blank <doug.blank@gmail.com>
#
# 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")

View File

@ -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<object_class>[A-Z][a-z]+)/" r"handle/(?P<handle>\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

View File

@ -42,7 +42,12 @@ class VCardCheck(unittest.TestCase):
self.header = """<database xmlns="http://gramps-project.org/xml/%s/">
<header>
<created date="%04d-%02d-%02d" version="%s"/>
<researcher/>
<provenance>
<database-id />
<last-transaction-timestamp />
</provenance>
<tree />
<researcher />
</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")

View File

@ -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 = """<database xmlns="http://gramps-project.org/xml/%s/">
<header>
<created date="%04d-%02d-%02d" version="%s"/>
<provenance/>
<tree/>
<researcher/>
</header>""" % (GRAMPS_XML_VERSION, date[0], date[1], date[2], VERSION)
expect_str = self.header + """
<people>
<person handle="I0000" id="I0000">
<gender>U</gender>
<name type="Birth Name">
<surname>Lastname</surname>
</name>
</person>
</people>
</database>"""
# 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 + "</database>")
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()

View File

@ -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)

View File

@ -0,0 +1,272 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<requires lib="grampswidgets" version="0.0"/>
<object class="GtkAccelGroup" id="accelgroup1"/>
<object class="GtkWindow" id="edittree">
<property name="can-focus">False</property>
<property name="window-position">center</property>
<child>
<object class="GtkEventBox" id="eventbox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="vbox2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">6</property>
<property name="position">0</property>
</packing>
</child>
<child>
<!-- n-columns=3 n-rows=5 -->
<object class="GtkGrid" id="table1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="row-spacing">6</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Name:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Copyright:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_License:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">_Description:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">_Contributors:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="name">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="copyright">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="license">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="MultiLineEntry" id="description">
<property name="name">description</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="MultiLineEntry" id="contributors">
<property name="name">contributors</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="action_area">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_cancel_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_ok_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="help_button">
<property name="label">gtk-help</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_help_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="hseparator1">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack-type">end</property>
<property name="position">3</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -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)

View File

@ -1,65 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.10"/>
<object class="GtkAccelGroup" id="accelgroup1"/>
<object class="GtkMenu" id="popup_menu">
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImageMenuItem" id="copy_from_db_to_preferences">
<property name="label" translatable="yes">Copy from DB to Preferences</property>
<property name="use_action_appearance">False</property>
<property name="label">Copy from DB to Preferences</property>
<property name="use-action-appearance">False</property>
<property name="name">copy_from_db_to_preferences</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<property name="accel_group">accelgroup1</property>
<property name="can-focus">False</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<property name="accel-group">accelgroup1</property>
<signal name="activate" handler="on_menu_activate" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="copy_from_preferences_to_db">
<property name="label" translatable="yes">Copy from Preferences to DB</property>
<property name="use_action_appearance">False</property>
<property name="label">Copy from Preferences to DB</property>
<property name="use-action-appearance">False</property>
<property name="name">copy_from_preferences_to_db</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<property name="accel_group">accelgroup1</property>
<property name="can-focus">False</property>
<property name="use-underline">True</property>
<property name="use-stock">True</property>
<property name="accel-group">accelgroup1</property>
<signal name="activate" handler="on_menu_activate" swapped="no"/>
</object>
</child>
</object>
<object class="GtkWindow" id="ownereditor">
<property name="can_focus">False</property>
<property name="window_position">center</property>
<property name="can-focus">False</property>
<property name="window-position">center</property>
<child>
<object class="GtkEventBox" id="eventbox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<signal name="button-press-event" handler="on_eventbox_button_press_event" swapped="no"/>
<child>
<object class="GtkBox" id="vbox2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkButtonBox" id="action_area">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<property name="can-focus">False</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="use_action_appearance">False</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_cancel_button_clicked" swapped="no"/>
</object>
<packing>
@ -71,12 +71,12 @@
<child>
<object class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="use_action_appearance">False</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_ok_button_clicked" swapped="no"/>
</object>
<packing>
@ -88,12 +88,12 @@
<child>
<object class="GtkButton" id="help_button">
<property name="label">gtk-help</property>
<property name="use_action_appearance">False</property>
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">False</property>
<property name="use_stock">True</property>
<property name="can-focus">True</property>
<property name="can-default">True</property>
<property name="receives-default">False</property>
<property name="use-stock">True</property>
<signal name="clicked" handler="on_help_button_clicked" swapped="no"/>
</object>
<packing>
@ -106,27 +106,27 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="hseparator1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">3</property>
<property name="pack_type">end</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
@ -136,258 +136,428 @@
</packing>
</child>
<child>
<!-- n-columns=3 n-rows=11 -->
<object class="GtkGrid" id="table1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">6</property>
<property name="row_spacing">6</property>
<property name="column_spacing">6</property>
<property name="can-focus">False</property>
<property name="border-width">6</property>
<property name="row-spacing">6</property>
<property name="column-spacing">6</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Name:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">name</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">name</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Street:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">address</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">address</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_City:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">city</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">city</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
<property name="left-attach">0</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_State/County:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">state</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">state</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
<property name="left-attach">0</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Country:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">country</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">country</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
<property name="left-attach">0</property>
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_ZIP/Postal Code:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">zip</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">zip</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
<property name="left-attach">0</property>
<property name="top-attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Phone:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">phone</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">phone</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
<property name="left-attach">0</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label8">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Email:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">email</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">email</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">8</property>
<property name="left-attach">0</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="name">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
<property name="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="address">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="city">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
<property name="left-attach">1</property>
<property name="top-attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="state">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
<property name="left-attach">1</property>
<property name="top-attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="country">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
<property name="left-attach">1</property>
<property name="top-attach">5</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="zip">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
<property name="left-attach">1</property>
<property name="top-attach">6</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="phone">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
<property name="left-attach">1</property>
<property name="top-attach">7</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="email">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">•</property>
<property name="invisible-char">•</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">8</property>
<property name="left-attach">1</property>
<property name="top-attach">8</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label9">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Locality:</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">locality</property>
<property name="use-underline">True</property>
<property name="mnemonic-widget">locality</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="locality">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can-focus">True</property>
<property name="hexpand">True</property>
<property name="invisible_char">●</property>
<property name="invisible-char">●</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
<property name="left-attach">1</property>
<property name="top-attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label11">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Right-click to copy from/to Researcher Preferences</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">10</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Right-click to copy from/to Researcher Preferences</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">_Person In Tree:</property>
<property name="use-underline">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">9</property>
<property name="width">2</property>
<property name="left-attach">0</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButton" id="remove_button">
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
<signal name="clicked" handler="on_remove_button_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="image2699">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove</property>
<child internal-child="accessible">
<object class="AtkObject" id="image2699-atkobject">
<property name="AtkObject::accessible-description" translatable="yes">Remove</property>
</object>
</child>
</object>
</child>
<child internal-child="accessible">
<object class="AtkObject" id="remove_button-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">Person</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="select_button">
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
<signal name="clicked" handler="on_select_button_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="image2671">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">gtk-index</property>
<child internal-child="accessible">
<object class="AtkObject" id="image2671-atkobject">
<property name="AtkObject::accessible-description" translatable="yes">Selector</property>
</object>
</child>
</object>
</child>
<child internal-child="accessible">
<object class="AtkObject" id="select_button-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">Person</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">2</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="add_button">
<property name="use-action-appearance">False</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="relief">none</property>
<signal name="clicked" handler="on_add_button_clicked" swapped="no"/>
<child>
<object class="GtkImage" id="image2697">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add</property>
<child internal-child="accessible">
<object class="AtkObject" id="image2697-atkobject">
<property name="AtkObject::accessible-description" translatable="yes">Add</property>
</object>
</child>
</object>
</child>
<child internal-child="accessible">
<object class="AtkObject" id="add_button-atkobject">
<property name="AtkObject::accessible-name" translatable="yes">Person</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack-type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="person_name">
<property name="name">person_name</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">9</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>

View File

@ -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)

View File

@ -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
#
# ------------------------------------------------------------------------