index df10104c4df459b96aaa39f062af060b75a81aab..d2897addf497c0f019d6cd5bbae4bf8edf6d6eff 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-#$Id: back_anydbm.py,v 1.108 2003-03-03 21:05:17 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, OneTimeKeys
from roundup.backends import locking
from roundup.hyperdb import String, Password, Date, Interval, Link, \
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 = {}
if self.indexer.should_reindex():
self.reindex()
- # figure the "curuserid"
- if self.journaltag is None:
- self.curuserid = None
- elif self.journaltag == 'admin':
- # admin user may not exist, but always has ID 1
- self.curuserid = '1'
- else:
- self.curuserid = self.user.lookup(self.journaltag)
+ def refresh_database(self):
+ "Rebuild the database"
+ self.reindex()
def reindex(self):
for klass in self.classes.values():
# add in the "calculated" properties (dupe so we don't affect
# calling code's node assumptions)
node = node.copy()
- node['creator'] = self.curuserid
+ node['creator'] = self.getuid()
node['creation'] = node['activity'] = date.Date()
self.newnodes.setdefault(classname, {})[nodeid] = 1
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 __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
# 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
+ 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()):
if creator:
journaltag = creator
else:
- journaltag = self.curuserid
+ journaltag = self.getuid()
if creation:
journaldate = creation.serialise()
else:
# 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
l.append(repr(value))
# append retired flag
- l.append(self.is_retired(nodeid))
+ l.append(repr(self.is_retired(nodeid)))
return l
# 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 propname == 'is retired':
+ # is the item retired?
+ if int(value):
+ d[self.db.RETIRED_FLAG] = 1
+ continue
elif value is None:
- # don't set Nones
+ d[propname] = None
continue
- elif isinstance(prop, hyperdb.Date):
+
+ prop = properties[propname]
+ if isinstance(prop, hyperdb.Date):
value = date.Date(value)
elif isinstance(prop, hyperdb.Interval):
value = date.Interval(value)
value = pwd
d[propname] = value
- # check retired flag
- if int(proplist[-1]):
- d[self.db.RETIRED_FLAG] = 1
+ # 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)
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.
return nodeid
# get the node's dict
- d = self.db.getnode(self.classname, nodeid, cache=cache)
+ d = self.db.getnode(self.classname, nodeid)
# check for one of the special props
if propname == 'creation':
# user's been retired, return admin
return '1'
else:
- return self.db.curuserid
+ return self.db.getuid()
# get the property (raises KeyErorr if invalid)
prop = self.properties[propname]
'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):
self.fireReactors('retire', nodeid, None)
+ 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.
'''
# 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=nodeid or
- propname={nodeid:1, }
+ '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.
- 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
+ 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:
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):
l = []
try:
for id in self.getnodeids(db=cldb):
- node = self.db.getnode(self.classname, id, db=cldb)
- if node.has_key(self.db.RETIRED_FLAG):
+ 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
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.
+ 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) and k != 'id':
- # 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)))
+ 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')
else:
bv = v
l.append((OTHER, k, bv))
- elif isinstance(propclass, Date):
- l.append((OTHER, k, date.Date(v)))
- elif isinstance(propclass, Interval):
- l.append((OTHER, k, date.Interval(v)))
elif isinstance(propclass, Number):
l.append((OTHER, k, int(v)))
else:
# 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:
r = cmp(bv, av)
if r != 0: return r
- # Multilink properties are sorted according to how many
- # links are present.
- elif isinstance(propclass, Multilink):
- r = cmp(len(av), len(bv))
- if r == 0:
- # Compare contents of multilink property if lenghts is
- # equal
- r = cmp ('.'.join(av), '.'.join(bv))
- if dir == '+':
- return r
- elif dir == '-':
- 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
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
+
+ 'cache' exists for backwards compatibility, and is not used.
'''
poss_msg = 'Possibly an access right configuration problem.'
if propname == 'content':
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