diff --git a/gramps/cli/arghandler.py b/gramps/cli/arghandler.py index 0db169d30..47e9b8da6 100644 --- a/gramps/cli/arghandler.py +++ b/gramps/cli/arghandler.py @@ -44,7 +44,6 @@ import sys #------------------------------------------------------------------------- from gramps.gen.recentfiles import recent_files from gramps.gen.utils.file import rm_tempdir, get_empty_tempdir -from gramps.gen.db import DbBsddb from .clidbman import CLIDbManager, NAME_FILE, find_locker_name from gramps.gen.plug import BasePluginManager @@ -491,7 +490,8 @@ class ArgHandler(object): self.imp_db_path, title = self.dbman.create_new_db_cli() else: self.imp_db_path = get_empty_tempdir("import_dbdir") - newdb = DbBsddb() + + newdb = self.dbstate.make_database("bsddb") newdb.write_version(self.imp_db_path) try: diff --git a/gramps/cli/clidbman.py b/gramps/cli/clidbman.py index 404104afe..5bb87e632 100644 --- a/gramps/cli/clidbman.py +++ b/gramps/cli/clidbman.py @@ -54,7 +54,6 @@ _LOG = logging.getLogger(DBLOGNAME) #------------------------------------------------------------------------- from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext -from gramps.gen.db import DbBsddb from gramps.gen.plug import BasePluginManager from gramps.gen.config import config from gramps.gen.constfunc import win, conv_to_unicode @@ -134,61 +133,42 @@ class CLIDbManager(object): def get_dbdir_summary(self, dirpath, name): """ - Returns (people_count, bsddb_version, schema_version) of - current DB. - Returns ("Unknown", "Unknown", "Unknown") if invalid DB or other error. + dirpath: full path to database + name: proper name of family tree + + Returns dictionary of summary item. + Should include at least, if possible: + + _("Path") + _("Family Tree") + _("Last accessed") + _("Database backend") + _("Locked?") + + and these details: + + _("Number of people") + _("Version") + _("Schema version") """ - from bsddb3 import dbshelve, db - - from gramps.gen.db import META, PERSON_TBL - from gramps.gen.db.dbconst import BDBVERSFN - - bdbversion_file = os.path.join(dirpath, BDBVERSFN) - if os.path.isfile(bdbversion_file): - vers_file = open(bdbversion_file) - bsddb_version = vers_file.readline().strip() - else: - return "Unknown", "Unknown", "Unknown" - - current_bsddb_version = str(db.version()) - if bsddb_version != current_bsddb_version: - return "Unknown", bsddb_version, "Unknown" - - env = db.DBEnv() - flags = db.DB_CREATE | db.DB_PRIVATE |\ - db.DB_INIT_MPOOL |\ - db.DB_INIT_LOG | db.DB_INIT_TXN + dbid = "bsddb" + dbid_path = os.path.join(dirpath, "database.txt") + if os.path.isfile(dbid_path): + dbid = open(dbid_path).read().strip() try: - env.open(dirpath, flags) + database = self.dbstate.make_database(dbid) + database.load(dirpath, None) + retval = database.get_summary() except Exception as msg: - LOG.warning("Error opening db environment for '%s': %s" % - (name, str(msg))) - try: - env.close() - except Exception as msg: - LOG.warning("Error closing db environment for '%s': %s" % - (name, str(msg))) - return "Unknown", bsddb_version, "Unknown" - dbmap1 = dbshelve.DBShelf(env) - fname = os.path.join(dirpath, META + ".db") - try: - dbmap1.open(fname, META, db.DB_HASH, db.DB_RDONLY) - except: - env.close() - return "Unknown", bsddb_version, "Unknown" - schema_version = dbmap1.get(b'version', default=None) - dbmap1.close() - dbmap2 = dbshelve.DBShelf(env) - fname = os.path.join(dirpath, PERSON_TBL + ".db") - try: - dbmap2.open(fname, PERSON_TBL, db.DB_HASH, db.DB_RDONLY) - except: - env.close() - return "Unknown", bsddb_version, schema_version - count = len(dbmap2) - dbmap2.close() - env.close() - return (count, bsddb_version, schema_version) + retval = {"Unavailable": str(msg)[:74] + "..."} + retval.update({ + _("Family Tree"): name, + _("Path"): dirpath, + _("Database backend"): dbid, + _("Last accessed"): time_val(dirpath)[1], + _("Locked?"): self.is_locked(dirpath), + }) + return retval def family_tree_summary(self): """ @@ -199,19 +179,7 @@ class CLIDbManager(object): for item in self.current_names: (name, dirpath, path_name, last, tval, enable, stock_id) = item - count, bsddb_version, schema_version = self.get_dbdir_summary(dirpath, name) - retval = {} - retval[_("Number of people")] = count - if enable: - retval[_("Locked?")] = _("yes") - else: - retval[_("Locked?")] = _("no") - retval[_("Bsddb version")] = bsddb_version - retval[_("Schema version")] = schema_version - retval[_("Family Tree")] = name - retval[_("Path")] = dirpath - retval[_("Last accessed")] = time.strftime('%x %X', - time.localtime(tval)) + retval = self.get_dbdir_summary(dirpath, name) summary_list.append( retval ) return summary_list @@ -275,7 +243,7 @@ class CLIDbManager(object): """ print(_('Import finished...')) - def create_new_db_cli(self, title=None, create_db=True): + def create_new_db_cli(self, title=None, create_db=True, dbid=None): """ Create a new database. """ @@ -294,7 +262,9 @@ class CLIDbManager(object): if create_db: # write the version number into metadata - newdb = DbBsddb() + if dbid is None: + dbid = "bsddb" + newdb = self.dbstate.make_database(dbid) newdb.write_version(new_path) (tval, last) = time_val(new_path) @@ -303,11 +273,11 @@ class CLIDbManager(object): last, tval, False, "")) return new_path, title - def _create_new_db(self, title=None): + def _create_new_db(self, title=None, dbid=None): """ Create a new database, do extra stuff needed """ - return self.create_new_db_cli(title) + return self.create_new_db_cli(title, dbid=dbid) def import_new_db(self, filename, user): """ @@ -360,8 +330,8 @@ class CLIDbManager(object): # Create a new database self.__start_cursor(_("Importing data...")) - dbclass = DbBsddb - dbase = dbclass() + + dbase = self.dbstate.make_database("bsddb") dbase.load(new_path, user.callback) import_function = plugin.get_import_function() diff --git a/gramps/cli/grampscli.py b/gramps/cli/grampscli.py index a45564a19..d7b9afea6 100644 --- a/gramps/cli/grampscli.py +++ b/gramps/cli/grampscli.py @@ -47,9 +47,9 @@ LOG = logging.getLogger(".grampscli") from gramps.gen.display.name import displayer as name_displayer from gramps.gen.config import config from gramps.gen.const import PLUGINS_DIR, USER_PLUGINS +from gramps.gen.db.dbconst import DBBACKEND from gramps.gen.errors import DbError from gramps.gen.dbstate import DbState -from gramps.gen.db import DbBsddb from gramps.gen.db.exceptions import (DbUpgradeRequiredError, BsddbDowngradeError, DbVersionError, @@ -152,9 +152,16 @@ class CLIDbLoader(object): else: mode = 'w' - dbclass = DbBsddb + dbid_path = os.path.join(filename, DBBACKEND) + if os.path.isfile(dbid_path): + with open(dbid_path) as fp: + dbid = fp.read().strip() + else: + dbid = "bsddb" + + db = self.dbstate.make_database(dbid) - self.dbstate.change_database(dbclass()) + self.dbstate.change_database(db) self.dbstate.db.disable_signals() self._begin_progress() diff --git a/gramps/gen/db/__init__.py b/gramps/gen/db/__init__.py index 5b79ce334..d9fcf1ad1 100644 --- a/gramps/gen/db/__init__.py +++ b/gramps/gen/db/__init__.py @@ -86,11 +86,27 @@ More details can be found in the manual's from .base import * from .dbconst import * -from .cursor import * -from .read import * -from .bsddbtxn import * from .txn import * -from .undoredo import * from .exceptions import * -from .write import * -from .backup import backup, restore +from .undoredo import * + +def find_surname_name(key, data): + """ + Creating a surname from raw name, to use for sort and index + returns a byte string + """ + return __index_surname(data[5]) + +def __index_surname(surn_list): + """ + All non pa/matronymic surnames are used in indexing. + pa/matronymic not as they change for every generation! + returns a byte string + """ + from gramps.gen.lib import NameOriginType + if surn_list: + surn = " ".join([x[0] for x in surn_list if not (x[3][0] in [ + NameOriginType.PATRONYMIC, NameOriginType.MATRONYMIC]) ]) + else: + surn = "" + return surn diff --git a/gramps/gen/db/backup.py b/gramps/gen/db/backup.py deleted file mode 100644 index 9329caaff..000000000 --- a/gramps/gen/db/backup.py +++ /dev/null @@ -1,213 +0,0 @@ -# -# Gramps - a GTK+/GNOME based genealogy program -# -# Copyright (C) 2007 Donald N. Allingham -# Copyright (C) 2011 Tim G L Lyons -# -# 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. -# -# gen/db/backup.py - -""" -Description -=========== - -This module Provides backup and restore functions for a database. The -backup function saves the data into backup files, while the restore -function loads the data back into a database. - -You should only restore the data into an empty database. - -Implementation -============== - -Not all of the database tables need to be backed up, since many are -automatically generated from the others. The tables that are backed up -are the primary tables and the metadata table. - -The database consists of a table of "pickled" tuples. Each of the -primary tables is "walked", and the pickled tuple is extracted, and -written to the backup file. - -Restoring the data is just as simple. The backup file is parsed an -entry at a time, and inserted into the associated database table. The -derived tables are built automatically as the items are entered into -db. -""" - -#------------------------------------------------------------------------- -# -# load standard python libraries -# -#------------------------------------------------------------------------- -import os -import pickle - -#------------------------------------------------------------------------ -# -# Gramps libs -# -#------------------------------------------------------------------------ -from .exceptions import DbException -from .write import FAMILY_TBL, PLACES_TBL, SOURCES_TBL, MEDIA_TBL, \ - EVENTS_TBL, PERSON_TBL, REPO_TBL, NOTE_TBL, TAG_TBL, META, CITATIONS_TBL - -#------------------------------------------------------------------------ -# -# Set up logging -# -#------------------------------------------------------------------------ -import logging -LOG = logging.getLogger(".Backup") - -def backup(database): - """ - Exports the database to a set of backup files. These files consist - of the pickled database tables, one file for each table. - - The heavy lifting is done by the private :py:func:`__do__export` function. - The purpose of this function is to catch any exceptions that occur. - - :param database: database instance to backup - :type database: DbDir - """ - try: - __do_export(database) - except (OSError, IOError) as msg: - raise DbException(str(msg)) - -def __mk_backup_name(database, base): - """ - Return the backup name of the database table - - :param database: database instance - :type database: DbDir - :param base: base name of the table - :type base: str - """ - return os.path.join(database.get_save_path(), base + ".gbkp") - -def __mk_tmp_name(database, base): - """ - Return the temporary backup name of the database table - - :param database: database instance - :type database: DbDir - :param base: base name of the table - :type base: str - """ - return os.path.join(database.get_save_path(), base + ".gbkp.new") - -def __do_export(database): - """ - Loop through each table of the database, saving the pickled data - a file. - - :param database: database instance to backup - :type database: DbDir - """ - try: - for (base, tbl) in __build_tbl_map(database): - backup_name = __mk_tmp_name(database, base) - backup_table = open(backup_name, 'wb') - - cursor = tbl.cursor() - data = cursor.first() - while data: - pickle.dump(data, backup_table, 2) - data = cursor.next() - cursor.close() - backup_table.close() - except (IOError,OSError): - return - - for (base, tbl) in __build_tbl_map(database): - new_name = __mk_backup_name(database, base) - old_name = __mk_tmp_name(database, base) - if os.path.isfile(new_name): - os.unlink(new_name) - os.rename(old_name, new_name) - -def restore(database): - """ - Restores the database to a set of backup files. These files consist - of the pickled database tables, one file for each table. - - The heavy lifting is done by the private :py:func:`__do__restore` function. - The purpose of this function is to catch any exceptions that occur. - - :param database: database instance to restore - :type database: DbDir - """ - try: - __do_restore(database) - except (OSError, IOError) as msg: - raise DbException(str(msg)) - -def __do_restore(database): - """ - Loop through each table of the database, restoring the pickled data - to the appropriate database file. - - :param database: database instance to backup - :type database: DbDir - """ - for (base, tbl) in __build_tbl_map(database): - backup_name = __mk_backup_name(database, base) - backup_table = open(backup_name, 'rb') - __load_tbl_txn(database, backup_table, tbl) - - database.rebuild_secondary() - -def __load_tbl_txn(database, backup_table, tbl): - """ - Return the temporary backup name of the database table - - :param database: database instance - :type database: DbDir - :param backup_table: file containing the backup data - :type backup_table: file - :param tbl: Berkeley db database table - :type tbl: Berkeley db database table - """ - try: - while True: - data = pickle.load(backup_table) - txn = database.env.txn_begin() - tbl.put(data[0], data[1], txn=txn) - txn.commit() - except EOFError: - backup_table.close() - -def __build_tbl_map(database): - """ - Builds a table map of names to database tables. - - :param database: database instance to backup - :type database: DbDir - """ - return [ - ( PERSON_TBL, database.person_map.db), - ( FAMILY_TBL, database.family_map.db), - ( PLACES_TBL, database.place_map.db), - ( SOURCES_TBL, database.source_map.db), - ( CITATIONS_TBL, database.citation_map.db), - ( REPO_TBL, database.repository_map.db), - ( NOTE_TBL, database.note_map.db), - ( MEDIA_TBL, database.media_map.db), - ( EVENTS_TBL, database.event_map.db), - ( TAG_TBL, database.tag_map.db), - ( META, database.metadata.db), - ] diff --git a/gramps/gen/db/dbconst.py b/gramps/gen/db/dbconst.py index ba3514afb..21a17db80 100644 --- a/gramps/gen/db/dbconst.py +++ b/gramps/gen/db/dbconst.py @@ -28,26 +28,23 @@ Declare constants used by database modules # constants # #------------------------------------------------------------------------- -__all__ = ( - ('DBPAGE', 'DBMODE', 'DBCACHE', 'DBLOCKS', 'DBOBJECTS', 'DBUNDO', - 'DBEXT', 'DBMODE_R', 'DBMODE_W', 'DBUNDOFN', 'DBLOCKFN', - 'DBRECOVFN','BDBVERSFN', 'DBLOGNAME', 'DBFLAGS_O', 'DBFLAGS_R', - 'DBFLAGS_D', 'SCHVERSFN', 'PCKVERSFN' - ) + - - ('PERSON_KEY', 'FAMILY_KEY', 'SOURCE_KEY', 'CITATION_KEY', - 'EVENT_KEY', 'MEDIA_KEY', 'PLACE_KEY', 'REPOSITORY_KEY', - 'NOTE_KEY', 'REFERENCE_KEY', 'TAG_KEY' - ) + - - ('TXNADD', 'TXNUPD', 'TXNDEL') - ) +__all__ = ( 'DBPAGE', 'DBMODE', 'DBCACHE', 'DBLOCKS', 'DBOBJECTS', 'DBUNDO', + 'DBEXT', 'DBMODE_R', 'DBMODE_W', 'DBUNDOFN', 'DBLOCKFN', + 'DBRECOVFN','BDBVERSFN', 'DBLOGNAME', 'SCHVERSFN', 'PCKVERSFN', + 'DBBACKEND', + 'PERSON_KEY', 'FAMILY_KEY', 'SOURCE_KEY', 'CITATION_KEY', + 'EVENT_KEY', 'MEDIA_KEY', 'PLACE_KEY', 'REPOSITORY_KEY', + 'NOTE_KEY', 'REFERENCE_KEY', 'TAG_KEY', + 'TXNADD', 'TXNUPD', 'TXNDEL', + "CLASS_TO_KEY_MAP", "KEY_TO_CLASS_MAP", "KEY_TO_NAME_MAP" + ) DBEXT = ".db" # File extension to be used for database files DBUNDOFN = "undo.db" # File name of 'undo' database DBLOCKFN = "lock" # File name of lock file DBRECOVFN = "need_recover" # File name of recovery file BDBVERSFN = "bdbversion.txt"# File name of Berkeley DB version file +DBBACKEND = "database.txt" # File name of Database backend file SCHVERSFN = "schemaversion.txt"# File name of schema version file PCKVERSFN = "pickleupgrade.txt" # Indicator that pickle has been upgrade t Python3 DBLOGNAME = ".Db" # Name of logger @@ -60,18 +57,6 @@ DBLOCKS = 100000 # Maximum number of locks supported DBOBJECTS = 100000 # Maximum number of simultaneously locked objects DBUNDO = 1000 # Maximum size of undo buffer -try: - from bsddb3.db import DB_CREATE, DB_AUTO_COMMIT, DB_DUP, DB_DUPSORT, DB_RDONLY - DBFLAGS_O = DB_CREATE | DB_AUTO_COMMIT # Default flags for database open - DBFLAGS_R = DB_RDONLY # Flags to open a database read-only - DBFLAGS_D = DB_DUP | DB_DUPSORT # Default flags for duplicate keys -except: - print("WARNING: no bsddb support") - # FIXME: make this more abstract to deal with other backends, or do not import - DBFLAGS_O = DB_CREATE = DB_AUTO_COMMIT = 0 - DBFLAGS_R = DB_RDONLY = 0 - DBFLAGS_D = DB_DUP = DB_DUPSORT = 0 - PERSON_KEY = 0 FAMILY_KEY = 1 SOURCE_KEY = 2 @@ -85,3 +70,37 @@ TAG_KEY = 9 CITATION_KEY = 10 TXNADD, TXNUPD, TXNDEL = 0, 1, 2 + +CLASS_TO_KEY_MAP = {"Person": PERSON_KEY, + "Family": FAMILY_KEY, + "Source": SOURCE_KEY, + "Citation": CITATION_KEY, + "Event": EVENT_KEY, + "MediaObject": MEDIA_KEY, + "Place": PLACE_KEY, + "Repository": REPOSITORY_KEY, + "Note" : NOTE_KEY, + "Tag": TAG_KEY} + +KEY_TO_CLASS_MAP = {PERSON_KEY: "Person", + FAMILY_KEY: "Family", + SOURCE_KEY: "Source", + CITATION_KEY: "Citation", + EVENT_KEY: "Event", + MEDIA_KEY: "MediaObject", + PLACE_KEY: "Place", + REPOSITORY_KEY: "Repository", + NOTE_KEY: "Note", + TAG_KEY: "Tag"} + +KEY_TO_NAME_MAP = {PERSON_KEY: 'person', + FAMILY_KEY: 'family', + EVENT_KEY: 'event', + SOURCE_KEY: 'source', + CITATION_KEY: 'citation', + PLACE_KEY: 'place', + MEDIA_KEY: 'media', + REPOSITORY_KEY: 'repository', + #REFERENCE_KEY: 'reference', + NOTE_KEY: 'note', + TAG_KEY: 'tag'} diff --git a/gramps/gen/db/undoredo.py b/gramps/gen/db/undoredo.py index 7fed4d876..bfcd652d6 100644 --- a/gramps/gen/db/undoredo.py +++ b/gramps/gen/db/undoredo.py @@ -1,77 +1,12 @@ -# -# Gramps - a GTK+/GNOME based genealogy program -# -# Copyright (C) 2004-2006 Donald N. Allingham -# Copyright (C) 2011 Tim G L Lyons -# -# 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. -# - -""" -Exports the DbUndo class for managing Gramps transactions -undos and redos. -""" - #------------------------------------------------------------------------- # # Standard python modules # #------------------------------------------------------------------------- -import time, os +import time import pickle from collections import deque -try: - from bsddb3 import db -except: - # FIXME: make this more abstract to deal with other backends - class db: - DBRunRecoveryError = 0 - DBAccessError = 0 - DBPageNotFoundError = 0 - DBInvalidArgError = 0 - -from ..const import GRAMPS_LOCALE as glocale -_ = glocale.translation.gettext - -#------------------------------------------------------------------------- -# -# Gramps modules -# -#------------------------------------------------------------------------- -from ..constfunc import conv_to_unicode, handle2internal, win -from .dbconst import * -from . import BSDDBTxn -from ..errors import DbError - -#------------------------------------------------------------------------- -# -# Local Constants -# -#------------------------------------------------------------------------- -DBERRS = (db.DBRunRecoveryError, db.DBAccessError, - db.DBPageNotFoundError, db.DBInvalidArgError) - -_SIGBASE = ('person', 'family', 'source', 'event', 'media', - 'place', 'repository', 'reference', 'note', 'tag', 'citation') - -#------------------------------------------------------------------------- -# -# DbUndo class -# -#------------------------------------------------------------------------- class DbUndo(object): """ Base class for the Gramps undo/redo manager. Needs to be subclassed @@ -100,7 +35,6 @@ class DbUndo(object): self.db.media_map, self.db.place_map, self.db.repository_map, - self.db.reference_map, self.db.note_map, self.db.tag_map, self.db.citation_map, @@ -171,6 +105,16 @@ class DbUndo(object): """ raise NotImplementedError + def __redo(self, update_history): + """ + """ + raise NotImplementedError + + def __undo(self, update_history): + """ + """ + raise NotImplementedError + def commit(self, txn, msg): """ Commit the transaction to the undo/redo database. "txn" should be @@ -196,105 +140,6 @@ class DbUndo(object): return False return self.__redo(update_history) - def undoredo(func): - """ - Decorator function to wrap undo and redo operations within a bsddb - transaction. It also catches bsddb errors and raises an exception - as appropriate - """ - def try_(self, *args, **kwargs): - try: - with BSDDBTxn(self.db.env) as txn: - self.txn = self.db.txn = txn.txn - status = func(self, *args, **kwargs) - if not status: - txn.abort() - self.db.txn = None - return status - - except DBERRS as msg: - self.db._log_error() - raise DbError(msg) - - return try_ - - @undoredo - def __undo(self, update_history=True): - """ - Access the last committed transaction, and revert the data to the - state before the transaction was committed. - """ - txn = self.undoq.pop() - self.redoq.append(txn) - transaction = txn - db = self.db - subitems = transaction.get_recnos(reverse=True) - - # Process all records in the transaction - for record_id in subitems: - (key, trans_type, handle, old_data, new_data) = \ - pickle.loads(self.undodb[record_id]) - - if key == REFERENCE_KEY: - self.undo_reference(old_data, handle, self.mapbase[key]) - else: - self.undo_data(old_data, handle, self.mapbase[key], - db.emit, _SIGBASE[key]) - # Notify listeners - if db.undo_callback: - if self.undo_count > 0: - db.undo_callback(_("_Undo %s") - % self.undoq[-1].get_description()) - else: - db.undo_callback(None) - - if db.redo_callback: - db.redo_callback(_("_Redo %s") - % transaction.get_description()) - - if update_history and db.undo_history_callback: - db.undo_history_callback() - return True - - @undoredo - def __redo(self, db=None, update_history=True): - """ - Access the last undone transaction, and revert the data to the state - before the transaction was undone. - """ - txn = self.redoq.pop() - self.undoq.append(txn) - transaction = txn - db = self.db - subitems = transaction.get_recnos() - - # Process all records in the transaction - for record_id in subitems: - (key, trans_type, handle, old_data, new_data) = \ - pickle.loads(self.undodb[record_id]) - - if key == REFERENCE_KEY: - self.undo_reference(new_data, handle, self.mapbase[key]) - else: - self.undo_data(new_data, handle, self.mapbase[key], - db.emit, _SIGBASE[key]) - # Notify listeners - if db.undo_callback: - db.undo_callback(_("_Undo %s") - % transaction.get_description()) - - if db.redo_callback: - if self.redo_count > 1: - new_transaction = self.redoq[-2] - db.redo_callback(_("_Redo %s") - % new_transaction.get_description()) - else: - db.redo_callback(None) - - if update_history and db.undo_history_callback: - db.undo_history_callback() - return True - def undo_reference(self, data, handle, db_map): """ Helper method to undo a reference map entry @@ -332,185 +177,3 @@ class DbUndo(object): undo_count = property(lambda self:len(self.undoq)) redo_count = property(lambda self:len(self.redoq)) - -class DbUndoList(DbUndo): - """ - Implementation of the Gramps undo database using a Python list - """ - def __init__(self, grampsdb): - """ - Class constructor - """ - super(DbUndoList, self).__init__(grampsdb) - self.undodb = [] - - def open(self): - """ - A list does not need to be opened - """ - pass - - def close(self): - """ - Close the list by resetting it to empty - """ - self.undodb = [] - self.clear() - - def append(self, value): - """ - Add an entry on the end of the list - """ - self.undodb.append(value) - return len(self.undodb)-1 - - def __getitem__(self, index): - """ - Return an item at the specified index - """ - return self.undodb[index] - - def __setitem__(self, index, value): - """ - Set an item at the speficied index to the given value - """ - self.undodb[index] = value - - def __iter__(self): - """ - Iterator - """ - for item in self.undodb: - yield item - - def __len__(self): - """ - Return number of entries in the list - """ - return len(self.undodb) - -class DbUndoBSDDB(DbUndo): - """ - Class constructor for Gramps undo/redo database using a bsddb recno - database as the backing store. - """ - - def __init__(self, grampsdb, path): - """ - Class constructor - """ - super(DbUndoBSDDB, self).__init__(grampsdb) - self.undodb = db.DB() - self.path = path - - def open(self): - """ - Open the undo/redo database - """ - path = self.path - self.undodb.open(path, db.DB_RECNO, db.DB_CREATE) - - def close(self): - """ - Close the undo/redo database - """ - self.undodb.close() - self.undodb = None - self.mapbase = None - self.db = None - - try: - os.remove(self.path) - except OSError: - pass - self.clear() - - def append(self, value): - """ - Add an entry on the end of the database - """ - return self.undodb.append(value) - - def __len__(self): - """ - Returns the number of entries in the database - """ - x = self.undodb.stat()['nkeys'] - y = len(self.undodb) - assert x == y - return x - - def __getitem__(self, index): - """ - Returns the entry stored at the specified index - """ - return self.undodb.get(index) - - def __setitem__(self, index, value): - """ - Sets the entry stored at the specified index to the value given. - """ - self.undodb.put(index, value) - - def __iter__(self): - """ - Iterator - """ - cursor = self.undodb.cursor() - data = cursor.first() - while data: - yield data - data = next(cursor) - -def testundo(): - class T: - def __init__(self): - self.msg = '' - self.timetstamp = 0 - def set_description(self, msg): - self.msg = msg - - class D: - def __init__(self): - self.person_map = {} - self.family_map = {} - self.source_map = {} - self.event_map = {} - self.media_map = {} - self.place_map = {} - self.note_map = {} - self.tag_map = {} - self.repository_map = {} - self.reference_map = {} - - print("list tests") - undo = DbUndoList(D()) - print(undo.append('foo')) - print(undo.append('bar')) - print(undo[0]) - undo[0] = 'foobar' - print(undo[0]) - print("len", len(undo)) - print("iter") - for data in undo: - print(data) - print() - print("bsddb tests") - undo = DbUndoBSDDB(D(), '/tmp/testundo') - undo.open() - print(undo.append('foo')) - print(undo.append('fo2')) - print(undo.append('fo3')) - print(undo[1]) - undo[1] = 'bar' - print(undo[1]) - for data in undo: - print(data) - print("len", len(undo)) - - print("test commit") - undo.commit(T(), msg="test commit") - undo.close() - -if __name__ == '__main__': - testundo() diff --git a/gramps/gen/dbstate.py b/gramps/gen/dbstate.py index 839d05cd2..b232d612e 100644 --- a/gramps/gen/dbstate.py +++ b/gramps/gen/dbstate.py @@ -22,13 +22,23 @@ """ Provide the database state class """ +import sys +import os +import io -from .db import DbBsddbRead from .db import DbReadBase from .proxy.proxybase import ProxyDbBase from .utils.callback import Callback from .config import config +#------------------------------------------------------------------------- +# +# set up logging +# +#------------------------------------------------------------------------- +import logging +LOG = logging.getLogger(".dbstate") + class DbState(Callback): """ Provide a class to encapsulate the state of the database. @@ -45,7 +55,7 @@ class DbState(Callback): just a place holder until a real DB is assigned. """ Callback.__init__(self) - self.db = DbBsddbRead() + self.db = self.make_database("bsddb") self.open = False self.stack = [] @@ -54,9 +64,10 @@ class DbState(Callback): Closes the existing db, and opens a new one. Retained for backward compatibility. """ - self.emit('no-database', ()) - self.db.close() - self.change_database_noclose(database) + if database: + self.emit('no-database', ()) + self.db.close() + self.change_database_noclose(database) def change_database_noclose(self, database): """ @@ -88,7 +99,7 @@ class DbState(Callback): """ self.emit('no-database', ()) self.db.close() - self.db = DbBsddbRead() + self.db = self.make_database("bsddb") self.db.db_is_open = False self.open = False self.emit('database-changed', (self.db, )) @@ -122,3 +133,100 @@ class DbState(Callback): """ self.db = self.stack.pop() self.emit('database-changed', (self.db, )) + + def make_database(self, id): + """ + Make a database, given a plugin id. + """ + from .plug import BasePluginManager + from .const import PLUGINS_DIR, USER_PLUGINS + + pmgr = BasePluginManager.get_instance() + pdata = pmgr.get_plugin(id) + + if not pdata: + # This might happen if using gramps from outside, and + # we haven't loaded plugins yet + pmgr.reg_plugins(PLUGINS_DIR, self, None) + pmgr.reg_plugins(USER_PLUGINS, self, None, load_on_reg=True) + pdata = pmgr.get_plugin(id) + + if pdata: + if pdata.reset_system: + if self.modules_is_set(): + self.reset_modules() + else: + self.save_modules() + mod = pmgr.load_plugin(pdata) + database = getattr(mod, pdata.databaseclass) + return database() + + def open_database(self, dbname, force_unlock=False, callback=None): + """ + Open a database by name and return the database. + """ + data = self.lookup_family_tree(dbname) + database = None + if data: + dbpath, locked, locked_by, backend = data + if (not locked) or (locked and force_unlock): + database = self.make_database(backend) + database.load(dbpath, callback=callback) + return database + + def lookup_family_tree(self, dbname): + """ + Find a Family Tree given its name, and return properties. + """ + dbdir = os.path.expanduser(config.get('behavior.database-path')) + for dpath in os.listdir(dbdir): + dirpath = os.path.join(dbdir, dpath) + path_name = os.path.join(dirpath, "name.txt") + if os.path.isfile(path_name): + file = io.open(path_name, 'r', encoding='utf8') + name = file.readline().strip() + file.close() + if dbname == name: + locked = False + locked_by = None + backend = None + fname = os.path.join(dirpath, "database.txt") + if os.path.isfile(fname): + ifile = io.open(fname, 'r', encoding='utf8') + backend = ifile.read().strip() + ifile.close() + else: + backend = "bsddb" + try: + fname = os.path.join(dirpath, "lock") + ifile = io.open(fname, 'r', encoding='utf8') + locked_by = ifile.read().strip() + locked = True + ifile.close() + except (OSError, IOError): + pass + return (dirpath, locked, locked_by, backend) + return None + + ## Work-around for databases that need sys refresh (django): + def modules_is_set(self): + LOG.info("modules_is_set?") + if hasattr(self, "_modules"): + return self._modules != None + else: + self._modules = None + return False + + def reset_modules(self): + LOG.info("reset_modules!") + # First, clear out old modules: + for key in list(sys.modules.keys()): + del(sys.modules[key]) + # Next, restore previous: + for key in self._modules: + sys.modules[key] = self._modules[key] + + def save_modules(self): + LOG.info("save_modules!") + self._modules = sys.modules.copy() + diff --git a/gramps/gen/merge/diff.py b/gramps/gen/merge/diff.py index 1ba5a45f7..0993638c3 100644 --- a/gramps/gen/merge/diff.py +++ b/gramps/gen/merge/diff.py @@ -28,7 +28,7 @@ from gramps.cli.user import User from ..dbstate import DbState from gramps.cli.grampscli import CLIManager from ..plug import BasePluginManager -from ..db.dictionary import DictionaryDb +from gramps.plugins.database.dictionarydb import DictionaryDb from gramps.gen.lib.handle import HandleClass, Handle from gramps.gen.lib import * from gramps.gen.lib.personref import PersonRef diff --git a/gramps/gen/plug/_gramplet.py b/gramps/gen/plug/_gramplet.py index c040bae7d..0159a8d03 100644 --- a/gramps/gen/plug/_gramplet.py +++ b/gramps/gen/plug/_gramplet.py @@ -70,7 +70,6 @@ class Gramplet(object): self.connect(self.gui.textview, "motion-notify-event", self.gui.on_motion) self.connect_signal('Person', self._active_changed) - self._db_changed(self.dbstate.db) active_person = self.get_active('Person') if active_person: # already changed @@ -321,8 +320,6 @@ class Gramplet(object): self._idle_id = 0 LOG.debug("gramplet updater: %s : One time, done!" % self.gui.title) return False - # FIXME: find out why Data Entry has this error, or just ignore it - import bsddb3 as bsddb try: retval = next(self._generator) if not retval: @@ -333,10 +330,6 @@ class Gramplet(object): LOG.debug("gramplet updater: %s: return %s" % (self.gui.title, retval)) return retval - except bsddb.db.DBCursorClosedError: - # not sure why---caused by Data Entry Gramplet - LOG.warn("bsddb.db.DBCursorClosedError in: %s" % self.gui.title) - return False except StopIteration: self._idle_id = 0 self._generator.close() diff --git a/gramps/gen/plug/_manager.py b/gramps/gen/plug/_manager.py index e047f6166..e30a089ae 100644 --- a/gramps/gen/plug/_manager.py +++ b/gramps/gen/plug/_manager.py @@ -412,6 +412,11 @@ class BasePluginManager(object): """ return self.__pgr.sidebar_plugins() + def get_reg_databases(self): + """ Return list of registered database backends + """ + return self.__pgr.database_plugins() + def get_external_opt_dict(self): """ Return the dictionary of external options. """ return self.__external_opt_dict diff --git a/gramps/gen/plug/_pluginreg.py b/gramps/gen/plug/_pluginreg.py index 04167cafc..70a93f0f2 100644 --- a/gramps/gen/plug/_pluginreg.py +++ b/gramps/gen/plug/_pluginreg.py @@ -70,8 +70,9 @@ VIEW = 8 RELCALC = 9 GRAMPLET = 10 SIDEBAR = 11 +DATABASE = 12 PTYPE = [REPORT , QUICKREPORT, TOOL, IMPORT, EXPORT, DOCGEN, GENERAL, - MAPSERVICE, VIEW, RELCALC, GRAMPLET, SIDEBAR] + MAPSERVICE, VIEW, RELCALC, GRAMPLET, SIDEBAR, DATABASE] PTYPE_STR = { REPORT: _('Report') , QUICKREPORT: _('Quickreport'), @@ -85,6 +86,7 @@ PTYPE_STR = { RELCALC: _('Relationships'), GRAMPLET: _('Gramplet'), SIDEBAR: _('Sidebar'), + DATABASE: _('Database'), } #possible report categories @@ -206,7 +208,7 @@ class PluginData(object): The python path where the plugin implementation can be found .. attribute:: ptype The plugin type. One of REPORT , QUICKREPORT, TOOL, IMPORT, - EXPORT, DOCGEN, GENERAL, MAPSERVICE, VIEW, GRAMPLET + EXPORT, DOCGEN, GENERAL, MAPSERVICE, VIEW, GRAMPLET, DATABASE .. attribute:: authors List of authors of the plugin, default=[] .. attribute:: authors_email @@ -349,6 +351,14 @@ class PluginData(object): the plugin is appended to the list of plugins. If START, then the plugin is prepended. Only set START if you want a plugin to be the first in the order of plugins + + Attributes for DATABASE plugins + + .. attribute:: databaseclass + The class in the module that is the database class + .. attribute:: reset_system + Boolean to indicate that the system (sys.modules) should + be reset. """ def __init__(self): @@ -421,6 +431,9 @@ class PluginData(object): self._menu_label = '' #VIEW and SIDEBAR attr self._order = END + #DATABASE attr + self._databaseclass = None + self._reset_system = False #GENERAL attr self._data = [] self._process = None @@ -931,6 +944,26 @@ class PluginData(object): order = property(_get_order, _set_order) + #DATABASE attributes + def _set_databaseclass(self, databaseclass): + if not self._ptype == DATABASE: + raise ValueError('databaseclass may only be set for DATABASE plugins') + self._databaseclass = databaseclass + + def _get_databaseclass(self): + return self._databaseclass + + def _set_reset_system(self, reset_system): + if not self._ptype == DATABASE: + raise ValueError('reset_system may only be set for DATABASE plugins') + self._reset_system = reset_system + + def _get_reset_system(self): + return self._reset_system + + databaseclass = property(_get_databaseclass, _set_databaseclass) + reset_system = property(_get_reset_system, _set_reset_system) + #GENERAL attr def _set_data(self, data): if not self._ptype in (GENERAL,): @@ -1032,6 +1065,7 @@ def make_environment(**kwargs): 'REPORT_MODE_CLI': REPORT_MODE_CLI, 'TOOL_MODE_GUI': TOOL_MODE_GUI, 'TOOL_MODE_CLI': TOOL_MODE_CLI, + 'DATABASE': DATABASE, 'GRAMPSVERSION': GRAMPSVERSION, 'START': START, 'END': END, @@ -1297,6 +1331,12 @@ class PluginRegister(object): """ return self.type_plugins(SIDEBAR) + def database_plugins(self): + """ + Return a list of :class:`PluginData` that are of type DATABASE + """ + return self.type_plugins(DATABASE) + def filter_load_on_reg(self): """ Return a list of :class:`PluginData` that have load_on_reg == True diff --git a/gramps/gen/utils/callman.py b/gramps/gen/utils/callman.py index ca6a94d86..939a0f6fa 100644 --- a/gramps/gen/utils/callman.py +++ b/gramps/gen/utils/callman.py @@ -303,7 +303,8 @@ class CallbackManager(object): Do a custom db connect signal outside of the primary object ones managed automatically. """ - self.custom_signal_keys.append(self.database.connect(name, callback)) + if self.database: + self.custom_signal_keys.append(self.database.connect(name, callback)) def __callbackcreator(self, signal, noarg=False): """ diff --git a/gramps/grampsapp.py b/gramps/grampsapp.py index b9e4a6015..e2d7215e7 100644 --- a/gramps/grampsapp.py +++ b/gramps/grampsapp.py @@ -136,14 +136,6 @@ if not sys.version_info >= MIN_PYTHON_VERSION : 'v3': MIN_PYTHON_VERSION[2]}) sys.exit(1) -try: - import bsddb3 -except ImportError: - logging.warning(_("\nYou don't have the python3 bsddb3 package installed." - " This package is needed to start Gramps.\n\n" - "Gramps will terminate now.")) - sys.exit(1) - #------------------------------------------------------------------------- # # Gramps libraries diff --git a/gramps/gui/aboutdialog.py b/gramps/gui/aboutdialog.py index 9bd977d55..bf0fc7053 100644 --- a/gramps/gui/aboutdialog.py +++ b/gramps/gui/aboutdialog.py @@ -28,7 +28,6 @@ import os import sys import io -import bsddb3 as bsddb ##import logging ##_LOG = logging.getLogger(".GrampsAboutDialog") @@ -60,6 +59,20 @@ _ = glocale.translation.gettext from gramps.gen.constfunc import get_env_var from .display import display_url +def ellipses(text): + """ + Ellipsize text on length 40 + """ + if len(text) > 40: + return text[:40] + "..." + return text + +try: + import bsddb3 as bsddb ## ok, in try/except + BSDDB_STR = ellipses(str(bsddb.__version__) + " " + str(bsddb.db.version())) +except: + BSDDB_STR = 'not found' + #------------------------------------------------------------------------- # # GrampsAboutDialog @@ -125,19 +138,11 @@ class GrampsAboutDialog(Gtk.AboutDialog): "Distribution: %s") % (ellipses(str(VERSION)), ellipses(str(sys.version).replace('\n','')), - ellipses(str(bsddb.__version__) + " " + str(bsddb.db.version())), + BSDDB_STR, ellipses(get_env_var('LANG','')), ellipses(operatingsystem), ellipses(distribution))) -def ellipses(text): - """ - Ellipsize text on length 40 - """ - if len(text) > 40: - return text[:40] + "..." - return text - #------------------------------------------------------------------------- # # AuthorParser diff --git a/gramps/gui/dbloader.py b/gramps/gui/dbloader.py index 6e7f85d5e..d95c6bb9a 100644 --- a/gramps/gui/dbloader.py +++ b/gramps/gui/dbloader.py @@ -52,10 +52,10 @@ from gi.repository import GObject # #------------------------------------------------------------------------- from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gen.db.dbconst import DBBACKEND _ = glocale.translation.gettext from gramps.cli.grampscli import CLIDbLoader from gramps.gen.config import config -from gramps.gen.db import DbBsddb from gramps.gen.db.exceptions import (DbUpgradeRequiredError, BsddbDowngradeError, DbVersionError, @@ -305,7 +305,14 @@ class DbLoader(CLIDbLoader): else: mode = 'w' - db = DbBsddb() + dbid_path = os.path.join(filename, DBBACKEND) + if os.path.isfile(dbid_path): + with open(dbid_path) as fp: + dbid = fp.read().strip() + else: + dbid = "bsddb" + + db = self.dbstate.make_database(dbid) db.disable_signals() self.dbstate.no_database() diff --git a/gramps/gui/dbman.py b/gramps/gui/dbman.py index 223aa8b14..eda65fe06 100644 --- a/gramps/gui/dbman.py +++ b/gramps/gui/dbman.py @@ -69,20 +69,18 @@ from gi.repository import Pango # #------------------------------------------------------------------------- from gramps.gen.const import GRAMPS_LOCALE as glocale +from gramps.gen.plug import BasePluginManager _ = glocale.translation.gettext from gramps.gen.const import URL_WIKISTRING from .user import User -from .dialog import ErrorDialog, QuestionDialog, QuestionDialog2 -from gramps.gen.db import DbBsddb +from .dialog import ErrorDialog, QuestionDialog, QuestionDialog2, ICON from .pluginmanager import GuiPluginManager from gramps.cli.clidbman import CLIDbManager, NAME_FILE, time_val from .ddtargets import DdTargets from gramps.gen.recentfiles import rename_filename, remove_filename from .glade import Glade -from gramps.gen.db.backup import restore from gramps.gen.db.exceptions import DbException - _RETURN = Gdk.keyval_from_name("Return") _KP_ENTER = Gdk.keyval_from_name("KP_Enter") @@ -106,6 +104,25 @@ ICON_COL = 6 RCS_BUTTON = { True : _('_Extract'), False : _('_Archive') } +class DatabaseDialog(Gtk.MessageDialog): + def __init__(self, parent, options): + """ + options = [(pdata, number), ...] + """ + Gtk.MessageDialog.__init__(self, + parent, + flags=Gtk.DialogFlags.MODAL, + type=Gtk.MessageType.QUESTION, + ) + self.set_icon(ICON) + self.set_title('') + self.set_markup('<span size="larger" weight="bold">%s</span>' % + _('Database Backend for New Tree')) + self.format_secondary_text( + _("Please select a database backend type:")) + for option, number in options: + self.add_button(option.name, number) + class DbManager(CLIDbManager): """ Database Manager. Opens a database manager window that allows users to @@ -531,8 +548,8 @@ class DbManager(CLIDbManager): new_path, newname = self._create_new_db("%s : %s" % (parent_name, name)) self.__start_cursor(_("Extracting archive...")) - dbclass = DbBsddb - dbase = dbclass() + + dbase = self.dbstate.make_database("bsddb") dbase.load(new_path, None) self.__start_cursor(_("Importing archive...")) @@ -719,18 +736,17 @@ class DbManager(CLIDbManager): fname = os.path.join(dirname, filename) os.unlink(fname) - newdb = DbBsddb() + newdb = self.dbstate.make_database("bsddb") newdb.write_version(dirname) - dbclass = DbBsddb - dbase = dbclass() + dbase = self.dbstate.make_database("bsddb") dbase.set_save_path(dirname) dbase.load(dirname, None) self.__start_cursor(_("Rebuilding database from backup files")) try: - restore(dbase) + dbase.restore() except DbException as msg: DbManager.ERROR(_("Error restoring backup data"), msg) @@ -764,19 +780,37 @@ class DbManager(CLIDbManager): message. """ self.new.set_sensitive(False) - try: - self._create_new_db() - except (OSError, IOError) as msg: - DbManager.ERROR(_("Could not create Family Tree"), - str(msg)) + dbid = None + pmgr = BasePluginManager.get_instance() + pdata = pmgr.get_reg_databases() + # If just one database backend, just use it: + if len(pdata) == 0: + DbManager.ERROR(_("No available database backends"), + _("Please check your dependencies.")) + elif len(pdata) == 1: + dbid = pdata[0].id + elif len(pdata) > 1: + options = sorted(list(zip(pdata, range(1, len(pdata) + 1))), key=lambda items: items[0].name) + d = DatabaseDialog(self.top, options) + number = d.run() + d.destroy() + if number >= 0: + dbid = [option[0].id for option in options if option[1] == number][0] + ### Now, let's load it up + if dbid: + try: + self._create_new_db(dbid=dbid) + except (OSError, IOError) as msg: + DbManager.ERROR(_("Could not create Family Tree"), + str(msg)) self.new.set_sensitive(True) - def _create_new_db(self, title=None, create_db=True): + def _create_new_db(self, title=None, create_db=True, dbid=None): """ Create a new database, append to model """ new_path, title = self.create_new_db_cli(conv_to_unicode(title, 'utf8'), - create_db) + create_db, dbid) path_name = os.path.join(new_path, NAME_FILE) (tval, last) = time_val(new_path) node = self.model.append(None, [title, new_path, path_name, diff --git a/gramps/gui/editors/editfamily.py b/gramps/gui/editors/editfamily.py index 707bf0853..2c5110b1b 100644 --- a/gramps/gui/editors/editfamily.py +++ b/gramps/gui/editors/editfamily.py @@ -26,7 +26,6 @@ # python modules # #------------------------------------------------------------------------- -from bsddb3 import db as bsddb_db import pickle #------------------------------------------------------------------------- @@ -1026,10 +1025,11 @@ class EditFamily(EditPrimary): ) def save(self, *obj): - try: - self.__do_save() - except bsddb_db.DBRunRecoveryError as msg: - RunDatabaseRepair(msg[1]) + ## FIXME: how to catch a specific error? + #try: + self.__do_save() + #except bsddb_db.DBRunRecoveryError as msg: + # RunDatabaseRepair(msg[1]) def __do_save(self): self.ok_button.set_sensitive(False) diff --git a/gramps/gui/logger/_errorreportassistant.py b/gramps/gui/logger/_errorreportassistant.py index e5c81597a..14c6ba1a2 100644 --- a/gramps/gui/logger/_errorreportassistant.py +++ b/gramps/gui/logger/_errorreportassistant.py @@ -30,7 +30,12 @@ from gi.repository import GdkPixbuf from gi.repository import GObject import cairo import sys, os -import bsddb3 as bsddb + +try: + import bsddb3 as bsddb # ok, in try/except + BSDDB_STR = str(bsddb.__version__) + " " + str(bsddb.db.version()) +except: + BSDDB_STR = 'not found' #------------------------------------------------------------------------- # @@ -166,7 +171,7 @@ class ErrorReportAssistant(Gtk.Assistant): "gobject version: %s\n"\ "cairo version : %s"\ % (str(sys.version).replace('\n',''), - str(bsddb.__version__) + " " + str(bsddb.db.version()), + BSDDB_STR, str(VERSION), get_env_var('LANG',''), operatingsystem, diff --git a/gramps/gui/pluginmanager.py b/gramps/gui/pluginmanager.py index edde82dac..bb609e984 100644 --- a/gramps/gui/pluginmanager.py +++ b/gramps/gui/pluginmanager.py @@ -205,7 +205,12 @@ class GuiPluginManager(Callback): return [plg for plg in self.basemgr.get_reg_docgens() if plg.id not in self.__hidden_plugins] + def get_reg_databases(self): + """ Return list of non hidden registered database backends + """ + return [plg for plg in self.basemgr.get_reg_databases() + if plg.id not in self.__hidden_plugins] + def get_reg_general(self, category=None): return [plg for plg in self.basemgr.get_reg_general(category) if plg.id not in self.__hidden_plugins] - diff --git a/gramps/gui/viewmanager.py b/gramps/gui/viewmanager.py index 721706faf..11e37a1c9 100644 --- a/gramps/gui/viewmanager.py +++ b/gramps/gui/viewmanager.py @@ -87,7 +87,6 @@ from gramps.gen.utils.file import media_path_full from .dbloader import DbLoader from .display import display_help, display_url from .configure import GrampsPreferences -from gramps.gen.db.backup import backup from gramps.gen.db.exceptions import DbException from .aboutdialog import GrampsAboutDialog from .navigator import Navigator @@ -762,7 +761,7 @@ class ViewManager(CLIManager): self.uistate.progress.show() self.uistate.push_message(self.dbstate, _("Autobackup...")) try: - backup(self.dbstate.db) + self.dbstate.db.backup() except DbException as msg: ErrorDialog(_("Error saving backup data"), msg) self.uistate.set_busy_cursor(False) @@ -1594,6 +1593,11 @@ def run_plugin(pdata, dbstate, uistate): mod = pmgr.load_plugin(pdata) if not mod: #import of plugin failed + failed = pmgr.get_fail_list() + if failed: + error_msg = failed[-1][1][1] + else: + error_msg = "(no error message)" ErrorDialog( _('Failed Loading Plugin'), _('The plugin %(name)s did not load and reported an error.\n\n' @@ -1608,7 +1612,7 @@ def run_plugin(pdata, dbstate, uistate): 'gramps_bugtracker_url' : URL_BUGHOME, 'firstauthoremail': pdata.authors_email[0] if pdata.authors_email else '...', - 'error_msg': pmgr.get_fail_list()[-1][1][1]}) + 'error_msg': error_msg}) return if pdata.ptype == REPORT: diff --git a/gramps/gen/db/test/__init__.py b/gramps/plugins/database/__init__.py similarity index 100% rename from gramps/gen/db/test/__init__.py rename to gramps/plugins/database/__init__.py diff --git a/gramps/plugins/database/bsddb.gpr.py b/gramps/plugins/database/bsddb.gpr.py new file mode 100644 index 000000000..82a23573c --- /dev/null +++ b/gramps/plugins/database/bsddb.gpr.py @@ -0,0 +1,31 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2015 Douglas 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. +# + +plg = newplugin() +plg.id = 'bsddb' +plg.name = _("BSDDB Database Backend") +plg.name_accell = _("_BSDDB Database Backend") +plg.description = _("Berkeley Software Distribution Database Backend") +plg.version = '1.0' +plg.gramps_target_version = "5.0" +plg.status = STABLE +plg.fname = 'bsddb.py' +plg.ptype = DATABASE +plg.databaseclass = 'DbBsddb' diff --git a/gramps/plugins/database/bsddb.py b/gramps/plugins/database/bsddb.py new file mode 100644 index 000000000..9cbca25ce --- /dev/null +++ b/gramps/plugins/database/bsddb.py @@ -0,0 +1,3 @@ + +from bsddb_support import DbBsddb + diff --git a/gramps/plugins/database/bsddb_support/__init__.py b/gramps/plugins/database/bsddb_support/__init__.py new file mode 100644 index 000000000..1dd4bce56 --- /dev/null +++ b/gramps/plugins/database/bsddb_support/__init__.py @@ -0,0 +1,95 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2000-2007 Donald N. Allingham +# +# 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. +# + +""" +Gramps Database API. + +Database Architecture +===================== + +Access to the database is made through Python classes. Exactly +what functionality you have is dependent on the properties of the +database. For example, if you are accessing a read-only view, then +you will only have access to a subset of the methods available. + +At the root of any database interface is either :py:class:`.DbReadBase` and/or +:py:class:`.DbWriteBase`. These define the methods to read and write to a +database, respectively. + +The full database hierarchy is: + +- :py:class:`.DbBsddb` - read and write implementation to BSDDB databases + + * :py:class:`.DbWriteBase` - virtual and implementation-independent methods + for reading data + + * :py:class:`.DbBsddbRead` - read-only (accessors, getters) implementation + to BSDDB databases + + + :py:class:`.DbReadBase` - virtual and implementation-independent + methods for reading data + + + :py:class:`.Callback` - callback and signal functions + + * :py:class:`.UpdateCallback` - callback functionality + +- :py:class:`.DbDjango` - read and write implementation to Django-based + databases + + * :py:class:`.DbWriteBase` - virtual and implementation-independent methods + for reading data + + * :py:class:`.DbReadBase` - virtual and implementation-independent methods + for reading data + +DbBsddb +======= + +The :py:class:`.DbBsddb` interface defines a hierarchical database +(non-relational) written in +`PyBSDDB <http://www.jcea.es/programacion/pybsddb.htm>`_. There is no +such thing as a database schema, and the meaning of the data is +defined in the Python classes above. The data is stored as pickled +tuples and unserialized into the primary data types (below). + +DbDjango +======== + +The DbDjango interface defines the Gramps data in terms of +*models* and *relations* from the +`Django project <http://www.djangoproject.com/>`_. The database +backend can be any implementation that supports Django, including +such popular SQL implementations as sqlite, MySQL, Postgresql, and +Oracle. The data is retrieved from the SQL fields, serialized and +then unserialized into the primary data types (below). + +More details can be found in the manual's +`Using database API <http://www.gramps-project.org/wiki/index.php?title=Using_database_API>`_. +""" + +from gramps.gen.db.base import * +from gramps.gen.db.dbconst import * +from .cursor import * +from .read import * +from .bsddbtxn import * +from gramps.gen.db.txn import * +from .undoredo import * +from gramps.gen.db.exceptions import * +from .write import * diff --git a/gramps/plugins/database/bsddb_support/backup.py b/gramps/plugins/database/bsddb_support/backup.py new file mode 100644 index 000000000..94350da56 --- /dev/null +++ b/gramps/plugins/database/bsddb_support/backup.py @@ -0,0 +1,74 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2007 Donald N. Allingham +# Copyright (C) 2011 Tim G L Lyons +# +# 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. +# +# gen/db/backup.py + +""" +Description +=========== + +This module Provides backup and restore functions for a database. The +backup function saves the data into backup files, while the restore +function loads the data back into a database. + +You should only restore the data into an empty database. + +Implementation +============== + +Not all of the database tables need to be backed up, since many are +automatically generated from the others. The tables that are backed up +are the primary tables and the metadata table. + +The database consists of a table of "pickled" tuples. Each of the +primary tables is "walked", and the pickled tuple is extracted, and +written to the backup file. + +Restoring the data is just as simple. The backup file is parsed an +entry at a time, and inserted into the associated database table. The +derived tables are built automatically as the items are entered into +db. +""" + +#------------------------------------------------------------------------- +# +# load standard python libraries +# +#------------------------------------------------------------------------- +import os +import pickle + +#------------------------------------------------------------------------ +# +# Gramps libs +# +#------------------------------------------------------------------------ +from gramps.gen.db.exceptions import DbException +from .write import FAMILY_TBL, PLACES_TBL, SOURCES_TBL, MEDIA_TBL, \ + EVENTS_TBL, PERSON_TBL, REPO_TBL, NOTE_TBL, TAG_TBL, META, CITATIONS_TBL + +#------------------------------------------------------------------------ +# +# Set up logging +# +#------------------------------------------------------------------------ +import logging +LOG = logging.getLogger(".Backup") + diff --git a/gramps/gen/db/bsddbtxn.py b/gramps/plugins/database/bsddb_support/bsddbtxn.py similarity index 100% rename from gramps/gen/db/bsddbtxn.py rename to gramps/plugins/database/bsddb_support/bsddbtxn.py diff --git a/gramps/gen/db/cursor.py b/gramps/plugins/database/bsddb_support/cursor.py similarity index 100% rename from gramps/gen/db/cursor.py rename to gramps/plugins/database/bsddb_support/cursor.py diff --git a/gramps/gen/db/read.py b/gramps/plugins/database/bsddb_support/read.py similarity index 98% rename from gramps/gen/db/read.py rename to gramps/plugins/database/bsddb_support/read.py index 87dc112d6..9280dbc09 100644 --- a/gramps/gen/db/read.py +++ b/gramps/plugins/database/bsddb_support/read.py @@ -53,30 +53,32 @@ import logging # GRAMPS libraries # #------------------------------------------------------------------------- -from ..lib.mediaobj import MediaObject -from ..lib.person import Person -from ..lib.family import Family -from ..lib.src import Source -from ..lib.citation import Citation -from ..lib.event import Event -from ..lib.place import Place -from ..lib.repo import Repository -from ..lib.note import Note -from ..lib.tag import Tag -from ..lib.genderstats import GenderStats -from ..lib.researcher import Researcher -from ..lib.nameorigintype import NameOriginType +from gramps.gen.lib.mediaobj import MediaObject +from gramps.gen.lib.person import Person +from gramps.gen.lib.family import Family +from gramps.gen.lib.src import Source +from gramps.gen.lib.citation import Citation +from gramps.gen.lib.event import Event +from gramps.gen.lib.place import Place +from gramps.gen.lib.repo import Repository +from gramps.gen.lib.note import Note +from gramps.gen.lib.tag import Tag +from gramps.gen.lib.genderstats import GenderStats +from gramps.gen.lib.researcher import Researcher +from gramps.gen.lib.nameorigintype import NameOriginType -from .dbconst import * -from ..utils.callback import Callback -from ..utils.cast import conv_dbstr_to_unicode -from . import (BsddbBaseCursor, DbReadBase) -from ..utils.id import create_id -from ..errors import DbError -from ..constfunc import handle2internal, get_env_var -from ..const import GRAMPS_LOCALE as glocale +from gramps.gen.utils.callback import Callback +from gramps.gen.utils.cast import conv_dbstr_to_unicode +from . import BsddbBaseCursor +from gramps.gen.db.base import DbReadBase +from gramps.gen.utils.id import create_id +from gramps.gen.errors import DbError +from gramps.gen.constfunc import handle2internal, get_env_var +from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext +from gramps.gen.db.dbconst import * + LOG = logging.getLogger(DBLOGNAME) LOG = logging.getLogger(".citation") #------------------------------------------------------------------------- @@ -84,7 +86,6 @@ LOG = logging.getLogger(".citation") # constants # #------------------------------------------------------------------------- -from .dbconst import * _SIGBASE = ('person', 'family', 'source', 'citation', 'event', 'media', 'place', 'repository', @@ -1975,3 +1976,16 @@ class DbBsddbRead(DbReadBase, Callback): self.__log_error() name = None return name + + def get_summary(self): + """ + Returns dictionary of summary item. + Should include, if possible: + + _("Number of people") + _("Version") + _("Schema version") + """ + return { + _("Number of people"): self.get_number_of_people(), + } diff --git a/gramps/plugins/database/bsddb_support/summary.py b/gramps/plugins/database/bsddb_support/summary.py new file mode 100644 index 000000000..ccc94d2dc --- /dev/null +++ b/gramps/plugins/database/bsddb_support/summary.py @@ -0,0 +1,62 @@ +## Removed from clidbman.py +## specific to bsddb + +from bsddb3 import dbshelve, db +import os + +from gramps.gen.db import META, PERSON_TBL +from gramps.gen.db.dbconst import BDBVERSFN + +def get_dbdir_summary(dirpath, name): + """ + Returns (people_count, bsddb_version, schema_version) of + current DB. + Returns ("Unknown", "Unknown", "Unknown") if invalid DB or other error. + """ + + bdbversion_file = os.path.join(dirpath, BDBVERSFN) + if os.path.isfile(bdbversion_file): + vers_file = open(bdbversion_file) + bsddb_version = vers_file.readline().strip() + else: + return "Unknown", "Unknown", "Unknown" + + current_bsddb_version = str(db.version()) + if bsddb_version != current_bsddb_version: + return "Unknown", bsddb_version, "Unknown" + + env = db.DBEnv() + flags = db.DB_CREATE | db.DB_PRIVATE |\ + db.DB_INIT_MPOOL |\ + db.DB_INIT_LOG | db.DB_INIT_TXN + try: + env.open(dirpath, flags) + except Exception as msg: + LOG.warning("Error opening db environment for '%s': %s" % + (name, str(msg))) + try: + env.close() + except Exception as msg: + LOG.warning("Error closing db environment for '%s': %s" % + (name, str(msg))) + return "Unknown", bsddb_version, "Unknown" + dbmap1 = dbshelve.DBShelf(env) + fname = os.path.join(dirpath, META + ".db") + try: + dbmap1.open(fname, META, db.DB_HASH, db.DB_RDONLY) + except: + env.close() + return "Unknown", bsddb_version, "Unknown" + schema_version = dbmap1.get(b'version', default=None) + dbmap1.close() + dbmap2 = dbshelve.DBShelf(env) + fname = os.path.join(dirpath, PERSON_TBL + ".db") + try: + dbmap2.open(fname, PERSON_TBL, db.DB_HASH, db.DB_RDONLY) + except: + env.close() + return "Unknown", bsddb_version, schema_version + count = len(dbmap2) + dbmap2.close() + env.close() + return (count, bsddb_version, schema_version) diff --git a/gramps/plugins/database/bsddb_support/test/__init__.py b/gramps/plugins/database/bsddb_support/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gramps/gen/db/test/cursor_test.py b/gramps/plugins/database/bsddb_support/test/cursor_test.py similarity index 100% rename from gramps/gen/db/test/cursor_test.py rename to gramps/plugins/database/bsddb_support/test/cursor_test.py diff --git a/gramps/gen/db/test/db_test.py b/gramps/plugins/database/bsddb_support/test/db_test.py similarity index 100% rename from gramps/gen/db/test/db_test.py rename to gramps/plugins/database/bsddb_support/test/db_test.py diff --git a/gramps/gen/db/test/grampsdbtestbase.py b/gramps/plugins/database/bsddb_support/test/grampsdbtestbase.py similarity index 100% rename from gramps/gen/db/test/grampsdbtestbase.py rename to gramps/plugins/database/bsddb_support/test/grampsdbtestbase.py diff --git a/gramps/gen/db/test/reference_map_test.py b/gramps/plugins/database/bsddb_support/test/reference_map_test.py similarity index 100% rename from gramps/gen/db/test/reference_map_test.py rename to gramps/plugins/database/bsddb_support/test/reference_map_test.py diff --git a/gramps/plugins/database/bsddb_support/undoredo.py b/gramps/plugins/database/bsddb_support/undoredo.py new file mode 100644 index 000000000..da6b3368e --- /dev/null +++ b/gramps/plugins/database/bsddb_support/undoredo.py @@ -0,0 +1,516 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2004-2006 Donald N. Allingham +# Copyright (C) 2011 Tim G L Lyons +# +# 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. +# + +""" +Exports the DbUndo class for managing Gramps transactions +undos and redos. +""" + +#------------------------------------------------------------------------- +# +# Standard python modules +# +#------------------------------------------------------------------------- +import time, os +import pickle +from collections import deque + +try: + from bsddb3 import db +except: + # FIXME: make this more abstract to deal with other backends + class db: + DBRunRecoveryError = 0 + DBAccessError = 0 + DBPageNotFoundError = 0 + DBInvalidArgError = 0 + +from gramps.gen.const import GRAMPS_LOCALE as glocale +_ = glocale.translation.gettext + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- +from gramps.gen.constfunc import conv_to_unicode, handle2internal, win +from gramps.gen.db.dbconst import * +from . import BSDDBTxn +from gramps.gen.errors import DbError + +#------------------------------------------------------------------------- +# +# Local Constants +# +#------------------------------------------------------------------------- +DBERRS = (db.DBRunRecoveryError, db.DBAccessError, + db.DBPageNotFoundError, db.DBInvalidArgError) + +_SIGBASE = ('person', 'family', 'source', 'event', 'media', + 'place', 'repository', 'reference', 'note', 'tag', 'citation') + +#------------------------------------------------------------------------- +# +# DbUndo class +# +#------------------------------------------------------------------------- +class DbUndo(object): + """ + Base class for the Gramps undo/redo manager. Needs to be subclassed + for use with a real backend. + """ + + __slots__ = ('undodb', 'db', 'mapbase', 'undo_history_timestamp', + 'txn', 'undoq', 'redoq') + + def __init__(self, grampsdb): + """ + Class constructor. Set up main instance variables + """ + self.db = grampsdb + self.undoq = deque() + self.redoq = deque() + self.undo_history_timestamp = time.time() + self.txn = None + # N.B. the databases have to be in the same order as the numbers in + # xxx_KEY in gen/db/dbconst.py + self.mapbase = ( + self.db.person_map, + self.db.family_map, + self.db.source_map, + self.db.event_map, + self.db.media_map, + self.db.place_map, + self.db.repository_map, + self.db.reference_map, + self.db.note_map, + self.db.tag_map, + self.db.citation_map, + ) + + def clear(self): + """ + Clear the undo/redo list (but not the backing storage) + """ + self.undoq.clear() + self.redoq.clear() + self.undo_history_timestamp = time.time() + self.txn = None + + def __enter__(self, value): + """ + Context manager method to establish the context + """ + self.open(value) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """ + Context manager method to finish the context + """ + if exc_type is None: + self.close() + return exc_type is None + + def open(self, value): + """ + Open the backing storage. Needs to be overridden in the derived + class. + """ + raise NotImplementedError + + def close(self): + """ + Close the backing storage. Needs to be overridden in the derived + class. + """ + raise NotImplementedError + + def append(self, value): + """ + Add a new entry on the end. Needs to be overridden in the derived + class. + """ + raise NotImplementedError + + def __getitem__(self, index): + """ + Returns an entry by index number. Needs to be overridden in the + derived class. + """ + raise NotImplementedError + + def __setitem__(self, index, value): + """ + Set an entry to a value. Needs to be overridden in the derived class. + """ + raise NotImplementedError + + def __len__(self): + """ + Returns the number of entries. Needs to be overridden in the derived + class. + """ + raise NotImplementedError + + def commit(self, txn, msg): + """ + Commit the transaction to the undo/redo database. "txn" should be + an instance of Gramps transaction class + """ + txn.set_description(msg) + txn.timestamp = time.time() + self.undoq.append(txn) + + def undo(self, update_history=True): + """ + Undo a previously committed transaction + """ + if self.db.readonly or self.undo_count == 0: + return False + return self.__undo(update_history) + + def redo(self, update_history=True): + """ + Redo a previously committed, then undone, transaction + """ + if self.db.readonly or self.redo_count == 0: + return False + return self.__redo(update_history) + + def undoredo(func): + """ + Decorator function to wrap undo and redo operations within a bsddb + transaction. It also catches bsddb errors and raises an exception + as appropriate + """ + def try_(self, *args, **kwargs): + try: + with BSDDBTxn(self.db.env) as txn: + self.txn = self.db.txn = txn.txn + status = func(self, *args, **kwargs) + if not status: + txn.abort() + self.db.txn = None + return status + + except DBERRS as msg: + self.db._log_error() + raise DbError(msg) + + return try_ + + @undoredo + def __undo(self, update_history=True): + """ + Access the last committed transaction, and revert the data to the + state before the transaction was committed. + """ + txn = self.undoq.pop() + self.redoq.append(txn) + transaction = txn + db = self.db + subitems = transaction.get_recnos(reverse=True) + + # Process all records in the transaction + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + + if key == REFERENCE_KEY: + self.undo_reference(old_data, handle, self.mapbase[key]) + else: + self.undo_data(old_data, handle, self.mapbase[key], + db.emit, _SIGBASE[key]) + # Notify listeners + if db.undo_callback: + if self.undo_count > 0: + db.undo_callback(_("_Undo %s") + % self.undoq[-1].get_description()) + else: + db.undo_callback(None) + + if db.redo_callback: + db.redo_callback(_("_Redo %s") + % transaction.get_description()) + + if update_history and db.undo_history_callback: + db.undo_history_callback() + return True + + @undoredo + def __redo(self, db=None, update_history=True): + """ + Access the last undone transaction, and revert the data to the state + before the transaction was undone. + """ + txn = self.redoq.pop() + self.undoq.append(txn) + transaction = txn + db = self.db + subitems = transaction.get_recnos() + + # Process all records in the transaction + for record_id in subitems: + (key, trans_type, handle, old_data, new_data) = \ + pickle.loads(self.undodb[record_id]) + + if key == REFERENCE_KEY: + self.undo_reference(new_data, handle, self.mapbase[key]) + else: + self.undo_data(new_data, handle, self.mapbase[key], + db.emit, _SIGBASE[key]) + # Notify listeners + if db.undo_callback: + db.undo_callback(_("_Undo %s") + % transaction.get_description()) + + if db.redo_callback: + if self.redo_count > 1: + new_transaction = self.redoq[-2] + db.redo_callback(_("_Redo %s") + % new_transaction.get_description()) + else: + db.redo_callback(None) + + if update_history and db.undo_history_callback: + db.undo_history_callback() + return True + + def undo_reference(self, data, handle, db_map): + """ + Helper method to undo a reference map entry + """ + try: + if data is None: + db_map.delete(handle, txn=self.txn) + else: + db_map.put(handle, data, txn=self.txn) + + except DBERRS as msg: + self.db._log_error() + raise DbError(msg) + + def undo_data(self, data, handle, db_map, emit, signal_root): + """ + Helper method to undo/redo the changes made + """ + try: + if data is None: + emit(signal_root + '-delete', ([handle2internal(handle)],)) + db_map.delete(handle, txn=self.txn) + else: + ex_data = db_map.get(handle, txn=self.txn) + if ex_data: + signal = signal_root + '-update' + else: + signal = signal_root + '-add' + db_map.put(handle, data, txn=self.txn) + emit(signal, ([handle2internal(handle)],)) + + except DBERRS as msg: + self.db._log_error() + raise DbError(msg) + + undo_count = property(lambda self:len(self.undoq)) + redo_count = property(lambda self:len(self.redoq)) + +class DbUndoList(DbUndo): + """ + Implementation of the Gramps undo database using a Python list + """ + def __init__(self, grampsdb): + """ + Class constructor + """ + super(DbUndoList, self).__init__(grampsdb) + self.undodb = [] + + def open(self): + """ + A list does not need to be opened + """ + pass + + def close(self): + """ + Close the list by resetting it to empty + """ + self.undodb = [] + self.clear() + + def append(self, value): + """ + Add an entry on the end of the list + """ + self.undodb.append(value) + return len(self.undodb)-1 + + def __getitem__(self, index): + """ + Return an item at the specified index + """ + return self.undodb[index] + + def __setitem__(self, index, value): + """ + Set an item at the speficied index to the given value + """ + self.undodb[index] = value + + def __iter__(self): + """ + Iterator + """ + for item in self.undodb: + yield item + + def __len__(self): + """ + Return number of entries in the list + """ + return len(self.undodb) + +class DbUndoBSDDB(DbUndo): + """ + Class constructor for Gramps undo/redo database using a bsddb recno + database as the backing store. + """ + + def __init__(self, grampsdb, path): + """ + Class constructor + """ + super(DbUndoBSDDB, self).__init__(grampsdb) + self.undodb = db.DB() + self.path = path + + def open(self): + """ + Open the undo/redo database + """ + path = self.path + self.undodb.open(path, db.DB_RECNO, db.DB_CREATE) + + def close(self): + """ + Close the undo/redo database + """ + self.undodb.close() + self.undodb = None + self.mapbase = None + self.db = None + + try: + os.remove(self.path) + except OSError: + pass + self.clear() + + def append(self, value): + """ + Add an entry on the end of the database + """ + return self.undodb.append(value) + + def __len__(self): + """ + Returns the number of entries in the database + """ + x = self.undodb.stat()['nkeys'] + y = len(self.undodb) + assert x == y + return x + + def __getitem__(self, index): + """ + Returns the entry stored at the specified index + """ + return self.undodb.get(index) + + def __setitem__(self, index, value): + """ + Sets the entry stored at the specified index to the value given. + """ + self.undodb.put(index, value) + + def __iter__(self): + """ + Iterator + """ + cursor = self.undodb.cursor() + data = cursor.first() + while data: + yield data + data = next(cursor) + +def testundo(): + class T: + def __init__(self): + self.msg = '' + self.timetstamp = 0 + def set_description(self, msg): + self.msg = msg + + class D: + def __init__(self): + self.person_map = {} + self.family_map = {} + self.source_map = {} + self.event_map = {} + self.media_map = {} + self.place_map = {} + self.note_map = {} + self.tag_map = {} + self.repository_map = {} + self.reference_map = {} + + print("list tests") + undo = DbUndoList(D()) + print(undo.append('foo')) + print(undo.append('bar')) + print(undo[0]) + undo[0] = 'foobar' + print(undo[0]) + print("len", len(undo)) + print("iter") + for data in undo: + print(data) + print() + print("bsddb tests") + undo = DbUndoBSDDB(D(), '/tmp/testundo') + undo.open() + print(undo.append('foo')) + print(undo.append('fo2')) + print(undo.append('fo3')) + print(undo[1]) + undo[1] = 'bar' + print(undo[1]) + for data in undo: + print(data) + print("len", len(undo)) + + print("test commit") + undo.commit(T(), msg="test commit") + undo.close() + +if __name__ == '__main__': + testundo() diff --git a/gramps/gen/db/upgrade.py b/gramps/plugins/database/bsddb_support/upgrade.py similarity index 98% rename from gramps/gen/db/upgrade.py rename to gramps/plugins/database/bsddb_support/upgrade.py index c720a9547..1f1cd6719 100644 --- a/gramps/gen/db/upgrade.py +++ b/gramps/plugins/database/bsddb_support/upgrade.py @@ -39,23 +39,24 @@ from bsddb3 import db # Gramps modules # #------------------------------------------------------------------------- -from ..const import GRAMPS_LOCALE as glocale +from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext -from ..constfunc import handle2internal -from ..lib.markertype import MarkerType -from ..lib.nameorigintype import NameOriginType -from ..lib.place import Place -from ..lib.placeref import PlaceRef -from ..lib.placetype import PlaceType -from ..lib.placename import PlaceName -from ..lib.eventtype import EventType -from ..lib.tag import Tag -from ..utils.file import create_checksum -from ..utils.id import create_id +from gramps.gen.constfunc import handle2internal +from gramps.gen.lib.markertype import MarkerType +from gramps.gen.lib.nameorigintype import NameOriginType +from gramps.gen.lib.place import Place +from gramps.gen.lib.placeref import PlaceRef +from gramps.gen.lib.placetype import PlaceType +from gramps.gen.lib.placename import PlaceName +from gramps.gen.lib.eventtype import EventType +from gramps.gen.lib.tag import Tag +from gramps.gen.utils.file import create_checksum +from gramps.gen.utils.id import create_id from . import BSDDBTxn from .write import _mkname, SURNAMES -from .dbconst import (PERSON_KEY, FAMILY_KEY, EVENT_KEY, - MEDIA_KEY, PLACE_KEY, REPOSITORY_KEY, SOURCE_KEY) +from gramps.gen.db.dbconst import (PERSON_KEY, FAMILY_KEY, EVENT_KEY, + MEDIA_KEY, PLACE_KEY, REPOSITORY_KEY, + SOURCE_KEY) from gramps.gui.dialog import (InfoDialog) LOG = logging.getLogger(".upgrade") @@ -359,7 +360,7 @@ def upgrade_datamap_17(datamap): """ new_srcattr_list = [] private = False - from ..lib.srcattrtype import SrcAttributeType + from gramps.gen.lib.srcattrtype import SrcAttributeType for (key, value) in datamap.items(): the_type = SrcAttributeType(key).serialize() new_srcattr_list.append((private, the_type, value)) diff --git a/gramps/gen/db/write.py b/gramps/plugins/database/bsddb_support/write.py similarity index 93% rename from gramps/gen/db/write.py rename to gramps/plugins/database/bsddb_support/write.py index 607735ec8..2eb665ac5 100644 --- a/gramps/gen/db/write.py +++ b/gramps/plugins/database/bsddb_support/write.py @@ -40,49 +40,46 @@ from functools import wraps import logging from sys import maxsize, getfilesystemencoding, version_info -try: - from bsddb3 import dbshelve, db -except: - # FIXME: make this more abstract to deal with other backends - class db: - DB_HASH = 0 - DBRunRecoveryError = 0 - DBAccessError = 0 - DBPageNotFoundError = 0 - DBInvalidArgError = 0 +from bsddb3 import dbshelve, db +from bsddb3.db import DB_CREATE, DB_AUTO_COMMIT, DB_DUP, DB_DUPSORT, DB_RDONLY + +DBFLAGS_O = DB_CREATE | DB_AUTO_COMMIT # Default flags for database open +DBFLAGS_R = DB_RDONLY # Flags to open a database read-only +DBFLAGS_D = DB_DUP | DB_DUPSORT # Default flags for duplicate keys #------------------------------------------------------------------------- # # Gramps modules # #------------------------------------------------------------------------- -from ..lib.person import Person -from ..lib.family import Family -from ..lib.src import Source -from ..lib.citation import Citation -from ..lib.event import Event -from ..lib.place import Place -from ..lib.repo import Repository -from ..lib.mediaobj import MediaObject -from ..lib.note import Note -from ..lib.tag import Tag -from ..lib.genderstats import GenderStats -from ..lib.researcher import Researcher +from gramps.gen.lib.person import Person +from gramps.gen.lib.family import Family +from gramps.gen.lib.src import Source +from gramps.gen.lib.citation import Citation +from gramps.gen.lib.event import Event +from gramps.gen.lib.place import Place +from gramps.gen.lib.repo import Repository +from gramps.gen.lib.mediaobj import MediaObject +from gramps.gen.lib.note import Note +from gramps.gen.lib.tag import Tag +from gramps.gen.lib.genderstats import GenderStats +from gramps.gen.lib.researcher import Researcher from . import (DbBsddbRead, DbWriteBase, BSDDBTxn, DbTxn, BsddbBaseCursor, BsddbDowngradeError, DbVersionError, DbEnvironmentError, DbUpgradeRequiredError, find_surname, - find_byte_surname, find_surname_name, DbUndoBSDDB as DbUndo, - exceptions) -from .dbconst import * -from ..utils.callback import Callback -from ..utils.cast import conv_dbstr_to_unicode -from ..utils.id import create_id -from ..updatecallback import UpdateCallback -from ..errors import DbError -from ..constfunc import (win, conv_to_unicode, handle2internal, + find_byte_surname, find_surname_name, DbUndoBSDDB as DbUndo) + +from gramps.gen.db import exceptions +from gramps.gen.db.dbconst import * +from gramps.gen.utils.callback import Callback +from gramps.gen.utils.cast import conv_dbstr_to_unicode +from gramps.gen.utils.id import create_id +from gramps.gen.updatecallback import UpdateCallback +from gramps.gen.errors import DbError +from gramps.gen.constfunc import (win, conv_to_unicode, handle2internal, get_env_var) -from ..const import HOME_DIR, GRAMPS_LOCALE as glocale +from gramps.gen.const import HOME_DIR, GRAMPS_LOCALE as glocale _ = glocale.translation.gettext _LOG = logging.getLogger(DBLOGNAME) @@ -133,39 +130,6 @@ DBERRS = (db.DBRunRecoveryError, db.DBAccessError, # these maps or modifying the values of the keys will break # existing databases. -CLASS_TO_KEY_MAP = {Person.__name__: PERSON_KEY, - Family.__name__: FAMILY_KEY, - Source.__name__: SOURCE_KEY, - Citation.__name__: CITATION_KEY, - Event.__name__: EVENT_KEY, - MediaObject.__name__: MEDIA_KEY, - Place.__name__: PLACE_KEY, - Repository.__name__:REPOSITORY_KEY, - Note.__name__: NOTE_KEY, - Tag.__name__: TAG_KEY} - -KEY_TO_CLASS_MAP = {PERSON_KEY: Person.__name__, - FAMILY_KEY: Family.__name__, - SOURCE_KEY: Source.__name__, - CITATION_KEY: Citation.__name__, - EVENT_KEY: Event.__name__, - MEDIA_KEY: MediaObject.__name__, - PLACE_KEY: Place.__name__, - REPOSITORY_KEY: Repository.__name__, - NOTE_KEY: Note.__name__, - TAG_KEY: Tag.__name__} - -KEY_TO_NAME_MAP = {PERSON_KEY: 'person', - FAMILY_KEY: 'family', - EVENT_KEY: 'event', - SOURCE_KEY: 'source', - CITATION_KEY: 'citation', - PLACE_KEY: 'place', - MEDIA_KEY: 'media', - REPOSITORY_KEY: 'repository', - #REFERENCE_KEY: 'reference', - NOTE_KEY: 'note', - TAG_KEY: 'tag'} #------------------------------------------------------------------------- # # Helper functions @@ -689,7 +653,7 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): return False @catch_db_error - def load(self, name, callback, mode=DBMODE_W, force_schema_upgrade=False, + def load(self, name, callback=None, mode=DBMODE_W, force_schema_upgrade=False, force_bsddb_upgrade=False, force_bsddb_downgrade=False, force_python_upgrade=False): @@ -2433,6 +2397,11 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): version = str(_DBVERSION) version_file.write(version) + versionpath = os.path.join(name, str(DBBACKEND)) + _LOG.debug("Write database backend file to 'bsddb'") + with open(versionpath, "w") as version_file: + version_file.write("bsddb") + self.metadata.close() self.env.close() @@ -2449,6 +2418,180 @@ class DbBsddb(DbBsddbRead, DbWriteBase, UpdateCallback): """ return DbTxn + def backup(self): + """ + Exports the database to a set of backup files. These files consist + of the pickled database tables, one file for each table. + + The heavy lifting is done by the private :py:func:`__do__export` function. + The purpose of this function is to catch any exceptions that occur. + + :param database: database instance to backup + :type database: DbDir + """ + try: + do_export(self) + except (OSError, IOError) as msg: + raise DbException(str(msg)) + + def restore(self): + """ + Restores the database to a set of backup files. These files consist + of the pickled database tables, one file for each table. + + The heavy lifting is done by the private :py:func:`__do__restore` function. + The purpose of this function is to catch any exceptions that occur. + + :param database: database instance to restore + :type database: DbDir + """ + try: + do_restore(self) + except (OSError, IOError) as msg: + raise DbException(str(msg)) + + def get_summary(self): + """ + Returns dictionary of summary item. + Should include, if possible: + + _("Number of people") + _("Version") + _("Schema version") + """ + schema_version = self.metadata.get(b'version', default=None) + bdbversion_file = os.path.join(self.path, BDBVERSFN) + if os.path.isfile(bdbversion_file): + vers_file = open(bdbversion_file) + bsddb_version = vers_file.readline().strip() + else: + bsddb_version = _("Unknown") + return { + _("Number of people"): self.get_number_of_people(), + _("Schema version"): schema_version, + _("Version"): bsddb_version, + } + + def prepare_import(self): + """ + Initialization before imports + """ + pass + + def commit_import(self): + """ + Post process after imports + """ + pass + +def mk_backup_name(database, base): + """ + Return the backup name of the database table + + :param database: database instance + :type database: DbDir + :param base: base name of the table + :type base: str + """ + return os.path.join(database.get_save_path(), base + ".gbkp") + +def mk_tmp_name(database, base): + """ + Return the temporary backup name of the database table + + :param database: database instance + :type database: DbDir + :param base: base name of the table + :type base: str + """ + return os.path.join(database.get_save_path(), base + ".gbkp.new") + +def do_export(database): + """ + Loop through each table of the database, saving the pickled data + a file. + + :param database: database instance to backup + :type database: DbDir + """ + try: + for (base, tbl) in build_tbl_map(database): + backup_name = mk_tmp_name(database, base) + backup_table = open(backup_name, 'wb') + + cursor = tbl.cursor() + data = cursor.first() + while data: + pickle.dump(data, backup_table, 2) + data = cursor.next() + cursor.close() + backup_table.close() + except (IOError,OSError): + return + + for (base, tbl) in build_tbl_map(database): + new_name = mk_backup_name(database, base) + old_name = mk_tmp_name(database, base) + if os.path.isfile(new_name): + os.unlink(new_name) + os.rename(old_name, new_name) + +def do_restore(database): + """ + Loop through each table of the database, restoring the pickled data + to the appropriate database file. + + :param database: database instance to backup + :type database: DbDir + """ + for (base, tbl) in build_tbl_map(database): + backup_name = mk_backup_name(database, base) + backup_table = open(backup_name, 'rb') + load_tbl_txn(database, backup_table, tbl) + + database.rebuild_secondary() + +def load_tbl_txn(database, backup_table, tbl): + """ + Return the temporary backup name of the database table + + :param database: database instance + :type database: DbDir + :param backup_table: file containing the backup data + :type backup_table: file + :param tbl: Berkeley db database table + :type tbl: Berkeley db database table + """ + try: + while True: + data = pickle.load(backup_table) + txn = database.env.txn_begin() + tbl.put(data[0], data[1], txn=txn) + txn.commit() + except EOFError: + backup_table.close() + +def build_tbl_map(database): + """ + Builds a table map of names to database tables. + + :param database: database instance to backup + :type database: DbDir + """ + return [ + ( PERSON_TBL, database.person_map.db), + ( FAMILY_TBL, database.family_map.db), + ( PLACES_TBL, database.place_map.db), + ( SOURCES_TBL, database.source_map.db), + ( CITATIONS_TBL, database.citation_map.db), + ( REPO_TBL, database.repository_map.db), + ( NOTE_TBL, database.note_map.db), + ( MEDIA_TBL, database.media_map.db), + ( EVENTS_TBL, database.event_map.db), + ( TAG_TBL, database.tag_map.db), + ( META, database.metadata.db), + ] + def _mkname(path, name): return os.path.join(path, name + DBEXT) diff --git a/gramps/plugins/database/dictionarydb.gpr.py b/gramps/plugins/database/dictionarydb.gpr.py new file mode 100644 index 000000000..d035de58a --- /dev/null +++ b/gramps/plugins/database/dictionarydb.gpr.py @@ -0,0 +1,31 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2015 Douglas 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. +# + +plg = newplugin() +plg.id = 'dictionarydb' +plg.name = _("Dictionary Database Backend") +plg.name_accell = _("Di_ctionary Database Backend") +plg.description = _("Dictionary (in-memory) Database Backend") +plg.version = '1.0' +plg.gramps_target_version = "5.0" +plg.status = STABLE +plg.fname = 'dictionarydb.py' +plg.ptype = DATABASE +plg.databaseclass = 'DictionaryDb' diff --git a/gramps/gen/db/dictionary.py b/gramps/plugins/database/dictionarydb.py similarity index 56% rename from gramps/gen/db/dictionary.py rename to gramps/plugins/database/dictionarydb.py index 36a8cd5fa..3fe7e0da5 100644 --- a/gramps/gen/db/dictionary.py +++ b/gramps/plugins/database/dictionarydb.py @@ -21,68 +21,163 @@ #------------------------------------------------------------------------ # -# Gramps Modules +# Python Modules # #------------------------------------------------------------------------ import pickle import base64 import time import re -from . import DbReadBase, DbWriteBase, DbTxn -from . import (PERSON_KEY, - FAMILY_KEY, - CITATION_KEY, - SOURCE_KEY, - EVENT_KEY, - MEDIA_KEY, - PLACE_KEY, - REPOSITORY_KEY, - NOTE_KEY, - TAG_KEY) +import os +import logging -from ..const import GRAMPS_LOCALE as glocale +#------------------------------------------------------------------------ +# +# Gramps Modules +# +#------------------------------------------------------------------------ +from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext -from ..errors import DbError -from ..utils.id import create_id -from ..lib.researcher import Researcher -from ..lib.mediaobj import MediaObject -from ..lib.person import Person -from ..lib.family import Family -from ..lib.src import Source -from ..lib.citation import Citation -from ..lib.event import Event -from ..lib.place import Place -from ..lib.repo import Repository -from ..lib.note import Note -from ..lib.tag import Tag +from gramps.gen.db import (DbReadBase, DbWriteBase, DbTxn, + KEY_TO_NAME_MAP, KEY_TO_CLASS_MAP) +from gramps.gen.db.undoredo import DbUndo +from gramps.gen.db.dbconst import * +from gramps.gen.utils.callback import Callback +from gramps.gen.updatecallback import UpdateCallback +from gramps.gen.db import (PERSON_KEY, + FAMILY_KEY, + CITATION_KEY, + SOURCE_KEY, + EVENT_KEY, + MEDIA_KEY, + PLACE_KEY, + REPOSITORY_KEY, + NOTE_KEY, + TAG_KEY) -class Cursor(object): +from gramps.gen.utils.id import create_id +from gramps.gen.lib.researcher import Researcher +from gramps.gen.lib.mediaobj import MediaObject +from gramps.gen.lib.person import Person +from gramps.gen.lib.family import Family +from gramps.gen.lib.src import Source +from gramps.gen.lib.citation import Citation +from gramps.gen.lib.event import Event +from gramps.gen.lib.place import Place +from gramps.gen.lib.repo import Repository +from gramps.gen.lib.note import Note +from gramps.gen.lib.tag import Tag +from gramps.gen.lib.genderstats import GenderStats + +_LOG = logging.getLogger(DBLOGNAME) + +def touch(fname, mode=0o666, dir_fd=None, **kwargs): + ## After http://stackoverflow.com/questions/1158076/implement-touch-using-python + flags = os.O_CREAT | os.O_APPEND + with os.fdopen(os.open(fname, flags=flags, mode=mode, dir_fd=dir_fd)) as f: + os.utime(f.fileno() if os.utime in os.supports_fd else fname, + dir_fd=None if os.supports_fd else dir_fd, **kwargs) + +class Environment(object): """ - Iterates through model returning (handle, raw_data)... + Implements the Environment API. """ - def __init__(self, model, func): - self.model = model - self.func = func + def __init__(self, db): + self.db = db + + def txn_begin(self): + return DictionaryTxn("DictionaryDb Transaction", self.db) + +class Table(object): + """ + Implements Table interface. + """ + def __init__(self, funcs): + self.funcs = funcs + + def cursor(self): + """ + Returns a Cursor for this Table. + """ + return self.funcs["cursor_func"]() + + def put(self, key, data, txn=None): + self.funcs["add_func"](data, txn) + +class Map(dict): + """ + Implements the map API for person_map, etc. + + Takes a Table() as argument. + """ + def __init__(self, tbl, *args, **kwargs): + super().__init__(*args, **kwargs) + self.db = tbl + +class MetaCursor(object): + def __init__(self): + pass def __enter__(self): return self def __iter__(self): - return self + return self.__next__() def __next__(self): - for handle in self.model.keys(): - return (handle, self.func(handle)) - def next(self): - for handle in self.model.keys(): - return (handle, self.func(handle)) + yield None def __exit__(self, *args, **kwargs): pass + def iter(self): + yield None + def first(self): + self._iter = self.__iter__() + return self.next() + def next(self): + try: + return next(self._iter) + except: + return None def close(self): pass -class Bookmarks: - def get(self): - return [] # handles - def append(self, handle): +class Cursor(object): + def __init__(self, map): + self.map = map + self._iter = self.__iter__() + def __enter__(self): + return self + def __iter__(self): + for item in self.map.keys(): + yield (bytes(item, "utf-8"), self.map[item]) + def __next__(self): + try: + return self._iter.__next__() + except StopIteration: + return None + def __exit__(self, *args, **kwargs): pass + def iter(self): + for item in self.map.keys(): + yield (bytes(item, "utf-8"), self.map[item]) + def first(self): + self._iter = self.__iter__() + try: + return next(self._iter) + except: + return + def next(self): + try: + return next(self._iter) + except: + return + def close(self): + pass + +class Bookmarks(object): + def __init__(self): + self.handles = [] + def get(self): + return self.handles + def append(self, handle): + self.handles.append(handle) class DictionaryTxn(DbTxn): def __init__(self, message, db, batch=False): @@ -102,13 +197,38 @@ class DictionaryTxn(DbTxn): """ txn[handle] = new_data -class DictionaryDb(DbWriteBase, DbReadBase): +class DictionaryDb(DbWriteBase, DbReadBase, UpdateCallback, Callback): """ A Gramps Database Backend. This replicates the grampsdb functions. """ - def __init__(self, *args, **kwargs): + __signals__ = dict((obj+'-'+op, signal) + for obj in + ['person', 'family', 'event', 'place', + 'source', 'citation', 'media', 'note', 'repository', 'tag'] + for op, signal in zip( + ['add', 'update', 'delete', 'rebuild'], + [(list,), (list,), (list,), None] + ) + ) + + # 2. Signals for long operations + __signals__.update(('long-op-'+op, signal) for op, signal in zip( + ['start', 'heartbeat', 'end'], + [(object,), None, None] + )) + + # 3. Special signal for change in home person + __signals__['home-person-changed'] = None + + # 4. Signal for change in person group name, parameters are + __signals__['person-groupname-rebuild'] = (str, str) + + __callback_map = {} + + def __init__(self, directory=None): DbReadBase.__init__(self) DbWriteBase.__init__(self) + Callback.__init__(self) self._tables['Person'].update( { "handle_func": self.get_person_from_handle, @@ -118,6 +238,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_person_handles, "add_func": self.add_person, "commit_func": self.commit_person, + "iter_func": self.iter_people, }) self._tables['Family'].update( { @@ -128,6 +249,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_family_handles, "add_func": self.add_family, "commit_func": self.commit_family, + "iter_func": self.iter_families, }) self._tables['Source'].update( { @@ -138,6 +260,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_source_handles, "add_func": self.add_source, "commit_func": self.commit_source, + "iter_func": self.iter_sources, }) self._tables['Citation'].update( { @@ -148,6 +271,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_citation_handles, "add_func": self.add_citation, "commit_func": self.commit_citation, + "iter_func": self.iter_citations, }) self._tables['Event'].update( { @@ -158,6 +282,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_event_handles, "add_func": self.add_event, "commit_func": self.commit_event, + "iter_func": self.iter_events, }) self._tables['Media'].update( { @@ -168,6 +293,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_media_object_handles, "add_func": self.add_object, "commit_func": self.commit_media_object, + "iter_func": self.iter_media_objects, }) self._tables['Place'].update( { @@ -178,6 +304,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_place_handles, "add_func": self.add_place, "commit_func": self.commit_place, + "iter_func": self.iter_places, }) self._tables['Repository'].update( { @@ -188,6 +315,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_repository_handles, "add_func": self.add_repository, "commit_func": self.commit_repository, + "iter_func": self.iter_repositories, }) self._tables['Note'].update( { @@ -198,6 +326,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_note_handles, "add_func": self.add_note, "commit_func": self.commit_note, + "iter_func": self.iter_notes, }) self._tables['Tag'].update( { @@ -208,6 +337,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): "handles_func": self.get_tag_handles, "add_func": self.add_tag, "commit_func": self.commit_tag, + "iter_func": self.iter_tags, }) # skip GEDCOM cross-ref check for now: self.set_feature("skip-check-xref", True) @@ -252,18 +382,27 @@ class DictionaryDb(DbWriteBase, DbReadBase): self.omap_index = 0 self.rmap_index = 0 self.nmap_index = 0 - self.env = None - self.person_map = {} - self.family_map = {} - self.place_map = {} - self.citation_map = {} - self.source_map = {} - self.repository_map = {} - self.note_map = {} - self.media_map = {} - self.event_map = {} - self.tag_map = {} - self.metadata = {} + self.env = Environment(self) + self.person_map = Map(Table(self._tables["Person"])) + self.person_id_map = {} + self.family_map = Map(Table(self._tables["Family"])) + self.family_id_map = {} + self.place_map = Map(Table(self._tables["Place"])) + self.place_id_map = {} + self.citation_map = Map(Table(self._tables["Citation"])) + self.citation_id_map = {} + self.source_map = Map(Table(self._tables["Source"])) + self.source_id_map = {} + self.repository_map = Map(Table(self._tables["Repository"])) + self.repository_id_map = {} + self.note_map = Map(Table(self._tables["Note"])) + self.note_id_map = {} + self.media_map = Map(Table(self._tables["Media"])) + self.media_id_map = {} + self.event_map = Map(Table(self._tables["Event"])) + self.event_id_map = {} + self.tag_map = Map(Table(self._tables["Tag"])) + self.metadata = Map(Table({"cursor_func": lambda: MetaCursor()})) self.name_group = {} self.undo_callback = None self.redo_callback = None @@ -271,6 +410,17 @@ class DictionaryDb(DbWriteBase, DbReadBase): self.modified = 0 self.txn = DictionaryTxn("DbDictionary Transaction", self) self.transaction = None + self.undodb = DbUndo(self) + self.abort_possible = False + self._bm_changes = 0 + self._directory = directory + self.full_name = None + self.path = None + self.brief_name = None + self.genderStats = GenderStats() # can pass in loaded stats as dict + self.owner = Researcher() + if directory: + self.load(directory) def version_supported(self): """Return True when the file has a supported version.""" @@ -287,15 +437,15 @@ class DictionaryDb(DbWriteBase, DbReadBase): return None def transaction_commit(self, txn): - pass - - def enable_signals(self): + ## FIXME pass def get_undodb(self): + ## FIXME return None def transaction_abort(self, txn): + ## FIXME pass @staticmethod @@ -544,131 +694,196 @@ class DictionaryDb(DbWriteBase, DbReadBase): def get_name_group_mapping(self, key): return None - def get_researcher(self): - obj = Researcher() - return obj - def get_person_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.person_map.keys() + ## Fixme: implement sort + return self.person_map.keys() - def get_family_handles(self): + def get_family_handles(self, sort_handles=False): + ## Fixme: implement sort return self.family_map.keys() - def get_event_handles(self): + def get_event_handles(self, sort_handles=False): + ## Fixme: implement sort return self.event_map.keys() def get_citation_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.citation_map.keys() + ## Fixme: implement sort + return self.citation_map.keys() def get_source_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.source_map.keys() + ## Fixme: implement sort + return self.source_map.keys() def get_place_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.place_map.keys() + ## Fixme: implement sort + return self.place_map.keys() - def get_repository_handles(self): + def get_repository_handles(self, sort_handles=False): + ## Fixme: implement sort return self.repository_map.keys() def get_media_object_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.media_map.keys() + ## Fixme: implement sort + return self.media_map.keys() - def get_note_handles(self): + def get_note_handles(self, sort_handles=False): + ## Fixme: implement sort return self.note_map.keys() def get_tag_handles(self, sort_handles=False): - if sort_handles: - raise Exception("Implement!") - else: - return self.tag_map.keys() + # FIXME: implement sort + return self.tag_map.keys() def get_event_from_handle(self, handle): - return self.event_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + event = None + if handle in self.event_map: + event = Event.create(self.event_map[handle]) + return event def get_family_from_handle(self, handle): - return self.family_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + family = None + if handle in self.family_map: + family = Family.create(self.family_map[handle]) + return family def get_repository_from_handle(self, handle): - return self.repository_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + repository = None + if handle in self.repository_map: + repository = Repository.create(self.repository_map[handle]) + return repository def get_person_from_handle(self, handle): - return self.person_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + person = None + if handle in self.person_map: + person = Person.create(self.person_map[handle]) + return person def get_place_from_handle(self, handle): - place = self.place_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + place = None + if handle in self.place_map: + place = Place.create(self.place_map[handle]) return place def get_citation_from_handle(self, handle): - citation = self.citation_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + citation = None + if handle in self.citation_map: + citation = Citation.create(self.citation_map[handle]) return citation def get_source_from_handle(self, handle): - source = self.source_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + source = None + if handle in self.source_map: + source = Source.create(self.source_map[handle]) return source def get_note_from_handle(self, handle): - note = self.note_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + note = None + if handle in self.note_map: + note = Note.create(self.note_map[handle]) return note def get_object_from_handle(self, handle): - media = self.media_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + media = None + if handle in self.media_map: + media = MediaObject.create(self.media_map[handle]) return media def get_tag_from_handle(self, handle): - tag = self.tag_map[handle] + if isinstance(handle, bytes): + handle = str(handle, "utf-8") + tag = None + if handle in self.tag_map: + tag = Tag.create(self.tag_map[handle]) return tag def get_default_person(self): - return None + handle = self.get_default_handle() + if handle: + return self.get_person_from_handle(handle) + else: + return None def iter_people(self): - return (person for person in self.person_map.values()) + return (Person.create(person) for person in self.person_map.values()) def iter_person_handles(self): return (handle for handle in self.person_map.keys()) def iter_families(self): - return (family for family in self.family_map.values()) + return (Family.create(family) for family in self.family_map.values()) def iter_family_handles(self): return (handle for handle in self.family_map.keys()) def get_tag_from_name(self, name): - for tag in self.tag_map.values(): + ## Slow, but typically not too many tags: + for data in self.tag_map.values(): + tag = Tag.create(data) if tag.name == name: return tag return None - def get_family_from_gramps_id(self, gramps_id): - for family in self.family_map.values(): - if family.gramps_id == gramps_id: - return family + def get_person_from_gramps_id(self, gramps_id): + if gramps_id in self.person_id_map: + return Person.create(self.person_id_map[gramps_id]) return None - def get_person_from_gramps_id(self, gramps_id): - for person in self.person_map.values(): - if person.gramps_id == gramps_id: - return person + def get_family_from_gramps_id(self, gramps_id): + if gramps_id in self.family_id_map: + return Family.create(self.family_id_map[gramps_id]) + return None + + def get_citation_from_gramps_id(self, gramps_id): + if gramps_id in self.citation_id_map: + return Citation.create(self.citation_id_map[gramps_id]) + return None + + def get_source_from_gramps_id(self, gramps_id): + if gramps_id in self.source_id_map: + return Source.create(self.source_id_map[gramps_id]) + return None + + def get_event_from_gramps_id(self, gramps_id): + if gramps_id in self.event_id_map: + return Event.create(self.event_id_map[gramps_id]) + return None + + def get_media_from_gramps_id(self, gramps_id): + if gramps_id in self.media_id_map: + return MediaObject.create(self.media_id_map[gramps_id]) return None def get_place_from_gramps_id(self, gramps_id): - for place in self.place_map.values(): - if place.gramps_id == gramps_id: - return place + if gramps_id in self.place_id_map: + return Place.create(self.place_id_map[gramps_id]) + return None + + def get_repository_from_gramps_id(self, gramps_id): + if gramps_id in self.repository_id_map: + return Repository.create(self.repository_id_map[gramps_id]) + return None + + def get_note_from_gramps_id(self, gramps_id): + if gramps_id in self.note_id_map: + return Note.create(self.note_id_map[gramps_id]) return None def get_number_of_people(self): @@ -681,7 +896,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): return len(self.place_map) def get_number_of_tags(self): - return 0 # FIXME + return len(self.tag_map) def get_number_of_families(self): return len(self.family_map) @@ -702,52 +917,48 @@ class DictionaryDb(DbWriteBase, DbReadBase): return len(self.repository_map) def get_place_cursor(self): - return Cursor(self.place_map, self.get_raw_place_data) + return Cursor(self.place_map) def get_person_cursor(self): - return Cursor(self.person_map, self.get_raw_person_data) + return Cursor(self.person_map) def get_family_cursor(self): - return Cursor(self.family_map, self.get_raw_family_data) + return Cursor(self.family_map) def get_event_cursor(self): - return Cursor(self.event_map, self.get_raw_event_data) + return Cursor(self.event_map) def get_note_cursor(self): - return Cursor(self.note_map, self.get_raw_note_data) + return Cursor(self.note_map) def get_tag_cursor(self): - return Cursor(self.tag_map, self.get_raw_tag_data) + return Cursor(self.tag_map) def get_repository_cursor(self): - return Cursor(self.repository_map, self.get_raw_repository_data) + return Cursor(self.repository_map) def get_media_cursor(self): - return Cursor(self.media_map, self.get_raw_object_data) + return Cursor(self.media_map) def get_citation_cursor(self): - return Cursor(self.citation_map, self.get_raw_citation_data) + return Cursor(self.citation_map) def get_source_cursor(self): - return Cursor(self.source_map, self.get_raw_source_data) + return Cursor(self.source_map) def has_gramps_id(self, obj_key, gramps_id): key2table = { - PERSON_KEY: self.person_map, - FAMILY_KEY: self.family_map, - SOURCE_KEY: self.source_map, - CITATION_KEY: self.citation_map, - EVENT_KEY: self.event_map, - MEDIA_KEY: self.media_map, - PLACE_KEY: self.place_map, - REPOSITORY_KEY: self.repository_map, - NOTE_KEY: self.note_map, + PERSON_KEY: self.person_id_map, + FAMILY_KEY: self.family_id_map, + SOURCE_KEY: self.source_id_map, + CITATION_KEY: self.citation_id_map, + EVENT_KEY: self.event_id_map, + MEDIA_KEY: self.media_id_map, + PLACE_KEY: self.place_id_map, + REPOSITORY_KEY: self.repository_id_map, + NOTE_KEY: self.note_id_map, } - map = key2table[obj_key] - for item in map.values(): - if item.gramps_id == gramps_id: - return True - return False + return gramps_id in key2table[obj_key] def has_person_handle(self, handle): return handle in self.person_map @@ -788,54 +999,61 @@ class DictionaryDb(DbWriteBase, DbReadBase): pass def set_default_person_handle(self, handle): + ## FIXME pass def set_mediapath(self, mediapath): + ## FIXME pass def get_raw_person_data(self, handle): if handle in self.person_map: - return self.person_map[handle].serialize() + return self.person_map[handle] return None def get_raw_family_data(self, handle): if handle in self.family_map: - return self.family_map[handle].serialize() + return self.family_map[handle] return None def get_raw_citation_data(self, handle): if handle in self.citation_map: - return self.citation_map[handle].serialize() + return self.citation_map[handle] return None def get_raw_source_data(self, handle): if handle in self.source_map: - return self.source_map[handle].serialize() + return self.source_map[handle] return None def get_raw_repository_data(self, handle): if handle in self.repository_map: - return self.repository_map[handle].serialize() + return self.repository_map[handle] return None def get_raw_note_data(self, handle): if handle in self.note_map: - return self.note_map[handle].serialize() + return self.note_map[handle] return None def get_raw_place_data(self, handle): if handle in self.place_map: - return self.place_map[handle].serialize() + return self.place_map[handle] return None def get_raw_object_data(self, handle): if handle in self.media_map: - return self.media_map[handle].serialize() + return self.media_map[handle] return None def get_raw_tag_data(self, handle): if handle in self.tag_map: - return self.tag_map[handle].serialize() + return self.tag_map[handle] + return None + + def get_raw_event_data(self, handle): + if handle in self.event_map: + return self.event_map[handle] return None def add_person(self, person, trans, set_gid=True): @@ -923,61 +1141,169 @@ class DictionaryDb(DbWriteBase, DbReadBase): return obj.handle def commit_person(self, person, trans, change_time=None): - self.person_map[person.handle] = person + emit = None + if not trans.batch: + if person.handle in self.person_map: + emit = "person-update" + else: + emit = "person-add" + self.person_map[person.handle] = person.serialize() + self.person_id_map[person.gramps_id] = self.person_map[person.handle] + # Emit after added: + if emit: + self.emit(emit, ([person.handle],)) def commit_family(self, family, trans, change_time=None): - self.family_map[family.handle] = family + emit = None + if not trans.batch: + if family.handle in self.family_map: + emit = "family-update" + else: + emit = "family-add" + self.family_map[family.handle] = family.serialize() + self.family_id_map[family.gramps_id] = self.family_map[family.handle] + # Emit after added: + if emit: + self.emit(emit, ([family.handle],)) def commit_citation(self, citation, trans, change_time=None): - self.citation_map[citation.handle] = citation + emit = None + if not trans.batch: + if citation.handle in self.citation_map: + emit = "citation-update" + else: + emit = "citation-add" + self.citation_map[citation.handle] = citation.serialize() + self.citation_id_map[citation.gramps_id] = self.citation_map[citation.handle] + # Emit after added: + if emit: + self.emit(emit, ([citation.handle],)) def commit_source(self, source, trans, change_time=None): - self.source_map[source.handle] = source + emit = None + if not trans.batch: + if source.handle in self.source_map: + emit = "source-update" + else: + emit = "source-add" + self.source_map[source.handle] = source.serialize() + self.source_id_map[source.gramps_id] = self.source_map[source.handle] + # Emit after added: + if emit: + self.emit(emit, ([source.handle],)) def commit_repository(self, repository, trans, change_time=None): - self.repository_map[repository.handle] = repository + emit = None + if not trans.batch: + if repository.handle in self.repository_map: + emit = "repository-update" + else: + emit = "repository-add" + self.repository_map[repository.handle] = repository.serialize() + self.repository_id_map[repository.gramps_id] = self.repository_map[repository.handle] + # Emit after added: + if emit: + self.emit(emit, ([repository.handle],)) def commit_note(self, note, trans, change_time=None): - self.note_map[note.handle] = note + emit = None + if not trans.batch: + if note.handle in self.note_map: + emit = "note-update" + else: + emit = "note-add" + self.note_map[note.handle] = note.serialize() + self.note_id_map[note.gramps_id] = self.note_map[note.handle] + # Emit after added: + if emit: + self.emit(emit, ([note.handle],)) def commit_place(self, place, trans, change_time=None): - self.place_map[place.handle] = place + emit = None + if not trans.batch: + if place.handle in self.place_map: + emit = "place-update" + else: + emit = "place-add" + self.place_map[place.handle] = place.serialize() + self.place_id_map[place.gramps_id] = self.place_map[place.handle] + # Emit after added: + if emit: + self.emit(emit, ([place.handle],)) def commit_event(self, event, trans, change_time=None): - self.event_map[event.handle] = event + emit = None + if not trans.batch: + if event.handle in self.event_map: + emit = "event-update" + else: + emit = "event-add" + self.event_map[event.handle] = event.serialize() + self.event_id_map[event.gramps_id] = self.event_map[event.handle] + # Emit after added: + if emit: + self.emit(emit, ([event.handle],)) def commit_tag(self, tag, trans, change_time=None): - self.tag_map[tag.handle] = tag + emit = None + if not trans.batch: + if tag.handle in self.tag_map: + emit = "tag-update" + else: + emit = "tag-add" + self.tag_map[tag.handle] = tag.serialize() + # Emit after added: + if emit: + self.emit(emit, ([tag.handle],)) - def commit_media_object(self, obj, transaction, change_time=None): - self.media_map[obj.handle] = obj + def commit_media_object(self, media, trans, change_time=None): + emit = None + if not trans.batch: + if media.handle in self.media_map: + emit = "media-update" + else: + emit = "media-add" + self.media_map[media.handle] = media.serialize() + self.media_id_map[media.gramps_id] = self.media_map[media.handle] + # Emit after added: + if emit: + self.emit(emit, ([media.handle],)) def get_gramps_ids(self, obj_key): key2table = { - PERSON_KEY: self.person_map, - FAMILY_KEY: self.family_map, - CITATION_KEY: self.citation_map, - SOURCE_KEY: self.source_map, - EVENT_KEY: self.event_map, - MEDIA_KEY: self.media_map, - PLACE_KEY: self.place_map, - REPOSITORY_KEY: self.repository_map, - NOTE_KEY: self.note_map, + PERSON_KEY: self.person_id_map, + FAMILY_KEY: self.family_id_map, + CITATION_KEY: self.citation_id_map, + SOURCE_KEY: self.source_id_map, + EVENT_KEY: self.event_id_map, + MEDIA_KEY: self.media_id_map, + PLACE_KEY: self.place_id_map, + REPOSITORY_KEY: self.repository_id_map, + NOTE_KEY: self.note_id_map, } - table = key2table[obj_key] - return [item.gramps_id for item in table.values()] + return list(key2table[obj_key].keys()) def transaction_begin(self, transaction): + ## FIXME return - def disable_signals(self): - pass - def set_researcher(self, owner): - pass + self.owner.set_from(owner) + + def get_researcher(self): + return self.owner def request_rebuild(self): - pass + self.emit('person-rebuild') + self.emit('family-rebuild') + self.emit('place-rebuild') + self.emit('source-rebuild') + self.emit('citation-rebuild') + self.emit('media-rebuild') + self.emit('event-rebuild') + self.emit('repository-rebuild') + self.emit('note-rebuild') + self.emit('tag-rebuild') def copy_from_db(self, db): """ @@ -1034,21 +1360,11 @@ class DictionaryDb(DbWriteBase, DbReadBase): if self.readonly or not handle: return - person = self.get_person_from_handle(handle) - #self.genderStats.uncount_person (person) - #self.remove_from_surname_list(person) - if isinstance(handle, str): - handle = handle.encode('utf-8') - if transaction.batch: - with BSDDBTxn(self.env, self.person_map) as txn: - self.delete_primary_from_reference_map(handle, transaction, - txn=txn.txn) - txn.delete(handle) - else: - self.delete_primary_from_reference_map(handle, transaction, - txn=self.txn) - self.person_map.delete(handle, txn=self.txn) - transaction.add(PERSON_KEY, TXNDEL, handle, person.serialize(), None) + if handle in self.person_map: + person = Person.create(self.person_map[handle]) + del self.person_map[handle] + del self.person_id_map[person.gramps_id] + self.emit("person-delete", ([handle],)) def remove_source(self, handle, transaction): """ @@ -1056,7 +1372,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.source_map, - SOURCE_KEY) + self.source_id_map, SOURCE_KEY) def remove_citation(self, handle, transaction): """ @@ -1064,7 +1380,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.citation_map, - CITATION_KEY) + self.citation_id_map, CITATION_KEY) def remove_event(self, handle, transaction): """ @@ -1072,7 +1388,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.event_map, - EVENT_KEY) + self.event_id_map, EVENT_KEY) def remove_object(self, handle, transaction): """ @@ -1080,7 +1396,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.media_map, - MEDIA_KEY) + self.media_id_map, MEDIA_KEY) def remove_place(self, handle, transaction): """ @@ -1088,7 +1404,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.place_map, - PLACE_KEY) + self.place_id_map, PLACE_KEY) def remove_family(self, handle, transaction): """ @@ -1096,7 +1412,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.family_map, - FAMILY_KEY) + self.family_id_map, FAMILY_KEY) def remove_repository(self, handle, transaction): """ @@ -1104,7 +1420,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.repository_map, - REPOSITORY_KEY) + self.repository_id_map, REPOSITORY_KEY) def remove_note(self, handle, transaction): """ @@ -1112,7 +1428,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.note_map, - NOTE_KEY) + self.note_id_map, NOTE_KEY) def remove_tag(self, handle, transaction): """ @@ -1120,7 +1436,7 @@ class DictionaryDb(DbWriteBase, DbReadBase): database, preserving the change in the passed transaction. """ self.__do_remove(handle, transaction, self.tag_map, - TAG_KEY) + None, TAG_KEY) def is_empty(self): """ @@ -1131,23 +1447,15 @@ class DictionaryDb(DbWriteBase, DbReadBase): return False return True - def __do_remove(self, handle, transaction, data_map, key): + def __do_remove(self, handle, transaction, data_map, data_id_map, key): if self.readonly or not handle: return - - if isinstance(handle, str): - handle = handle.encode('utf-8') - if transaction.batch: - with BSDDBTxn(self.env, data_map) as txn: - self.delete_primary_from_reference_map(handle, transaction, - txn=txn.txn) - txn.delete(handle) - else: - self.delete_primary_from_reference_map(handle, transaction, - txn=self.txn) - old_data = data_map.get(handle, txn=self.txn) - data_map.delete(handle, txn=self.txn) - transaction.add(key, TXNDEL, handle, old_data, None) + if handle in data_map: + obj = self._tables[KEY_TO_CLASS_MAP[key]]["class_func"].create(data_map[handle]) + del data_map[handle] + if data_id_map: + del data_id_map[obj.gramps_id] + self.emit(KEY_TO_NAME_MAP[key] + "-delete", ([handle],)) def delete_primary_from_reference_map(self, handle, transaction, txn=None): """ @@ -1206,3 +1514,318 @@ class DictionaryDb(DbWriteBase, DbReadBase): transaction.add(REFERENCE_KEY, TXNDEL, key, old_data, None) #transaction.reference_del.append(str(key)) self.reference_map.delete(key, txn=txn) + + ## Missing: + + def backup(self): + ## FIXME + pass + + def close(self): + if self._directory: + from gramps.plugins.export.exportxml import XmlWriter + from gramps.cli.user import User + writer = XmlWriter(self, User(), strip_photos=0, compress=1) + filename = os.path.join(self._directory, "data.gramps") + writer.write(filename) + filename = os.path.join(self._directory, "meta_data.db") + touch(filename) + + def find_backlink_handles(self, handle, include_classes=None): + ## FIXME + return [] + + def find_initial_person(self): + items = self.person_map.keys() + if len(items) > 0: + return self.get_person_from_handle(list(items)[0]) + return None + + def find_place_child_handles(self, handle): + ## FIXME + return [] + + def get_bookmarks(self): + return self.bookmarks + + def get_child_reference_types(self): + ## FIXME + return [] + + def get_citation_bookmarks(self): + return self.citation_bookmarks + + def get_cursor(self, table, txn=None, update=False, commit=False): + ## FIXME + ## called from a complete find_back_ref + pass + + # cursors for lookups in the reference_map for back reference + # lookups. The reference_map has three indexes: + # the main index: a tuple of (primary_handle, referenced_handle) + # the primary_handle index: the primary_handle + # the referenced_handle index: the referenced_handle + # the main index is unique, the others allow duplicate entries. + + def get_default_handle(self): + items = self.person_map.keys() + if len(items) > 0: + return list(items)[0] + return None + + def get_event_attribute_types(self): + ## FIXME + return [] + + def get_event_bookmarks(self): + return self.event_bookmarks + + def get_event_roles(self): + ## FIXME + return [] + + def get_event_types(self): + ## FIXME + return [] + + def get_family_attribute_types(self): + ## FIXME + return [] + + def get_family_bookmarks(self): + return self.family_bookmarks + + def get_family_event_types(self): + ## FIXME + return [] + + def get_family_relation_types(self): + ## FIXME + return [] + + def get_media_attribute_types(self): + ## FIXME + return [] + + def get_media_bookmarks(self): + return self.media_bookmarks + + def get_name_types(self): + ## FIXME + return [] + + def get_note_bookmarks(self): + return self.note_bookmarks + + def get_note_types(self): + ## FIXME + return [] + + def get_origin_types(self): + ## FIXME + return [] + + def get_person_attribute_types(self): + ## FIXME + return [] + + def get_person_event_types(self): + ## FIXME + return [] + + def get_place_bookmarks(self): + return self.place_bookmarks + + def get_place_tree_cursor(self): + ## FIXME + return [] + + def get_place_types(self): + ## FIXME + return [] + + def get_repo_bookmarks(self): + return self.repo_bookmarks + + def get_repository_types(self): + ## FIXME + return [] + + def get_save_path(self): + return self._directory + + def get_source_attribute_types(self): + ## FIXME + return [] + + def get_source_bookmarks(self): + return self.source_bookmarks + + def get_source_media_types(self): + ## FIXME + return [] + + def get_surname_list(self): + ## FIXME + return [] + + def get_url_types(self): + ## FIXME + return [] + + def has_changed(self): + ## FIXME + return True + + def is_open(self): + return self._directory is not None + + def iter_citation_handles(self): + return (key for key in self.citation_map.keys()) + + def iter_citations(self): + return (Citation.create(key) for key in self.citation_map.values()) + + def iter_event_handles(self): + return (key for key in self.event_map.keys()) + + def iter_events(self): + return (Event.create(key) for key in self.event_map.values()) + + def iter_media_objects(self): + return (MediaObject.create(key) for key in self.media_map.values()) + + def iter_note_handles(self): + return (key for key in self.note_map.keys()) + + def iter_notes(self): + return (Note.create(key) for key in self.note_map.values()) + + def iter_place_handles(self): + return (key for key in self.place_map.keys()) + + def iter_places(self): + return (Place.create(key) for key in self.place_map.values()) + + def iter_repositories(self): + return (Repository.create(key) for key in self.repositories_map.values()) + + def iter_repository_handles(self): + return (key for key in self.repositories_map.keys()) + + def iter_source_handles(self): + return (key for key in self.source_map.keys()) + + def iter_sources(self): + return (Source.create(key) for key in self.source_map.values()) + + def iter_tag_handles(self): + return (key for key in self.tag_map.keys()) + + def iter_tags(self): + return (Tag.create(key) for key in self.tag_map.values()) + + def load(self, directory, callback=None, mode=None, + force_schema_upgrade=False, + force_bsddb_upgrade=False, + force_bsddb_downgrade=False, + force_python_upgrade=False): + from gramps.plugins.importer.importxml import importData + from gramps.cli.user import User + self._directory = directory + self.full_name = os.path.abspath(self._directory) + self.path = self.full_name + self.brief_name = os.path.basename(self._directory) + filename = os.path.join(directory, "data.gramps") + if os.path.isfile(filename): + importData(self, filename, User()) + + def redo(self, update_history=True): + ## FIXME + pass + + def restore(self): + ## FIXME + pass + + def set_prefixes(self, person, media, family, source, citation, + place, event, repository, note): + ## FIXME + pass + + def set_save_path(self, directory): + self._directory = directory + self.full_name = os.path.abspath(self._directory) + self.path = self.full_name + self.brief_name = os.path.basename(self._directory) + + def undo(self, update_history=True): + ## FIXME + pass + + def write_version(self, directory): + """Write files for a newly created DB.""" + versionpath = os.path.join(directory, str(DBBACKEND)) + _LOG.debug("Write database backend file to 'dictionarydb'") + with open(versionpath, "w") as version_file: + version_file.write("dictionarydb") + + def report_bm_change(self): + """ + Add 1 to the number of bookmark changes during this session. + """ + self._bm_changes += 1 + + def db_has_bm_changes(self): + """ + Return whethere there were bookmark changes during the session. + """ + return self._bm_changes > 0 + + def get_summary(self): + """ + Returns dictionary of summary item. + Should include, if possible: + + _("Number of people") + _("Version") + _("Schema version") + """ + return { + _("Number of people"): self.get_number_of_people(), + } + + def get_dbname(self): + """ + In DictionaryDb, the database is in a text file at the path + """ + filepath = os.path.join(self._directory, "name.txt") + try: + name_file = open(filepath, "r") + name = name_file.readline().strip() + name_file.close() + except (OSError, IOError) as msg: + _LOG.error(str(msg)) + name = None + return name + + def reindex_reference_map(self): + ## FIXME + pass + + def rebuild_secondary(self, update): + ## FIXME + pass + + def prepare_import(self): + """ + Initialization before imports + """ + pass + + def commit_import(self): + """ + Post process after imports + """ + pass + diff --git a/gramps/plugins/gramplet/leak.py b/gramps/plugins/gramplet/leak.py index 2e30e7582..3c10a1681 100644 --- a/gramps/plugins/gramplet/leak.py +++ b/gramps/plugins/gramplet/leak.py @@ -31,7 +31,6 @@ Show uncollected objects in a window. #------------------------------------------------------------------------ from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext -from bsddb3.db import DBError #------------------------------------------------------------------------ # @@ -155,6 +154,13 @@ class Leak(Gramplet): parent=self.uistate.window) def display(self): + try: + from bsddb3.db import DBError + except: + class DBError(Exception): + """ + Dummy. + """ gc.collect(2) self.model.clear() count = 0 diff --git a/gramps/plugins/importer/importcsv.py b/gramps/plugins/importer/importcsv.py index 4c1e61764..44f7d29ed 100644 --- a/gramps/plugins/importer/importcsv.py +++ b/gramps/plugins/importer/importcsv.py @@ -103,8 +103,10 @@ def importData(dbase, filename, user): parser = CSVParser(dbase, user, (config.get('preferences.tag-on-import-format') if config.get('preferences.tag-on-import') else None)) try: + dbase.prepare_import() with open(filename, 'r') as filehandle: parser.parse(filehandle) + dbase.commit_import() except EnvironmentError as err: user.notify_error(_("%s could not be opened\n") % filename, str(err)) return diff --git a/gramps/plugins/importer/importgedcom.py b/gramps/plugins/importer/importgedcom.py index 5f095674a..34a1d4876 100644 --- a/gramps/plugins/importer/importgedcom.py +++ b/gramps/plugins/importer/importgedcom.py @@ -131,7 +131,9 @@ def importData(database, filename, user): try: read_only = database.readonly database.readonly = False + database.prepare_import() gedparse.parse_gedcom_file(False) + database.commit_import() database.readonly = read_only ifile.close() except IOError as msg: diff --git a/gramps/plugins/importer/importgeneweb.py b/gramps/plugins/importer/importgeneweb.py index 334cfac7c..f3c23e074 100644 --- a/gramps/plugins/importer/importgeneweb.py +++ b/gramps/plugins/importer/importgeneweb.py @@ -154,7 +154,9 @@ def importData(database, filename, user): return try: + database.prepare_import() status = g.parse_geneweb_file() + database.commit_import() except IOError as msg: errmsg = _("%s could not be opened\n") % filename user.notify_error(errmsg,str(msg)) diff --git a/gramps/plugins/importer/importgpkg.py b/gramps/plugins/importer/importgpkg.py index 3c7f1b4c7..2a164b09d 100644 --- a/gramps/plugins/importer/importgpkg.py +++ b/gramps/plugins/importer/importgpkg.py @@ -93,7 +93,9 @@ def impData(database, name, user): imp_db_name = os.path.join(tmpdir_path, XMLFILE) importer = importData + database.prepare_import() info = importer(database, imp_db_name, user) + database.commit_import() newmediapath = database.get_mediapath() #import of gpkg should not change media path as all media has new paths! diff --git a/gramps/plugins/importer/importprogen.py b/gramps/plugins/importer/importprogen.py index 16b72eeea..3ad119660 100644 --- a/gramps/plugins/importer/importprogen.py +++ b/gramps/plugins/importer/importprogen.py @@ -75,7 +75,9 @@ def _importData(database, filename, user): return try: + database.prepare_import() status = g.parse_progen_file() + database.commit_import() except ProgenError as msg: user.notify_error(_("Pro-Gen data error"), str(msg)) return diff --git a/gramps/plugins/importer/importvcard.py b/gramps/plugins/importer/importvcard.py index 9495afb5f..e9f42d44b 100644 --- a/gramps/plugins/importer/importvcard.py +++ b/gramps/plugins/importer/importvcard.py @@ -63,8 +63,10 @@ def importData(database, filename, user): """Function called by Gramps to import data on persons in VCard format.""" parser = VCardParser(database) try: + database.prepare_import() with OpenFileOrStdin(filename) as filehandle: parser.parse(filehandle) + database.commit_import() except EnvironmentError as msg: user.notify_error(_("%s could not be opened\n") % filename, str(msg)) return diff --git a/gramps/plugins/importer/importxml.py b/gramps/plugins/importer/importxml.py index 00aa28420..acee74f69 100644 --- a/gramps/plugins/importer/importxml.py +++ b/gramps/plugins/importer/importxml.py @@ -57,7 +57,7 @@ from gramps.gen.lib import (Address, Attribute, AttributeType, ChildRef, SrcAttribute, SrcAttributeType, StyledText, StyledTextTag, StyledTextTagType, Surname, Tag, Url) from gramps.gen.db import DbTxn -from gramps.gen.db.write import CLASS_TO_KEY_MAP +#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 @@ -68,7 +68,7 @@ from gramps.gen.display.name import displayer as name_displayer from gramps.gen.db.dbconst import (PERSON_KEY, FAMILY_KEY, SOURCE_KEY, EVENT_KEY, MEDIA_KEY, PLACE_KEY, REPOSITORY_KEY, NOTE_KEY, TAG_KEY, - CITATION_KEY) + CITATION_KEY, CLASS_TO_KEY_MAP) from gramps.gen.updatecallback import UpdateCallback from gramps.version import VERSION from gramps.gen.config import config @@ -122,6 +122,7 @@ def importData(database, filename, user): line_cnt = 0 person_cnt = 0 + database.prepare_import() with ImportOpenFileContextManager(filename, user) as xml_file: if xml_file is None: return @@ -162,6 +163,7 @@ def importData(database, filename, user): "valid Gramps database.")) return + database.commit_import() database.readonly = read_only return info diff --git a/gramps/webapp/reports.py b/gramps/webapp/reports.py index c0c7f480e..35cc66356 100644 --- a/gramps/webapp/reports.py +++ b/gramps/webapp/reports.py @@ -85,9 +85,9 @@ def import_file(db, filename, user): print("ERROR:", name, exception) return False import_function = getattr(mod, pdata.import_function) - db.prepare_import() + #db.prepare_import() retval = import_function(db, filename, user) - db.commit_import() + #db.commit_import() return retval return False diff --git a/gramps/webapp/utils.py b/gramps/webapp/utils.py index 07eddd341..10e0aaf38 100644 --- a/gramps/webapp/utils.py +++ b/gramps/webapp/utils.py @@ -143,8 +143,12 @@ def get_person_from_handle(db, handle): return None def probably_alive(handle): + ## FIXME: need to call after save? person = db.get_person_from_handle(handle) - return alive(person, db) + if person: + return alive(person, db) + else: + return True def format_number(number, with_grouping=True): if number != "":