gramps/src/DbManager.py
2009-01-14 03:09:03 +00:00

1165 lines
39 KiB
Python

#
# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# $Id$
"""
Provide the management of databases. This includes opening, renaming,
creating, and deleting of databases.
"""
#-------------------------------------------------------------------------
#
# Standard python modules
#
#-------------------------------------------------------------------------
import os
import time
import copy
import subprocess
from gettext import gettext as _
#-------------------------------------------------------------------------
#
# set up logging
#
#-------------------------------------------------------------------------
import logging
LOG = logging.getLogger(".DbManager")
if os.sys.platform == "win32":
_RCS_FOUND = os.system("rcs -V >nul 2>nul") == 0
if _RCS_FOUND and "TZ" not in os.environ:
# RCS requires the "TZ" variable be set.
os.environ["TZ"] = str(time.timezone)
else:
_RCS_FOUND = os.system("rcs -V >/dev/null 2>/dev/null") == 0
#-------------------------------------------------------------------------
#
# GTK/Gnome modules
#
#-------------------------------------------------------------------------
import gtk
from gtk import glade
from gtk.gdk import ACTION_COPY
import pango
#-------------------------------------------------------------------------
#
# gramps modules
#
#-------------------------------------------------------------------------
import const
from QuestionDialog import ErrorDialog, QuestionDialog
import gen.db
from gen.plug import PluginManager
import GrampsDbUtils
import Config
import Mime
from DdTargets import DdTargets
import RecentFiles
_RETURN = gtk.gdk.keyval_from_name("Return")
_KP_ENTER = gtk.gdk.keyval_from_name("KP_Enter")
#-------------------------------------------------------------------------
#
# constants
#
#-------------------------------------------------------------------------
DEFAULT_TITLE = _("Family Tree")
NAME_FILE = "name.txt"
META_NAME = "meta_data.db"
ARCHIVE = "rev.gramps"
ARCHIVE_V = "rev.gramps,v"
NAME_COL = 0
PATH_COL = 1
FILE_COL = 2
DATE_COL = 3
DSORT_COL = 4
OPEN_COL = 5
STOCK_COL = 6
RCS_BUTTON = { True : _('_Extract'), False : _('_Archive') }
class CLIDbManager:
"""
Database manager without GTK functionality, allows users to create and
open databases
"""
def __init__(self, dbstate):
self.dbstate = dbstate
self.msg = None
if dbstate:
self.active = dbstate.db.get_save_path()
else:
self.active = None
self.current_names = []
self._populate_cli()
def empty(self, val):
"""Callback that does nothing
"""
pass
def get_dbdir_summary(self, file_name):
"""
Returns (people_count, version_number) of current DB.
Returns ("Unknown", "Unknown") if invalid DB or other error.
"""
from bsddb import dbshelve, db
from gen.db import META, PERSON_TBL
env = db.DBEnv()
flags = db.DB_CREATE | db.DB_PRIVATE |\
db.DB_INIT_MPOOL | db.DB_INIT_LOCK |\
db.DB_INIT_LOG | db.DB_INIT_TXN | db.DB_THREAD
try:
env.open(file_name, flags)
except:
return "Unknown", "Unknown"
dbmap1 = dbshelve.DBShelf(env)
fname = os.path.join(file_name, META + ".db")
try:
dbmap1.open(fname, META, db.DB_HASH, db.DB_RDONLY)
except:
return "Unknown", "Unknown"
version = dbmap1.get('version', default=None)
dbmap1.close()
dbmap2 = dbshelve.DBShelf(env)
fname = os.path.join(file_name, PERSON_TBL + ".db")
try:
dbmap2.open(fname, PERSON_TBL, db.DB_HASH, db.DB_RDONLY)
except:
env.close()
return "Unknown", "Unknown"
count = len(dbmap2)
dbmap2.close()
env.close()
return (count, version)
def family_tree_summary(self):
"""
Return a list of dictionaries of the known family trees.
"""
# make the default directory if it does not exist
list = []
for item in self.current_names:
(name, dirpath, path_name, last,
tval, enable, stock_id) = item
count, version = self.get_dbdir_summary(dirpath)
retval = {}
retval["Number of people"] = count
if enable:
retval["Locked?"] = "yes"
else:
retval["Locked?"] = "no"
retval["DB version"] = version
retval["Family tree"] = name
retval["Path"] = dirpath
retval["Last accessed"] = time.strftime('%x %X',
time.localtime(tval))
list.append( retval )
return list
def _populate_cli(self):
""" Get the list of current names in the database dir
"""
# make the default directory if it does not exist
dbdir = os.path.expanduser(Config.get(Config.DATABASE_PATH))
make_dbdir(dbdir)
self.current_names = []
for dpath in os.listdir(dbdir):
dirpath = os.path.join(dbdir, dpath)
path_name = os.path.join(dirpath, NAME_FILE)
if os.path.isfile(path_name):
name = file(path_name).readline().strip()
(tval, last) = time_val(dirpath)
(enable, stock_id) = icon_values(dirpath, self.active,
self.dbstate.db.is_open())
if (stock_id == 'gramps-lock'):
last = find_locker_name(dirpath)
self.current_names.append(
(name, os.path.join(dbdir, dpath), path_name,
last, tval, enable, stock_id))
self.current_names.sort()
def get_family_tree_path(self, name):
"""
Given a name, return None if name not existing or the path to the
database if it is a known database name.
"""
for data in self.current_names:
if data[0] == name:
return data[1]
return None
def family_tree_list(self):
"""Return a list of name, dirname of the known family trees
"""
lst = [(x[0], x[1]) for x in self.current_names]
return lst
def __start_cursor(self, msg):
"""
Do needed things to start import visually, eg busy cursor
"""
print _('Starting Import, %s') % msg
def __end_cursor(self):
"""
Set end of a busy cursor
"""
print _('Import finished...')
def _create_new_db_cli(self, title=None):
"""
Create a new database.
"""
new_path = find_next_db_dir()
os.mkdir(new_path)
path_name = os.path.join(new_path, NAME_FILE)
if title is None:
name_list = [ name[0] for name in self.current_names ]
title = find_next_db_name(name_list)
name_file = open(path_name, "w")
name_file.write(title)
name_file.close()
# write the version number into metadata
newdb = gen.db.GrampsDBDir()
newdb.write_version(new_path)
(tval, last) = time_val(new_path)
self.current_names.append((title, new_path, path_name,
last, tval, False, ""))
return new_path, title
def _create_new_db(self, title=None):
"""
Create a new database, do extra stuff needed
"""
return self._create_new_db_cli(title)
def import_new_db(self, filename, callback):
"""
Attempt to import the provided file into a new database.
A new database will only be created if an appropriate importer was
found.
@return: A tuple of (new_path, name) for the new database
or (None, None) if no import was performed.
"""
pmgr = PluginManager.get_instance()
(name, ext) = os.path.splitext(os.path.basename(filename))
format = ext[1:].lower()
for plugin in pmgr.get_import_plugins():
if format == plugin.get_extension():
new_path, name = self._create_new_db(name)
# Create a new database
self.__start_cursor(_("Importing data..."))
dbclass = gen.db.GrampsDBDir
dbase = dbclass()
dbase.load(new_path, callback)
import_function = plugin.get_import_function()
import_function(dbase, filename, callback)
# finish up
self.__end_cursor()
dbase.close()
return new_path, name
return None, None
def is_locked(self, dbpath):
"""
returns True if there is a lock file in the dirpath
"""
if os.path.isfile(os.path.join(dbpath,"lock")):
return True
return False
def needs_recovery(self, dbpath):
"""
returns True if the database in dirpath needs recovery
"""
if os.path.isfile(os.path.join(dbpath,"need_recover")):
return True
return False
def break_lock(self, dbpath):
"""
Breaks the lock on a database
"""
if os.path.exists(os.path.join(dbpath, "lock")):
os.unlink(os.path.join(dbpath, "lock"))
class DbManager(CLIDbManager):
"""
Database Manager. Opens a database manager window that allows users to
create, rename, delete and open databases.
"""
def __init__(self, dbstate, parent=None):
"""
Create the top level window from the glade description, and extracts
the GTK widgets that are needed.
"""
CLIDbManager.__init__(self, dbstate)
self.glade = glade.XML(const.GLADE_FILE, "dbmanager", "gramps")
self.top = self.glade.get_widget('dbmanager')
if parent:
self.top.set_transient_for(parent)
self.connect = self.glade.get_widget('connect')
self.cancel = self.glade.get_widget('cancel')
self.new = self.glade.get_widget('new')
self.remove = self.glade.get_widget('remove')
self.dblist = self.glade.get_widget('dblist')
self.rename = self.glade.get_widget('rename')
self.repair = self.glade.get_widget('repair')
self.rcs = self.glade.get_widget('rcs')
self.msg = self.glade.get_widget('msg')
self.model = None
self.column = None
self.lock_file = None
self.data_to_delete = None
self.selection = self.dblist.get_selection()
self.dblist.set_rules_hint(True)
self.__connect_signals()
self.__build_interface()
self._populate_model()
def __connect_signals(self):
"""
Connects the signals to the buttons on the interface.
"""
ddtargets = [ DdTargets.URI_LIST.target() ]
self.top.drag_dest_set(gtk.DEST_DEFAULT_ALL, ddtargets, ACTION_COPY)
self.remove.connect('clicked', self.__remove_db)
self.new.connect('clicked', self.__new_db)
self.rename.connect('clicked', self.__rename_db)
self.repair.connect('clicked', self.__repair_db)
self.selection.connect('changed', self.__selection_changed)
self.dblist.connect('button-press-event', self.__button_press)
self.dblist.connect('key-press-event', self.__key_press)
self.top.connect('drag_data_received', self.__drag_data_received)
self.top.connect('drag_motion', drag_motion)
self.top.connect('drag_drop', drop_cb)
if _RCS_FOUND:
self.rcs.connect('clicked', self.__rcs)
def __button_press(self, obj, event):
"""
Checks for a double click event. In the tree view, we want to
treat a double click as if it was OK button press. However, we have
to make sure that an item was selected first.
"""
if event.type == gtk.gdk._2BUTTON_PRESS and event.button == 1:
if self.connect.get_property('sensitive'):
self.top.response(gtk.RESPONSE_OK)
return True
return False
def __key_press(self, obj, event):
"""
Grab ENTER so it does not start editing the cell, but behaves
like double click instead
"""
if not event.state or event.state in (gtk.gdk.MOD2_MASK,):
if event.keyval in (_RETURN, _KP_ENTER):
if self.connect.get_property('sensitive'):
self.top.response(gtk.RESPONSE_OK)
return True
return False
def __selection_changed(self, selection):
"""
Called when the selection is changed in the TreeView.
"""
self.__update_buttons(selection)
def __update_buttons(self, selection):
"""
What we are trying to detect is the selection or unselection of a row.
When a row is unselected, the Open, Rename, and Remove buttons
are set insensitive. If a row is selected, the rename and remove
buttons are disabled, and the Open button is disabled if the
row represents a open database.
"""
# Get the current selection
store, node = selection.get_selected()
# if nothing is selected
if not node:
self.connect.set_sensitive(False)
self.rename.set_sensitive(False)
self.rcs.set_sensitive(False)
self.repair.set_sensitive(False)
self.remove.set_sensitive(False)
return
path = self.model.get_path(node)
if path is None:
return
is_rev = len(path) > 1
self.rcs.set_label(RCS_BUTTON[is_rev])
if store.get_value(node, STOCK_COL) == gtk.STOCK_OPEN:
self.connect.set_sensitive(False)
if _RCS_FOUND:
self.rcs.set_sensitive(True)
else:
self.connect.set_sensitive(not is_rev)
if _RCS_FOUND and is_rev:
self.rcs.set_sensitive(True)
else:
self.rcs.set_sensitive(False)
if store.get_value(node, STOCK_COL) == gtk.STOCK_DIALOG_ERROR:
path = store.get_value(node, PATH_COL)
backup = os.path.join(path, "person.gbkp")
self.repair.set_sensitive(os.path.isfile(backup))
else:
self.repair.set_sensitive(False)
self.rename.set_sensitive(True)
self.remove.set_sensitive(True)
def __build_interface(self):
"""
Builds the columns for the TreeView. The columns are:
Icon, Database Name, Last Modified
The Icon column gets its data from column 6 of the database model.
It is expecting either None, or a GTK stock icon name
The Database Name column is an editable column. We connect to the
'edited' signal, so that we can change the name when the user changes
the column.
The last modified column simply displays the last modification time.
"""
# build the database name column
render = gtk.CellRendererText()
render.set_property('ellipsize', pango.ELLIPSIZE_END)
render.connect('edited', self.__change_name)
render.connect('editing-canceled', self.__stop_edit)
render.connect('editing-started', self.__start_edit)
self.column = gtk.TreeViewColumn(_('Family tree name'), render,
text=NAME_COL)
self.column.set_sort_column_id(NAME_COL)
self.column.set_resizable(True)
self.column.set_min_width(275)
self.dblist.append_column(self.column)
self.name_renderer = render
# build the icon column
render = gtk.CellRendererPixbuf()
icon_column = gtk.TreeViewColumn(_('Status'), render,
stock_id=STOCK_COL)
self.dblist.append_column(icon_column)
# build the last modified cocolumn
render = gtk.CellRendererText()
column = gtk.TreeViewColumn(_('Last modified'), render, text=DATE_COL)
column.set_sort_column_id(DSORT_COL)
self.dblist.append_column(column)
# set the rules hit
self.dblist.set_rules_hint(True)
def __populate(self):
"""
Builds the data and the display model.
"""
self._populate_cli()
self._populate_model()
def _populate_model(self):
"""
Builds the display model.
"""
self.model = gtk.TreeStore(str, str, str, str, int, bool, str)
#use current names to set up the model
for items in self.current_names:
data = [items[0], items[1], items[2], items[3],
items[4], items[5], items[6]]
node = self.model.append(None, data)
for rdata in find_revisions(os.path.join(items[1], ARCHIVE_V)):
data = [ rdata[2], rdata[0], items[1], rdata[1], 0, False, "" ]
self.model.append(node, data)
self.dblist.set_model(self.model)
def existing_name(self, name, skippath=None):
"""
Return true if a name is present in the model already.
If skippath given, the name of skippath is not considered
"""
iter = self.model.get_iter_first()
while (iter):
path = self.model.get_path(iter)
if path == skippath:
continue
itername = self.model.get_value(iter, NAME_COL)
if itername.strip() == name.strip():
return True
iter = self.model.iter_next(iter)
return False
def run(self):
"""
Runs the dialog, returning None if nothing has been chosen,
or the path and name if something has been selected
"""
while True:
value = self.top.run()
if value == gtk.RESPONSE_OK:
store, node = self.selection.get_selected()
# don't open a locked file
if store.get_value(node, STOCK_COL) == 'gramps-lock':
self.__ask_to_break_lock(store, node)
continue
# don't open a version
if len(store.get_path(node)) > 1:
continue
if node:
self.top.destroy()
return (store.get_value(node, PATH_COL),
store.get_value(node, NAME_COL))
else:
self.top.destroy()
return None
def __ask_to_break_lock(self, store, node):
"""
Prompts the user for permission to break the lock file that another
process has set on the file.
"""
path = store.get_path(node)
self.lock_file = store[path][PATH_COL]
QuestionDialog(
_("Break the lock on the '%s' database?") % store[path][0],
_("GRAMPS believes that someone else is actively editing "
"this database. You cannot edit this database while it "
"is locked. If no one is editing the database you may "
"safely break the lock. However, if someone else is editing "
"the database and you break the lock, you may corrupt the "
"database."),
_("Break lock"),
self.__really_break_lock)
def __really_break_lock(self):
"""
Deletes the lock file associated with the selected database, then updates
the display appropriately.
"""
try:
self.break_lock(self.lock_file)
store, node = self.selection.get_selected()
dbpath = store.get_value(node, PATH_COL)
(tval, last) = time_val(dbpath)
store.set_value(node, OPEN_COL, 0)
store.set_value(node, STOCK_COL, "")
store.set_value(node, DATE_COL, last)
store.set_value(node, DSORT_COL, tval)
except IOError:
return
def __stop_edit(self, *args):
self.name_renderer.set_property('editable', False)
self.__update_buttons(self.selection)
def __start_edit(self, *args):
"""
Do no allow to click Load while changing name, to force users to finish
the action of renaming. Hack around the fact that clicking button
sends a 'editing-canceled' signal loosing the new name
"""
self.connect.set_sensitive(False)
self.rename.set_sensitive(False)
def __change_name(self, renderer_sel, path, new_text):
"""
Change the name of the database. This is a callback from the
column, which has been marked as editable.
If the new string is empty, do nothing. Otherwise, renaming the
database is simply changing the contents of the name file.
"""
if len(new_text) > 0:
node = self.model.get_iter(path)
old_text = self.model.get_value(node, NAME_COL)
if not old_text.strip() == new_text.strip():
#If there is a ":" in path, then it as revision
if ":" in path :
self.__rename_revision(path, new_text)
else:
self.__rename_database(path, new_text)
self.name_renderer.set_property('editable', False)
self.__update_buttons(self.selection)
def __rename_revision(self, path, new_text):
"""
Renames the RCS revision using the rcs command. The rcs command
is in the format of:
rcs -mREV:NEW_NAME archive
"""
node = self.model.get_iter(path)
db_dir = self.model.get_value(node, FILE_COL)
rev = self.model.get_value(node, PATH_COL)
archive = os.path.join(db_dir, ARCHIVE_V)
cmd = [ "rcs", "-x,v", "-m%s:%s" % (rev, new_text), archive ]
proc = subprocess.Popen(cmd, stderr = subprocess.PIPE)
status = proc.wait()
message = "\n".join(proc.stderr.readlines())
proc.stderr.close()
del proc
if status != 0:
ErrorDialog(
_("Rename failed"),
_("An attempt to rename a version failed "
"with the following message:\n\n%s") % message
)
else:
self.model.set_value(node, NAME_COL, new_text)
def __rename_database(self, path, new_text):
"""
Renames the database by writing the new value to the name.txt file
"""
new_text = new_text.strip()
node = self.model.get_iter(path)
filename = self.model.get_value(node, FILE_COL)
if self.existing_name(new_text, skippath=path):
ErrorDialog(
_("Could not rename the Family Tree."),
_("Family Tree already exists, choose a unique name."))
return
try:
name_file = open(filename, "r")
old_text=name_file.read()
name_file.close()
name_file = open(filename, "w")
name_file.write(new_text)
name_file.close()
RecentFiles.rename_filename(old_text, new_text)
self.model.set_value(node, NAME_COL, new_text)
except (OSError, IOError), msg:
ErrorDialog(
_("Could not rename family tree"),
str(msg))
def __rcs(self, obj):
"""
Callback for the RCS button. If the tree path is > 1, then we are
on an RCS revision, in which case we can check out. If not, then
we can only check in.
"""
store, node = self.selection.get_selected()
tree_path = store.get_path(node)
if len(tree_path) > 1:
parent_node = store.get_iter((tree_path[0],))
parent_name = store.get_value(parent_node, NAME_COL)
name = store.get_value(node, NAME_COL)
revision = store.get_value(node, PATH_COL)
db_path = store.get_value(node, FILE_COL)
self.__checkout_copy(parent_name, name, revision, db_path)
else:
base_path = self.dbstate.db.get_save_path()
archive = os.path.join(base_path, ARCHIVE)
check_in(self.dbstate.db, archive, None, self.__start_cursor)
self.__end_cursor()
self.__populate()
def __checkout_copy(self, parent_name, name, revision, db_path):
"""
Create a new database, then extracts a revision from RCS and
imports it into the db
"""
new_path, newname = self._create_new_db("%s : %s" % (parent_name, name))
self.__start_cursor(_("Extracting archive..."))
dbclass = gen.db.GrampsDBDir
dbase = dbclass()
dbase.load(new_path, None)
self.__start_cursor(_("Importing archive..."))
check_out(dbase, revision, db_path, None)
self.__end_cursor()
dbase.close()
def __remove_db(self, obj):
"""
Callback associated with the Remove button. Get the selected
row and data, then call the verification dialog.
"""
store, node = self.selection.get_selected()
path = store.get_path(node)
self.data_to_delete = store[path]
if len(path) == 1:
QuestionDialog(
_("Remove the '%s' family tree?") % self.data_to_delete[0],
_("Removing this family tree will permanently destroy the data."),
_("Remove family tree"),
self.__really_delete_db)
else:
rev = self.data_to_delete[0]
parent = store[(path[0],)][0]
QuestionDialog(
_("Remove the '%(revision)s' version of '%(database)s'") % {
'revision' : rev,
'database' : parent
},
_("Removing this version will prevent you from "
"extracting it in the future."),
_("Remove version"),
self.__really_delete_version)
def __really_delete_db(self):
"""
Delete the selected database. If the databse is open, close it first.
Then scan the database directory, deleting the files, and finally
removing the directory.
"""
# close the database if the user has requested to delete the
# active database
if self.data_to_delete[OPEN_COL]:
self.dbstate.no_database()
store, node = self.selection.get_selected()
path = store.get_path(node)
node = self.model.get_iter(path)
filename = self.model.get_value(node, FILE_COL)
try:
name_file = open(filename, "r")
file_name_to_delete=name_file.read()
name_file.close()
RecentFiles.remove_filename(file_name_to_delete)
for (top, dirs, files) in os.walk(self.data_to_delete[1]):
for filename in files:
os.unlink(os.path.join(top, filename))
os.rmdir(self.data_to_delete[1])
except (IOError, OSError), msg:
ErrorDialog(_("Could not delete family tree"),
str(msg))
# rebuild the display
self.__populate()
def __really_delete_version(self):
"""
Delete the selected database. If the databse is open, close it first.
Then scan the database directory, deleting the files, and finally
removing the directory.
"""
db_dir = self.data_to_delete[FILE_COL]
rev = self.data_to_delete[PATH_COL]
archive = os.path.join(db_dir, ARCHIVE_V)
cmd = [ "rcs", "-x,v", "-o%s" % rev, "-q", archive ]
proc = subprocess.Popen(cmd, stderr = subprocess.PIPE)
status = proc.wait()
message = "\n".join(proc.stderr.readlines())
proc.stderr.close()
del proc
if status != 0:
ErrorDialog(
_("Deletion failed"),
_("An attempt to delete a version failed "
"with the following message:\n\n%s") % message
)
# rebuild the display
self.__populate()
def __rename_db(self, obj):
"""
Start the rename process by calling the start_editing option on
the line with the cursor.
"""
store, node = self.selection.get_selected()
path = self.model.get_path(node)
self.name_renderer.set_property('editable', True)
self.dblist.set_cursor(path, focus_column=self.column,
start_editing=True)
def __repair_db(self, obj):
"""
Start the repair process by calling the start_editing option on
the line with the cursor.
"""
store, node = self.selection.get_selected()
dirname = store[node][1]
opened = store[node][5]
if opened:
self.dbstate.no_database()
# delete files that are not backup files or the .txt file
for filename in os.listdir(dirname):
if os.path.splitext(filename)[1] not in (".gbkp", ".txt"):
fname = os.path.join(dirname, filename)
os.unlink(fname)
newdb = gen.db.GrampsDBDir()
newdb.write_version(dirname)
dbclass = gen.db.GrampsDBDir
dbase = dbclass()
dbase.set_save_path(dirname)
dbase.load(dirname, None)
self.__start_cursor(_("Rebuilding database from backup files"))
GrampsDbUtils.Backup.restore(dbase)
self.__end_cursor()
dbase.close()
self.dbstate.no_database()
self.__populate()
def __start_cursor(self, msg):
"""
Set the cursor to the busy state, and displays the associated
message
"""
self.msg.set_label(msg)
self.top.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH))
while (gtk.events_pending()):
gtk.main_iteration()
def __end_cursor(self):
"""
Set the cursor back to normal and clears the message
"""
self.top.window.set_cursor(None)
self.msg.set_label("")
def __new_db(self, obj):
"""
Callback wrapper around the actual routine that creates the
new database. Catch OSError and IOError and display a warning
message.
"""
self.new.set_sensitive(False)
try:
self._create_new_db()
except (OSError, IOError), msg:
ErrorDialog(_("Could not create family tree"),
str(msg))
self.new.set_sensitive(True)
def _create_new_db(self, title=None):
"""
Create a new database, append to model
"""
new_path, title = self._create_new_db_cli(title)
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,
last, tval, False, ''])
self.selection.select_iter(node)
path = self.model.get_path(node)
self.name_renderer.set_property('editable', True)
self.dblist.set_cursor(path, focus_column=self.column,
start_editing=True)
return new_path, title
def __drag_data_received(self, widget, context, xpos, ypos, selection,
info, rtime):
"""
Handle the reception of drag data
"""
drag_value = selection.data
fname = None
type = None
title = None
# we are only interested in this if it is a file:// URL.
if drag_value and drag_value[0:7] == "file://":
drag_value = drag_value.strip()
fname, title = self.import_new_db(drag_value[7:], None)
return fname, title
def drag_motion(wid, context, xpos, ypos, time_stamp):
"""
DND callback that is called on a DND drag motion begin
"""
context.drag_status(gtk.gdk.ACTION_COPY, time_stamp)
return True
def drop_cb(wid, context, xpos, ypos, time_stamp):
"""
DND callback that finishes the DND operation
"""
context.finish(True, False, time_stamp)
return True
def find_next_db_name(name_list):
"""
Scan the name list, looking for names that do not yet exist.
Use the DEFAULT_TITLE as the basis for the database name.
"""
i = 1
while True:
title = "%s %d" % (DEFAULT_TITLE, i)
if title not in name_list:
return title
i += 1
def find_next_db_dir():
"""
Searches the default directory for the first available default
database name. Base the name off the current time. In all actuality,
the first should be valid.
"""
while True:
base = "%x" % int(time.time())
dbdir = os.path.expanduser(Config.get(Config.DATABASE_PATH))
new_path = os.path.join(dbdir, base)
if not os.path.isdir(new_path):
break
return new_path
def make_dbdir(dbdir):
"""
Create the default database directory, as defined by dbdir
"""
try:
if not os.path.isdir(dbdir):
os.makedirs(dbdir)
except (IOError, OSError), msg:
LOG.error(_("Could not make database directory: ") + str(msg))
def time_val(dirpath):
"""
Return the last modified time of the database. We do this by looking
at the modification time of the meta db file. If this file does not
exist, we indicate that database as never modified.
"""
meta = os.path.join(dirpath, META_NAME)
if os.path.isfile(meta):
tval = os.stat(meta)[9]
last = time.strftime('%x %X', time.localtime(tval))
else:
tval = 0
last = _("Never")
return (tval, last)
def icon_values(dirpath, active, is_open):
"""
If the directory path is the active path, then return values
that indicate to use the icon, and which icon to use.
"""
if os.path.isfile(os.path.join(dirpath,"need_recover")):
return (True, gtk.STOCK_DIALOG_ERROR)
elif dirpath == active and is_open:
return (True, gtk.STOCK_OPEN)
elif os.path.isfile(os.path.join(dirpath,"lock")):
return (True, 'gramps-lock')
else:
return (False, "")
def find_revisions(name):
"""
Finds all the revisions of the specfied RCS archive.
"""
import re
rev = re.compile("\s*revision\s+([\d\.]+)")
date = re.compile("date:\s+(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)[-+]\d\d;")
if not os.path.isfile(name) or not _RCS_FOUND:
return []
rlog = [ "rlog", "-x,v", "-zLT" , name ]
proc = subprocess.Popen(rlog, stdout = subprocess.PIPE)
proc.wait()
revlist = []
date_str = ""
rev_str = ""
com_str = ""
get_next = False
if os.path.isfile(name):
for line in proc.stdout:
match = rev.match(line)
if match:
rev_str = copy.copy(match.groups()[0])
continue
match = date.match(line)
if match:
date_str = time.strftime('%x %X',
time.strptime(match.groups()[0], '%Y-%m-%d %H:%M:%S'))
get_next = True
continue
if get_next:
get_next = False
com_str = line.strip()
revlist.append((rev_str, date_str, com_str))
proc.stdout.close()
del proc
return revlist
def find_locker_name(dirpath):
"""
Opens the lock file if it exists, reads the contexts and returns
the contents, which should be like "Locked by USERNAME".
If a file is encountered with errors, we return 'Unknown'
This data is displayed in the time column of the manager
"""
try:
fname = os.path.join(dirpath, "lock")
ifile = open(fname)
last = ifile.read().strip()
ifile.close()
except (OSError, IOError):
last = _("Unknown")
return last
def check_out(dbase, rev, path, callback):
"""
Checks out the revision from rcs, and loads the resulting XML file
into the database.
"""
co_cmd = [ "co", "-x,v", "-q%s" % rev] + [ os.path.join(path, ARCHIVE),
os.path.join(path, ARCHIVE_V)]
proc = subprocess.Popen(co_cmd, stderr = subprocess.PIPE)
status = proc.wait()
message = "\n".join(proc.stderr.readlines())
proc.stderr.close()
del proc
if status != 0:
ErrorDialog(
_("Retrieve failed"),
_("An attempt to retrieve the data failed "
"with the following message:\n\n%s") % message
)
return
pmgr = PluginManager.get_instance()
for plugin in pmgr.get_import_plugins():
if plugin.get_extension() == "gramps":
rdr = plugin.get_import_function()
xml_file = os.path.join(path, ARCHIVE)
rdr(dbase, xml_file, callback)
os.unlink(xml_file)
def check_in(dbase, filename, callback, cursor_func = None):
"""
Checks in the specified file into RCS
"""
init = [ "rcs", '-x,v', '-i', '-U', '-q', '-t-"GRAMPS database"' ]
ci_cmd = [ "ci", '-x,v', "-q", "-f" ]
archive_name = filename + ",v"
glade_xml_file = glade.XML(const.GLADE_FILE, "comment", "gramps")
top = glade_xml_file.get_widget('comment')
text = glade_xml_file.get_widget('description')
top.run()
comment = text.get_text()
top.destroy()
if not os.path.isfile(archive_name):
cmd = init + [archive_name]
proc = subprocess.Popen(cmd,
stderr = subprocess.PIPE)
status = proc.wait()
message = "\n".join(proc.stderr.readlines())
proc.stderr.close()
del proc
if status != 0:
ErrorDialog(
_("Archiving failed"),
_("An attempt to create the archive failed "
"with the following message:\n\n%s") % message
)
if cursor_func:
cursor_func(_("Creating data to be archived..."))
xmlwrite = GrampsDbUtils.XmlWriter(dbase, callback, False, 0)
xmlwrite.write(filename)
if cursor_func:
cursor_func(_("Saving archive..."))
cmd = ci_cmd + ['-m%s' % comment, filename, archive_name ]
proc = subprocess.Popen(cmd,
stderr = subprocess.PIPE)
status = proc.wait()
message = "\n".join(proc.stderr.readlines())
proc.stderr.close()
del proc
if status != 0:
ErrorDialog(
_("Archiving failed"),
_("An attempt to archive the data failed "
"with the following message:\n\n%s") % message
)