index 46c44d1b653ec0baf022f253dc5bad8e0bb04982..d2897addf497c0f019d6cd5bbae4bf8edf6d6eff 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-#$Id: back_anydbm.py,v 1.69 2002-09-03 07:33:01 richard Exp $
+#$Id: back_anydbm.py,v 1.131 2003-11-14 00:11:18 richard Exp $
'''
This module defines a backend that saves the hyperdatabase in a database
chosen by anydbm. It is guaranteed to always be available in python
serious bugs, and is not available)
'''
-import whichdb, anydbm, os, marshal, re, weakref, string, copy
+try:
+ import anydbm, sys
+ # dumbdbm only works in python 2.1.2+
+ if sys.version_info < (2,1,2):
+ import dumbdbm
+ assert anydbm._defaultmod != dumbdbm
+ del dumbdbm
+except AssertionError:
+ print "WARNING: you should upgrade to python 2.1.3"
+
+import whichdb, os, marshal, re, weakref, string, copy
from roundup import hyperdb, date, password, roundupdb, security
from blobfiles import FileStorage
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
from roundup.indexer import Indexer
-from locking import acquire_lock, release_lock
+from roundup.backends import locking
from roundup.hyperdb import String, Password, Date, Interval, Link, \
- Multilink, DatabaseError, Boolean, Number
+ Multilink, DatabaseError, Boolean, Number, Node
+from roundup.date import Range
#
# Now the database
The 'journaltag' is a token that will be attached to the journal
entries for any edits done on the database. If 'journaltag' is
None, the database is opened in read-only mode: the Class.create(),
- Class.set(), and Class.retire() methods are disabled.
- '''
+ Class.set(), Class.retire(), and Class.restore() methods are
+ disabled.
+ '''
self.config, self.journaltag = config, journaltag
self.dir = config.DATABASE
self.classes = {}
self.transactions = []
self.indexer = Indexer(self.dir)
self.sessions = Sessions(self.config)
+ self.otks = OneTimeKeys(self.config)
self.security = security.Security(self)
# ensure files are group readable and writable
os.umask(0002)
+ # lock it
+ lockfilenm = os.path.join(self.dir, 'lock')
+ self.lockfile = locking.acquire_lock(lockfilenm)
+ self.lockfile.write(str(os.getpid()))
+ self.lockfile.flush()
+
def post_init(self):
- '''Called once the schema initialisation has finished.'''
+ ''' Called once the schema initialisation has finished.
+ '''
# reindex the db if necessary
if self.indexer.should_reindex():
self.reindex()
+ def refresh_database(self):
+ "Rebuild the database"
+ self.reindex()
+
def reindex(self):
for klass in self.classes.values():
for nodeid in klass.list():
'''
if __debug__:
print >>hyperdb.DEBUG, 'getclass', (self, classname)
- return self.classes[classname]
+ try:
+ return self.classes[classname]
+ except KeyError:
+ raise KeyError, 'There is no class called "%s"'%classname
#
# Class DBs
if os.path.exists(path):
db_type = whichdb.whichdb(path)
if not db_type:
- raise hyperdb.DatabaseError, "Couldn't identify database type"
+ raise DatabaseError, "Couldn't identify database type"
elif os.path.exists(path+'.db'):
# if the path ends in '.db', it's a dbm database, whether
# anydbm says it's dbhash or not!
try:
dbm = __import__(db_type)
except ImportError:
- raise hyperdb.DatabaseError, \
+ raise DatabaseError, \
"Couldn't open database - the required module '%s'"\
" is not available"%db_type
if __debug__:
mode)
return dbm.open(path, mode)
- def lockdb(self, name):
- ''' Lock a database file
- '''
- path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
- return acquire_lock(path)
-
#
# Node IDs
#
''' Generate a new id for the given class
'''
# open the ids DB - create if if doesn't exist
- lock = self.lockdb('_ids')
db = self.opendb('_ids', 'c')
if db.has_key(classname):
newid = db[classname] = str(int(db[classname]) + 1)
newid = str(self.getclass(classname).count()+1)
db[classname] = newid
db.close()
- release_lock(lock)
return newid
def setid(self, classname, setid):
''' Set the id counter: used during import of database
'''
# open the ids DB - create if if doesn't exist
- lock = self.lockdb('_ids')
db = self.opendb('_ids', 'c')
db[classname] = str(setid)
db.close()
- release_lock(lock)
#
# Nodes
'''
if __debug__:
print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
+
+ # we'll be supplied these props if we're doing an import
+ if not node.has_key('creator'):
+ # add in the "calculated" properties (dupe so we don't affect
+ # calling code's node assumptions)
+ node = node.copy()
+ node['creator'] = self.getuid()
+ node['creation'] = node['activity'] = date.Date()
+
self.newnodes.setdefault(classname, {})[nodeid] = 1
self.cache.setdefault(classname, {})[nodeid] = node
self.savenode(classname, nodeid, node)
print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
self.dirtynodes.setdefault(classname, {})[nodeid] = 1
+ # update the activity time (dupe so we don't affect
+ # calling code's node assumptions)
+ node = node.copy()
+ node['activity'] = date.Date()
+
# can't set without having already loaded the node
self.cache[classname][nodeid] = node
self.savenode(classname, nodeid, node)
def getnode(self, classname, nodeid, db=None, cache=1):
''' get a node from the database
+
+ Note the "cache" parameter is not used, and exists purely for
+ backward compatibility!
'''
if __debug__:
print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
- if cache:
- # try the cache
- cache_dict = self.cache.setdefault(classname, {})
- if cache_dict.has_key(nodeid):
- if __debug__:
- print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
- nodeid)
- return cache_dict[nodeid]
+
+ # try the cache
+ cache_dict = self.cache.setdefault(classname, {})
+ if cache_dict.has_key(nodeid):
+ if __debug__:
+ print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
+ nodeid)
+ return cache_dict[nodeid]
if __debug__:
print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
if db is None:
db = self.getclassdb(classname)
if not db.has_key(nodeid):
+ # try the cache - might be a brand-new node
+ cache_dict = self.cache.setdefault(classname, {})
+ if cache_dict.has_key(nodeid):
+ if __debug__:
+ print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
+ nodeid)
+ return cache_dict[nodeid]
raise IndexError, "no such %s %s"%(classname, nodeid)
# check the uncommitted, destroyed nodes
# get the property spec
prop = properties[k]
- if isinstance(prop, Password):
+ if isinstance(prop, Password) and v is not None:
d[k] = str(v)
elif isinstance(prop, Date) and v is not None:
d[k] = v.serialise()
d[k] = date.Date(v)
elif isinstance(prop, Interval) and v is not None:
d[k] = date.Interval(v)
- elif isinstance(prop, Password):
+ elif isinstance(prop, Password) and v is not None:
p = password.Password()
p.unpack(v)
d[k] = p
count = count + len(db.keys())
return count
- def getnodeids(self, classname, db=None):
- if __debug__:
- print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
-
- res = []
-
- # start off with the new nodes
- if self.newnodes.has_key(classname):
- res += self.newnodes[classname].keys()
-
- if db is None:
- db = self.getclassdb(classname)
- res = res + db.keys()
-
- # remove the uncommitted, destroyed nodes
- if self.destroyednodes.has_key(classname):
- for nodeid in self.destroyednodes[classname].keys():
- if db.has_key(nodeid):
- res.remove(nodeid)
-
- return res
-
#
# Files - special node properties
#
# Journal
#
- def addjournal(self, classname, nodeid, action, params):
+ def addjournal(self, classname, nodeid, action, params, creator=None,
+ creation=None):
''' Journal the Action
'action' may be:
'''
if __debug__:
print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
- action, params)
+ action, params, creator, creation)
self.transactions.append((self.doSaveJournal, (classname, nodeid,
- action, params)))
+ action, params, creator, creation)))
def getjournal(self, classname, nodeid):
''' get the journal for id
'''
if __debug__:
print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
+
+ # our journal result
+ res = []
+
+ # add any journal entries for transactions not committed to the
+ # database
+ for method, args in self.transactions:
+ if method != self.doSaveJournal:
+ continue
+ (cache_classname, cache_nodeid, cache_action, cache_params,
+ cache_creator, cache_creation) = args
+ if cache_classname == classname and cache_nodeid == nodeid:
+ if not cache_creator:
+ cache_creator = self.getuid()
+ if not cache_creation:
+ cache_creation = date.Date()
+ res.append((cache_nodeid, cache_creation, cache_creator,
+ cache_action, cache_params))
+
# attempt to open the journal - in some rare cases, the journal may
# not exist
try:
if str(error) == "need 'c' or 'n' flag to open new db":
raise IndexError, 'no such %s %s'%(classname, nodeid)
elif error.args[0] != 2:
+ # this isn't a "not found" error, be alarmed!
raise
+ if res:
+ # we have unsaved journal entries, return them
+ return res
raise IndexError, 'no such %s %s'%(classname, nodeid)
try:
journal = marshal.loads(db[nodeid])
except KeyError:
db.close()
+ if res:
+ # we have some unsaved journal entries, be happy!
+ return res
raise IndexError, 'no such %s %s'%(classname, nodeid)
db.close()
- res = []
+
+ # add all the saved journal entries for this node
for nodeid, date_stamp, user, action, params in journal:
res.append((nodeid, date.Date(date_stamp), user, action, params))
return res
if __debug__:
print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
+ pack_before = pack_before.serialise()
for classname in self.getclasses():
# get the journal db
db_name = 'journals.%s'%classname
# unpack the entry
(nodeid, date_stamp, self.journaltag, action,
params) = entry
- date_stamp = date.Date(date_stamp)
# if the entry is after the pack date, _or_ the initial
# create entry, then it stays
if date_stamp > pack_before or action == 'create':
l.append(entry)
- elif action == 'set':
- # grab the last set entry to keep information on
- # activity
- last_set_entry = entry
- if last_set_entry:
- date_stamp = last_set_entry[1]
- # if the last set entry was made after the pack date
- # then it is already in the list
- if date_stamp < pack_before:
- l.append(last_set_entry)
db[key] = marshal.dumps(l)
if db_type == 'gdbm':
db.reorganize()
'''
if __debug__:
print >>hyperdb.DEBUG, 'commit', (self,)
- # TODO: lock the DB
# keep a handle to all the database files opened
self.databases = {}
- # now, do all the transactions
- reindex = {}
- for method, args in self.transactions:
- reindex[method(*args)] = 1
-
- # now close all the database files
- for db in self.databases.values():
- db.close()
- del self.databases
- # TODO: unlock the DB
+ try:
+ # now, do all the transactions
+ reindex = {}
+ for method, args in self.transactions:
+ reindex[method(*args)] = 1
+ finally:
+ # make sure we close all the database files
+ for db in self.databases.values():
+ db.close()
+ del self.databases
# reindex the nodes that request it
for classname, nodeid in filter(None, reindex.keys()):
# save the indexer state
self.indexer.save_index()
+ self.clearCache()
+
+ def clearCache(self):
# all transactions committed, back to normal
self.cache = {}
self.dirtynodes = {}
self.databases[db_name] = self.opendb(db_name, 'c')
return self.databases[db_name]
- def doSaveJournal(self, classname, nodeid, action, params):
- # handle supply of the special journalling parameters (usually
- # supplied on importing an existing database)
+ def doSaveJournal(self, classname, nodeid, action, params, creator,
+ creation):
+ # serialise the parameters now if necessary
if isinstance(params, type({})):
- if params.has_key('creator'):
- journaltag = self.user.get(params['creator'], 'username')
- del params['creator']
- else:
- journaltag = self.journaltag
- if params.has_key('created'):
- journaldate = params['created'].serialise()
- del params['created']
- else:
- journaldate = date.Date().serialise()
- if params.has_key('activity'):
- del params['activity']
-
- # serialise the parameters now
if action in ('set', 'create'):
params = self.serialise(classname, params)
+
+ # handle supply of the special journalling parameters (usually
+ # supplied on importing an existing database)
+ if creator:
+ journaltag = creator
+ else:
+ journaltag = self.getuid()
+ if creation:
+ journaldate = creation.serialise()
else:
- journaltag = self.journaltag
journaldate = date.Date().serialise()
# create the journal entry
self.destroyednodes = {}
self.transactions = []
+ def close(self):
+ ''' Nothing to do
+ '''
+ if self.lockfile is not None:
+ locking.release_lock(self.lockfile)
+ if self.lockfile is not None:
+ self.lockfile.close()
+ self.lockfile = None
+
_marker = []
class Class(hyperdb.Class):
'''The handle to a particular class of nodes in a hyperdatabase.'''
# do the db-related init stuff
db.addclass(self)
- self.auditors = {'create': [], 'set': [], 'retire': []}
- self.reactors = {'create': [], 'set': [], 'retire': []}
+ self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
+ self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
def enableJournalling(self):
'''Turn journalling on for this class
These operations trigger detectors and can be vetoed. Attempts
to modify the "creation" or "activity" properties cause a KeyError.
'''
+ self.fireAuditors('create', None, propvalues)
+ newid = self.create_inner(**propvalues)
+ self.fireReactors('create', newid, None)
+ return newid
+
+ def create_inner(self, **propvalues):
+ ''' Called by create, in-between the audit and react calls.
+ '''
if propvalues.has_key('id'):
raise KeyError, '"id" is reserved'
if propvalues.has_key('creation') or propvalues.has_key('activity'):
raise KeyError, '"creation" and "activity" are reserved'
-
- self.fireAuditors('create', None, propvalues)
-
# new node's id
newid = self.db.newid(self.classname)
l = []
for entry in value:
if type(entry) != type(''):
- raise ValueError, '"%s" link value (%s) must be '\
- 'String'%(key, value)
+ raise ValueError, '"%s" multilink value (%r) '\
+ 'must contain Strings'%(key, value)
# if it isn't a number, it's a key
if not num_re.match(entry):
try:
(self.classname, newid, key))
elif isinstance(prop, String):
- if type(value) != type(''):
+ if type(value) != type('') and type(value) != type(u''):
raise TypeError, 'new property "%s" not a string'%key
elif isinstance(prop, Password):
# done
self.db.addnode(self.classname, newid, propvalues)
if self.do_journal:
- self.db.addjournal(self.classname, newid, 'create', propvalues)
-
- self.fireReactors('create', newid, None)
+ self.db.addjournal(self.classname, newid, 'create', {})
return newid
proptype = properties[prop]
value = self.get(nodeid, prop)
# "marshal" data where needed
- if isinstance(proptype, hyperdb.Date):
+ if value is None:
+ pass
+ elif isinstance(proptype, hyperdb.Date):
value = value.get_tuple()
elif isinstance(proptype, hyperdb.Interval):
value = value.get_tuple()
elif isinstance(proptype, hyperdb.Password):
value = str(value)
l.append(repr(value))
+
+ # append retired flag
+ l.append(repr(self.is_retired(nodeid)))
+
return l
def import_list(self, propnames, proplist):
# make the new node's property map
d = {}
+ newid = None
for i in range(len(propnames)):
- # Use eval to reverse the repr() used to output the CSV
- value = eval(proplist[i])
-
# Figure the property for this column
propname = propnames[i]
- prop = properties[propname]
+
+ # Use eval to reverse the repr() used to output the CSV
+ value = eval(proplist[i])
# "unmarshal" where necessary
if propname == 'id':
newid = value
continue
- elif isinstance(prop, hyperdb.Date):
+ elif propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ d[self.db.RETIRED_FLAG] = 1
+ continue
+ elif value is None:
+ d[propname] = None
+ continue
+
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Date):
value = date.Date(value)
elif isinstance(prop, hyperdb.Interval):
value = date.Interval(value)
pwd = password.Password()
pwd.unpack(value)
value = pwd
- if value is not None:
- d[propname] = value
+ d[propname] = value
- # add
+ # get a new id if necessary
+ if newid is None:
+ newid = self.db.newid(self.classname)
+
+ # add the node and journal
self.db.addnode(self.classname, newid, d)
- self.db.addjournal(self.classname, newid, 'create', d)
+
+ # extract the journalling stuff and nuke it
+ if d.has_key('creator'):
+ creator = d['creator']
+ del d['creator']
+ else:
+ creator = None
+ if d.has_key('creation'):
+ creation = d['creation']
+ del d['creation']
+ else:
+ creation = None
+ if d.has_key('activity'):
+ del d['activity']
+ self.db.addjournal(self.classname, newid, 'create', {}, creator,
+ creation)
return newid
def get(self, nodeid, propname, default=_marker, cache=1):
IndexError is raised. 'propname' must be the name of a property
of this class or a KeyError is raised.
- 'cache' indicates whether the transaction cache should be queried
- for the node. If the node has been modified and you need to
- determine what its values prior to modification are, you need to
- set cache=0.
+ 'cache' exists for backward compatibility, and is not used.
Attempts to get the "creation" or "activity" properties should
do the right thing.
if propname == 'id':
return nodeid
+ # get the node's dict
+ d = self.db.getnode(self.classname, nodeid)
+
+ # check for one of the special props
if propname == 'creation':
+ if d.has_key('creation'):
+ return d['creation']
if not self.do_journal:
raise ValueError, 'Journalling is disabled for this class'
journal = self.db.getjournal(self.classname, nodeid)
# on the strange chance that there's no journal
return date.Date()
if propname == 'activity':
+ if d.has_key('activity'):
+ return d['activity']
if not self.do_journal:
raise ValueError, 'Journalling is disabled for this class'
journal = self.db.getjournal(self.classname, nodeid)
# on the strange chance that there's no journal
return date.Date()
if propname == 'creator':
+ if d.has_key('creator'):
+ return d['creator']
if not self.do_journal:
raise ValueError, 'Journalling is disabled for this class'
journal = self.db.getjournal(self.classname, nodeid)
if journal:
- name = self.db.getjournal(self.classname, nodeid)[0][2]
+ num_re = re.compile('^\d+$')
+ value = self.db.getjournal(self.classname, nodeid)[0][2]
+ if num_re.match(value):
+ return value
+ else:
+ # old-style "username" journal tag
+ try:
+ return self.db.user.lookup(value)
+ except KeyError:
+ # user's been retired, return admin
+ return '1'
else:
- return None
- try:
- return self.db.user.lookup(name)
- except KeyError:
- # the journaltag user doesn't exist any more
- return None
+ return self.db.getuid()
# get the property (raises KeyErorr if invalid)
prop = self.properties[propname]
- # get the node's dict
- d = self.db.getnode(self.classname, nodeid, cache=cache)
-
if not d.has_key(propname):
if default is _marker:
if isinstance(prop, Multilink):
return d[propname]
- # XXX not in spec
+ # not in spec
def getnode(self, nodeid, cache=1):
''' Return a convenience wrapper for the node.
'nodeid' must be the id of an existing node of this class or an
IndexError is raised.
- 'cache' indicates whether the transaction cache should be queried
- for the node. If the node has been modified and you need to
- determine what its values prior to modification are, you need to
- set cache=0.
+ 'cache' exists for backwards compatibility, and is not used.
'''
- return Node(self, nodeid, cache=cache)
+ return Node(self, nodeid)
def set(self, nodeid, **propvalues):
'''Modify a property on an existing node of this class.
self.fireAuditors('set', nodeid, propvalues)
# Take a copy of the node dict so that the subsequent set
# operation doesn't modify the oldvalues structure.
- try:
- # try not using the cache initially
- oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
- cache=0))
- except IndexError:
- # this will be needed if somone does a create() and set()
- # with no intervening commit()
- oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
+ oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
node = self.db.getnode(self.classname, nodeid)
if node.has_key(self.db.RETIRED_FLAG):
# this will raise the KeyError if the property isn't valid
# ... we don't use getprops() here because we only care about
# the writeable properties.
- prop = self.properties[propname]
+ try:
+ prop = self.properties[propname]
+ except KeyError:
+ raise KeyError, '"%s" has no property named "%s"'%(
+ self.classname, propname)
# if the value's the same as the existing value, no sense in
# doing anything
- if node.has_key(propname) and value == node[propname]:
+ current = node.get(propname, None)
+ if value == current:
del propvalues[propname]
continue
+ journalvalues[propname] = current
# do stuff based on the prop type
if isinstance(prop, Link):
if self.do_journal and prop.do_journal:
# register the unlink with the old linked node
- if node[propname] is not None:
+ if node.has_key(propname) and node[propname] is not None:
self.db.addjournal(link_class, node[propname], 'unlink',
(self.classname, nodeid, propname))
journalvalues[propname] = tuple(l)
elif isinstance(prop, String):
- if value is not None and type(value) != type(''):
+ if value is not None and type(value) != type('') and type(value) != type(u''):
raise TypeError, 'new property "%s" not a string'%propname
elif isinstance(prop, Password):
self.db.setnode(self.classname, nodeid, node)
if self.do_journal:
- propvalues.update(journalvalues)
- self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+ self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
self.fireReactors('set', nodeid, oldvalues)
self.fireReactors('retire', nodeid, None)
- def is_retired(self, nodeid):
+ def restore(self, nodeid):
+ '''Restpre a retired node.
+
+ Make node available for all operations like it was before retirement.
+ '''
+ if self.db.journaltag is None:
+ raise DatabaseError, 'Database open read-only'
+
+ node = self.db.getnode(self.classname, nodeid)
+ # check if key property was overrided
+ key = self.getkey()
+ try:
+ id = self.lookup(node[key])
+ except KeyError:
+ pass
+ else:
+ raise KeyError, "Key property (%s) of retired node clashes with \
+ existing one (%s)" % (key, node[key])
+ # Now we can safely restore node
+ self.fireAuditors('restore', nodeid, None)
+ del node[self.db.RETIRED_FLAG]
+ self.db.setnode(self.classname, nodeid, node)
+ if self.do_journal:
+ self.db.addjournal(self.classname, nodeid, 'restored', None)
+
+ self.fireReactors('restore', nodeid, None)
+
+ def is_retired(self, nodeid, cldb=None):
'''Return true if the node is retired.
'''
- node = self.db.getnode(cn, nodeid, cldb)
+ node = self.db.getnode(self.classname, nodeid, cldb)
if node.has_key(self.db.RETIRED_FLAG):
return 1
return 0
def destroy(self, nodeid):
'''Destroy a node.
-
+
WARNING: this method should never be used except in extremely rare
situations where there could never be links to the node being
deleted
The returned list contains tuples of the form
- (date, tag, action, params)
+ (nodeid, date, tag, action, params)
'date' is a Timestamp object specifying the time of the change and
'tag' is the journaltag specified when the database was opened.
otherwise a KeyError is raised.
'''
if not self.key:
- raise TypeError, 'No key property set'
+ raise TypeError, 'No key property set for class %s'%self.classname
cldb = self.db.getclassdb(self.classname)
try:
- for nodeid in self.db.getnodeids(self.classname, cldb):
+ for nodeid in self.getnodeids(cldb):
node = self.db.getnode(self.classname, nodeid, cldb)
if node.has_key(self.db.RETIRED_FLAG):
continue
if node[self.key] == keyvalue:
- cldb.close()
return nodeid
finally:
cldb.close()
- raise KeyError, keyvalue
+ raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
+ keyvalue, self.classname)
- # XXX: change from spec - allows multiple props to match
+ # change from spec - allows multiple props to match
def find(self, **propspec):
- '''Get the ids of nodes in this class which link to the given nodes.
+ '''Get the ids of items in this class which link to the given items.
+
+ 'propspec' consists of keyword args propname=itemid or
+ propname={itemid:1, }
+ 'propname' must be the name of a property in this class, or a
+ KeyError is raised. That property must be a Link or
+ Multilink property, or a TypeError is raised.
- 'propspec' consists of keyword args propname={nodeid:1,}
- 'propname' must be the name of a property in this class, or a
- KeyError is raised. That property must be a Link or Multilink
- property, or a TypeError is raised.
+ Any item in this class whose 'propname' property links to any of the
+ itemids will be returned. Used by the full text indexing, which knows
+ that "foo" occurs in msg1, msg3 and file7, so we have hits on these
+ issues:
- Any node in this class whose 'propname' property links to any of the
- nodeids will be returned. Used by the full text indexing, which knows
- that "foo" occurs in msg1, msg3 and file7, so we have hits on these issues:
db.issue.find(messages={'1':1,'3':1}, files={'7':1})
'''
propspec = propspec.items()
- for propname, nodeids in propspec:
+ for propname, itemids in propspec:
# check the prop is OK
prop = self.properties[propname]
if not isinstance(prop, Link) and not isinstance(prop, Multilink):
cldb = self.db.getclassdb(self.classname)
l = []
try:
- for id in self.db.getnodeids(self.classname, db=cldb):
- node = self.db.getnode(self.classname, id, db=cldb)
- if node.has_key(self.db.RETIRED_FLAG):
+ for id in self.getnodeids(db=cldb):
+ item = self.db.getnode(self.classname, id, db=cldb)
+ if item.has_key(self.db.RETIRED_FLAG):
continue
- for propname, nodeids in propspec:
- # can't test if the node doesn't have this property
- if not node.has_key(propname):
+ for propname, itemids in propspec:
+ # can't test if the item doesn't have this property
+ if not item.has_key(propname):
continue
- if type(nodeids) is type(''):
- nodeids = {nodeids:1}
+ if type(itemids) is not type({}):
+ itemids = {itemids:1}
+
+ # grab the property definition and its value on this item
prop = self.properties[propname]
- value = node[propname]
- if isinstance(prop, Link) and nodeids.has_key(value):
+ value = item[propname]
+ if isinstance(prop, Link) and itemids.has_key(value):
l.append(id)
break
elif isinstance(prop, Multilink):
hit = 0
for v in value:
- if nodeids.has_key(v):
+ if itemids.has_key(v):
l.append(id)
hit = 1
break
l = []
cldb = self.db.getclassdb(self.classname)
try:
- for nodeid in self.db.getnodeids(self.classname, cldb):
+ for nodeid in self.getnodeids(cldb):
node = self.db.getnode(self.classname, nodeid, cldb)
if node.has_key(self.db.RETIRED_FLAG):
continue
for key, value in requirements.items():
+ if not node.has_key(key):
+ break
if node[key] is None or node[key].lower() != value:
break
else:
cn = self.classname
cldb = self.db.getclassdb(cn)
try:
- for nodeid in self.db.getnodeids(cn, cldb):
+ for nodeid in self.getnodeids(cldb):
node = self.db.getnode(cn, nodeid, cldb)
if node.has_key(self.db.RETIRED_FLAG):
continue
l.sort()
return l
- def filter(self, search_matches, filterspec, sort, group,
- num_re = re.compile('^\d+$')):
+ def getnodeids(self, db=None):
+ ''' Return a list of ALL nodeids
+ '''
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getnodeids', (self, self.classname, db)
+
+ res = []
+
+ # start off with the new nodes
+ if self.db.newnodes.has_key(self.classname):
+ res += self.db.newnodes[self.classname].keys()
+
+ if db is None:
+ db = self.db.getclassdb(self.classname)
+ res = res + db.keys()
+
+ # remove the uncommitted, destroyed nodes
+ if self.db.destroyednodes.has_key(self.classname):
+ for nodeid in self.db.destroyednodes[self.classname].keys():
+ if db.has_key(nodeid):
+ res.remove(nodeid)
+
+ return res
+
+ def filter(self, search_matches, filterspec, sort=(None,None),
+ group=(None,None), num_re = re.compile('^\d+$')):
''' Return a list of the ids of the active nodes in this class that
match the 'filter' spec, sorted by the group spec and then the
sort spec.
"sort" and "group" are (dir, prop) where dir is '+', '-' or None
and prop is a prop name or None
"search_matches" is {nodeid: marker}
+
+ The filter must match all properties specificed - but if the
+ property value to match is a list, any one of the values in the
+ list may match for that property to match. Unless the property
+ is a Multilink, in which case the item's property list must
+ match the filterspec list.
'''
cn = self.classname
LINK = 0
MULTILINK = 1
STRING = 2
+ DATE = 3
+ INTERVAL = 4
OTHER = 6
+
+ timezone = self.db.getUserTimezone()
for k, v in filterspec.items():
propclass = props[k]
if isinstance(propclass, Link):
u = []
link_class = self.db.classes[propclass.classname]
for entry in v:
- if entry == '-1': entry = None
+ # the value -1 is a special "not set" sentinel
+ if entry == '-1':
+ entry = None
elif not num_re.match(entry):
try:
entry = link_class.lookup(entry)
l.append((LINK, k, u))
elif isinstance(propclass, Multilink):
- if type(v) is not type([]):
+ # the value -1 is a special "not set" sentinel
+ if v in ('-1', ['-1']):
+ v = []
+ elif type(v) is not type([]):
v = [v]
+
# replace key values with node ids
u = []
link_class = self.db.classes[propclass.classname]
raise ValueError, 'new property "%s": %s not a %s'%(
k, entry, self.properties[k].classname)
u.append(entry)
+ u.sort()
l.append((MULTILINK, k, u))
- elif isinstance(propclass, String):
- # simple glob searching
- v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
- v = v.replace('?', '.')
- v = v.replace('*', '.*?')
- l.append((STRING, k, re.compile(v, re.I)))
+ elif isinstance(propclass, String) and k != 'id':
+ if type(v) is not type([]):
+ v = [v]
+ m = []
+ for v in v:
+ # simple glob searching
+ v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
+ v = v.replace('?', '.')
+ v = v.replace('*', '.*?')
+ m.append(v)
+ m = re.compile('(%s)'%('|'.join(m)), re.I)
+ l.append((STRING, k, m))
+ elif isinstance(propclass, Date):
+ try:
+ date_rng = Range(v, date.Date, offset=timezone)
+ l.append((DATE, k, date_rng))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+ elif isinstance(propclass, Interval):
+ try:
+ intv_rng = Range(v, date.Interval)
+ l.append((INTERVAL, k, intv_rng))
+ except ValueError:
+ # If range creation fails - ignore that search parameter
+ pass
+
elif isinstance(propclass, Boolean):
if type(v) is type(''):
bv = v.lower() in ('yes', 'true', 'on', '1')
cldb = self.db.getclassdb(cn)
try:
# TODO: only full-scan once (use items())
- for nodeid in self.db.getnodeids(cn, cldb):
+ for nodeid in self.getnodeids(cldb):
node = self.db.getnode(cn, nodeid, cldb)
if node.has_key(self.db.RETIRED_FLAG):
continue
# apply filter
for t, k, v in filterspec:
+ # handle the id prop
+ if k == 'id' and v == nodeid:
+ continue
+
# make sure the node has the property
if not node.has_key(k):
# this node doesn't have this property, so reject it
# filterspec aren't in this node's property, then skip
# it
have = node[k]
+ # check for matching the absence of multilink values
+ if not v and have:
+ break
+
+ # othewise, make sure this node has each of the
+ # required values
for want in v:
if want not in have:
break
continue
break
elif t == STRING:
+ if node[k] is None:
+ break
# RE search
- if node[k] is None or not v.search(node[k]):
+ if not v.search(node[k]):
+ break
+ elif t == DATE or t == INTERVAL:
+ if node[k] is None:
break
+ if v.to_value:
+ if not (v.from_value <= node[k] and v.to_value >= node[k]):
+ break
+ else:
+ if not (v.from_value <= node[k]):
+ break
elif t == OTHER:
# straight value comparison for the other types
if node[k] != v:
b_id, bn = b
# sort by group and then sort
for dir, prop in group, sort:
- if dir is None: continue
+ if dir is None or prop is None: continue
# sorting is class-specific
propclass = properties[prop]
if isinstance(propclass, String):
# clean up the strings
if av and av[0] in string.uppercase:
- av = an[prop] = av.lower()
+ av = av.lower()
if bv and bv[0] in string.uppercase:
- bv = bn[prop] = bv.lower()
+ bv = bv.lower()
if (isinstance(propclass, String) or
isinstance(propclass, Date)):
# it might be a string that's really an integer
r = cmp(bv, av)
if r != 0: return r
- # Multilink properties are sorted according to how many
- # links are present.
- elif isinstance(propclass, Multilink):
- if dir == '+':
- r = cmp(len(av), len(bv))
- if r != 0: return r
- elif dir == '-':
- r = cmp(len(bv), len(av))
- if r != 0: return r
- elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
+ else:
+ # all other types just compare
if dir == '+':
r = cmp(av, bv)
elif dir == '-':
r = cmp(bv, av)
+ if r != 0: return r
# end for dir, prop in sort, group:
# if all else fails, compare the ids
d['id'] = String()
d['creation'] = hyperdb.Date()
d['activity'] = hyperdb.Date()
- d['creator'] = hyperdb.Link("user")
+ d['creator'] = hyperdb.Link('user')
return d
def addprop(self, **properties):
for react in self.reactors[action]:
react(self.db, self, nodeid, oldvalues)
-class FileClass(Class):
+class FileClass(Class, hyperdb.FileClass):
'''This class defines a large chunk of data. To support this, it has a
mandatory String property "content" which is typically saved off
externally to the hyperdb.
default_mime_type = 'text/plain'
def create(self, **propvalues):
- ''' snaffle the file propvalue and store in a file
+ ''' Snarf the "content" propvalue and store in a file
'''
+ # we need to fire the auditors now, or the content property won't
+ # be in propvalues for the auditors to play with
+ self.fireAuditors('create', None, propvalues)
+
+ # now remove the content property so it's not stored in the db
content = propvalues['content']
del propvalues['content']
- newid = Class.create(self, **propvalues)
+
+ # do the database create
+ newid = Class.create_inner(self, **propvalues)
+
+ # fire reactors
+ self.fireReactors('create', newid, None)
+
+ # store off the content as a file
self.db.storefile(self.classname, newid, None, content)
return newid
# extract the "content" property from the proplist
i = propnames.index('content')
- content = proplist[i]
+ content = eval(proplist[i])
del propnames[i]
del proplist[i]
return newid
def get(self, nodeid, propname, default=_marker, cache=1):
- ''' trap the content propname and get it from the file
- '''
+ ''' Trap the content propname and get it from the file
- poss_msg = 'Possibly a access right configuration problem.'
+ 'cache' exists for backwards compatibility, and is not used.
+ '''
+ poss_msg = 'Possibly an access right configuration problem.'
if propname == 'content':
try:
return self.db.getfile(self.classname, nodeid, None)
except IOError, (strerror):
- # BUG: by catching this we donot see an error in the log.
+ # XXX by catching this we donot see an error in the log.
return 'ERROR reading file: %s%s\n%s\n%s'%(
self.classname, nodeid, poss_msg, strerror)
if default is not _marker:
- return Class.get(self, nodeid, propname, default, cache=cache)
+ return Class.get(self, nodeid, propname, default)
else:
- return Class.get(self, nodeid, propname, cache=cache)
+ return Class.get(self, nodeid, propname)
def getprops(self, protected=1):
''' In addition to the actual properties on the node, these methods
modified.
'''
d = Class.getprops(self, protected=protected).copy()
- if protected:
- d['content'] = hyperdb.String()
+ d['content'] = hyperdb.String()
return d
def index(self, nodeid):
self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
mime_type)
-# XXX deviation from spec - was called ItemClass
+# deviation from spec - was called ItemClass
class IssueClass(Class, roundupdb.IssueClass):
# Overridden methods:
def __init__(self, db, classname, **properties):
Class.__init__(self, db, classname, **properties)
#
-#$Log: not supported by cvs2svn $
-#Revision 1.68 2002/09/03 05:46:20 richard
-#handle disappearing users for journaltags
-#
-#Revision 1.67 2002/09/03 02:53:53 richard
-#Fixed nasty bug that was preventing changes to multilinks going through.
-#
-#Revision 1.66 2002/09/01 04:32:30 richard
-#. Lots of cleanup in the classic html (stylesheet, search page, index page, ...)
-#. Reinstated searching, but not query saving yet
-#. Filtering only allows sorting and grouping by one property - all backends
-# now implement this behaviour.
-#. Nosy list journalling turned off by default, everything else is on.
-#. Added some convenience methods (reverse, propchanged, [item] accesses, ...)
-#. Did I mention the stylesheet is much cleaner now? :)
-#
-#Revision 1.65 2002/08/30 08:35:45 richard
-#minor edits
-#
-#Revision 1.64 2002/08/22 07:57:11 richard
-#Consistent quoting
-#
-#Revision 1.63 2002/08/22 04:42:28 richard
-#use more robust date stamp comparisons in pack(), make journal smaller too
-#
-#Revision 1.62 2002/08/21 07:07:27 richard
-#In preparing to turn back on link/unlink journal events (by default these
-#are turned off) I've:
-#- fixed back_anydbm so it can journal those events again (had broken it
-# with recent changes)
-#- changed the serialisation format for dates and intervals to use a
-# numbers-only (and sign for Intervals) string instead of tuple-of-ints.
-# Much smaller.
-#
-#Revision 1.61 2002/08/19 02:53:27 richard
-#full database export and import is done
-#
-#Revision 1.60 2002/08/19 00:23:19 richard
-#handle "unset" initial Link values (!)
-#
-#Revision 1.59 2002/08/16 04:28:13 richard
-#added is_retired query to Class
-#
-#Revision 1.58 2002/08/01 15:06:24 gmcm
-#Use same regex to split search terms as used to index text.
-#Fix to back_metakit for not changing journaltag on reopen.
-#Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
-#Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
-#
-#Revision 1.57 2002/07/31 23:57:36 richard
-# . web forms may now unset Link values (like assignedto)
-#
-#Revision 1.56 2002/07/31 22:04:33 richard
-#cleanup
-#
-#Revision 1.55 2002/07/30 08:22:38 richard
-#Session storage in the hyperdb was horribly, horribly inefficient. We use
-#a simple anydbm wrapper now - which could be overridden by the metakit
-#backend or RDB backend if necessary.
-#Much, much better.
-#
-#Revision 1.54 2002/07/26 08:26:59 richard
-#Very close now. The cgi and mailgw now use the new security API. The two
-#templates have been migrated to that setup. Lots of unit tests. Still some
-#issue in the web form for editing Roles assigned to users.
-#
-#Revision 1.53 2002/07/25 07:14:06 richard
-#Bugger it. Here's the current shape of the new security implementation.
-#Still to do:
-# . call the security funcs from cgi and mailgw
-# . change shipped templates to include correct initialisation and remove
-# the old config vars
-#... that seems like a lot. The bulk of the work has been done though. Honest :)
-#
-#Revision 1.52 2002/07/19 03:36:34 richard
-#Implemented the destroy() method needed by the session database (and possibly
-#others). At the same time, I removed the leading underscores from the hyperdb
-#methods that Really Didn't Need Them.
-#The journal also raises IndexError now for all situations where there is a
-#request for the journal of a node that doesn't have one. It used to return
-#[] in _some_ situations, but not all. This _may_ break code, but the tests
-#pass...
-#
-#Revision 1.51 2002/07/18 23:07:08 richard
-#Unit tests and a few fixes.
-#
-#Revision 1.50 2002/07/18 11:50:58 richard
-#added tests for number type too
-#
-#Revision 1.49 2002/07/18 11:41:10 richard
-#added tests for boolean type, and fixes to anydbm backend
-#
-#Revision 1.48 2002/07/18 11:17:31 gmcm
-#Add Number and Boolean types to hyperdb.
-#Add conversion cases to web, mail & admin interfaces.
-#Add storage/serialization cases to back_anydbm & back_metakit.
-#
-#Revision 1.47 2002/07/14 23:18:20 richard
-#. fixed the journal bloat from multilink changes - we just log the add or
-# remove operations, not the whole list
-#
-#Revision 1.46 2002/07/14 06:06:34 richard
-#Did some old TODOs
-#
-#Revision 1.45 2002/07/14 04:03:14 richard
-#Implemented a switch to disable journalling for a Class. CGI session
-#database now uses it.
-#
-#Revision 1.44 2002/07/14 02:05:53 richard
-#. all storage-specific code (ie. backend) is now implemented by the backends
-#
-#Revision 1.43 2002/07/10 06:30:30 richard
-#...except of course it's nice to use valid Python syntax
-#
-#Revision 1.42 2002/07/10 06:21:38 richard
-#Be extra safe
-#
-#Revision 1.41 2002/07/10 00:21:45 richard
-#explicit database closing
-#
-#Revision 1.40 2002/07/09 04:19:09 richard
-#Added reindex command to roundup-admin.
-#Fixed reindex on first access.
-#Also fixed reindexing of entries that change.
-#
-#Revision 1.39 2002/07/09 03:02:52 richard
-#More indexer work:
-#- all String properties may now be indexed too. Currently there's a bit of
-# "issue" specific code in the actual searching which needs to be
-# addressed. In a nutshell:
-# + pass 'indexme="yes"' as a String() property initialisation arg, eg:
-# file = FileClass(db, "file", name=String(), type=String(),
-# comment=String(indexme="yes"))
-# + the comment will then be indexed and be searchable, with the results
-# related back to the issue that the file is linked to
-#- as a result of this work, the FileClass has a default MIME type that may
-# be overridden in a subclass, or by the use of a "type" property as is
-# done in the default templates.
-#- the regeneration of the indexes (if necessary) is done once the schema is
-# set up in the dbinit.
-#
-#Revision 1.38 2002/07/08 06:58:15 richard
-#cleaned up the indexer code:
-# - it splits more words out (much simpler, faster splitter)
-# - removed code we'll never use (roundup.roundup_indexer has the full
-# implementation, and replaces roundup.indexer)
-# - only index text/plain and rfc822/message (ideas for other text formats to
-# index are welcome)
-# - added simple unit test for indexer. Needs more tests for regression.
-#
-#Revision 1.37 2002/06/20 23:52:35 richard
-#More informative error message
-#
-#Revision 1.36 2002/06/19 03:07:19 richard
-#Moved the file storage commit into blobfiles where it belongs.
-#
-#Revision 1.35 2002/05/25 07:16:24 rochecompaan
-#Merged search_indexing-branch with HEAD
-#
-#Revision 1.34 2002/05/15 06:21:21 richard
-# . node caching now works, and gives a small boost in performance
-#
-#As a part of this, I cleaned up the DEBUG output and implemented TRACE
-#output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
-#CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
-#(using if __debug__ which is compiled out with -O)
-#
-#Revision 1.33 2002/04/24 10:38:26 rochecompaan
-#All database files are now created group readable and writable.
-#
-#Revision 1.32 2002/04/15 23:25:15 richard
-#. node ids are now generated from a lockable store - no more race conditions
-#
-#We're using the portalocker code by Jonathan Feinberg that was contributed
-#to the ASPN Python cookbook. This gives us locking across Unix and Windows.
-#
-#Revision 1.31 2002/04/03 05:54:31 richard
-#Fixed serialisation problem by moving the serialisation step out of the
-#hyperdb.Class (get, set) into the hyperdb.Database.
-#
-#Also fixed htmltemplate after the showid changes I made yesterday.
-#
-#Unit tests for all of the above written.
-#
-#Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
-# . Added feature #526730 - search for messages capability
-#
-#Revision 1.30 2002/02/27 03:40:59 richard
-#Ran it through pychecker, made fixes
-#
-#Revision 1.29 2002/02/25 14:34:31 grubert
-# . use blobfiles in back_anydbm which is used in back_bsddb.
-# change test_db as dirlist does not work for subdirectories.
-# ATTENTION: blobfiles now creates subdirectories for files.
-#
-#Revision 1.28 2002/02/16 09:14:17 richard
-# . #514854 ] History: "User" is always ticket creator
-#
-#Revision 1.27 2002/01/22 07:21:13 richard
-#. fixed back_bsddb so it passed the journal tests
-#
-#... it didn't seem happy using the back_anydbm _open method, which is odd.
-#Yet another occurrance of whichdb not being able to recognise older bsddb
-#databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
-#process.
-#
-#Revision 1.26 2002/01/22 05:18:38 rochecompaan
-#last_set_entry was referenced before assignment
-#
-#Revision 1.25 2002/01/22 05:06:08 rochecompaan
-#We need to keep the last 'set' entry in the journal to preserve
-#information on 'activity' for nodes.
-#
-#Revision 1.24 2002/01/21 16:33:20 rochecompaan
-#You can now use the roundup-admin tool to pack the database
-#
-#Revision 1.23 2002/01/18 04:32:04 richard
-#Rollback was breaking because a message hadn't actually been written to the file. Needs
-#more investigation.
-#
-#Revision 1.22 2002/01/14 02:20:15 richard
-# . changed all config accesses so they access either the instance or the
-# config attriubute on the db. This means that all config is obtained from
-# instance_config instead of the mish-mash of classes. This will make
-# switching to a ConfigParser setup easier too, I hope.
-#
-#At a minimum, this makes migration a _little_ easier (a lot easier in the
-#0.5.0 switch, I hope!)
-#
-#Revision 1.21 2002/01/02 02:31:38 richard
-#Sorry for the huge checkin message - I was only intending to implement #496356
-#but I found a number of places where things had been broken by transactions:
-# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
-# for _all_ roundup-generated smtp messages to be sent to.
-# . the transaction cache had broken the roundupdb.Class set() reactors
-# . newly-created author users in the mailgw weren't being committed to the db
-#
-#Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
-#on when I found that stuff :):
-# . #496356 ] Use threading in messages
-# . detectors were being registered multiple times
-# . added tests for mailgw
-# . much better attaching of erroneous messages in the mail gateway
-#
-#Revision 1.20 2001/12/18 15:30:34 rochecompaan
-#Fixed bugs:
-# . Fixed file creation and retrieval in same transaction in anydbm
-# backend
-# . Cgi interface now renders new issue after issue creation
-# . Could not set issue status to resolved through cgi interface
-# . Mail gateway was changing status back to 'chatting' if status was
-# omitted as an argument
-#
-#Revision 1.19 2001/12/17 03:52:48 richard
-#Implemented file store rollback. As a bonus, the hyperdb is now capable of
-#storing more than one file per node - if a property name is supplied,
-#the file is called designator.property.
-#I decided not to migrate the existing files stored over to the new naming
-#scheme - the FileClass just doesn't specify the property name.
-#
-#Revision 1.18 2001/12/16 10:53:38 richard
-#take a copy of the node dict so that the subsequent set
-#operation doesn't modify the oldvalues structure
-#
-#Revision 1.17 2001/12/14 23:42:57 richard
-#yuck, a gdbm instance tests false :(
-#I've left the debugging code in - it should be removed one day if we're ever
-#_really_ anal about performace :)
-#
-#Revision 1.16 2001/12/12 03:23:14 richard
-#Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
-#incorrectly identifies a dbm file as a dbhash file on my system. This has
-#been submitted to the python bug tracker as issue #491888:
-#https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
-#
-#Revision 1.15 2001/12/12 02:30:51 richard
-#I fixed the problems with people whose anydbm was using the dbm module at the
-#backend. It turns out the dbm module modifies the file name to append ".db"
-#and my check to determine if we're opening an existing or new db just
-#tested os.path.exists() on the filename. Well, no longer! We now perform a
-#much better check _and_ cope with the anydbm implementation module changing
-#too!
-#I also fixed the backends __init__ so only ImportError is squashed.
-#
-#Revision 1.14 2001/12/10 22:20:01 richard
-#Enabled transaction support in the bsddb backend. It uses the anydbm code
-#where possible, only replacing methods where the db is opened (it uses the
-#btree opener specifically.)
-#Also cleaned up some change note generation.
-#Made the backends package work with pydoc too.
-#
-#Revision 1.13 2001/12/02 05:06:16 richard
-#. We now use weakrefs in the Classes to keep the database reference, so
-# the close() method on the database is no longer needed.
-# I bumped the minimum python requirement up to 2.1 accordingly.
-#. #487480 ] roundup-server
-#. #487476 ] INSTALL.txt
-#
-#I also cleaned up the change message / post-edit stuff in the cgi client.
-#There's now a clearly marked "TODO: append the change note" where I believe
-#the change note should be added there. The "changes" list will obviously
-#have to be modified to be a dict of the changes, or somesuch.
-#
-#More testing needed.
-#
-#Revision 1.12 2001/12/01 07:17:50 richard
-#. We now have basic transaction support! Information is only written to
-# the database when the commit() method is called. Only the anydbm
-# backend is modified in this way - neither of the bsddb backends have been.
-# The mail, admin and cgi interfaces all use commit (except the admin tool
-# doesn't have a commit command, so interactive users can't commit...)
-#. Fixed login/registration forwarding the user to the right page (or not,
-# on a failure)
-#
-#Revision 1.11 2001/11/21 02:34:18 richard
-#Added a target version field to the extended issue schema
-#
-#Revision 1.10 2001/10/09 23:58:10 richard
-#Moved the data stringification up into the hyperdb.Class class' get, set
-#and create methods. This means that the data is also stringified for the
-#journal call, and removes duplication of code from the backends. The
-#backend code now only sees strings.
-#
-#Revision 1.9 2001/10/09 07:25:59 richard
-#Added the Password property type. See "pydoc roundup.password" for
-#implementation details. Have updated some of the documentation too.
-#
-#Revision 1.8 2001/09/29 13:27:00 richard
-#CGI interfaces now spit up a top-level index of all the instances they can
-#serve.
-#
-#Revision 1.7 2001/08/12 06:32:36 richard
-#using isinstance(blah, Foo) now instead of isFooType
-#
-#Revision 1.6 2001/08/07 00:24:42 richard
-#stupid typo
-#
-#Revision 1.5 2001/08/07 00:15:51 richard
-#Added the copyright/license notice to (nearly) all files at request of
-#Bizar Software.
-#
-#Revision 1.4 2001/07/30 01:41:36 richard
-#Makes schema changes mucho easier.
-#
-#Revision 1.3 2001/07/25 01:23:07 richard
-#Added the Roundup spec to the new documentation directory.
-#
-#Revision 1.2 2001/07/23 08:20:44 richard
-#Moved over to using marshal in the bsddb and anydbm backends.
-#roundup-admin now has a "freshen" command that'll load/save all nodes (not
-# retired - mod hyperdb.Class.list() so it lists retired nodes)
-#
-#