diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py
index 4ab22268aafdf57cd4b7b98022dad397576246ea..c8ca0a57ff593596bf02f5b1e125f3e93e8ab15f 100644 (file)
--- a/roundup/hyperdb.py
+++ b/roundup/hyperdb.py
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: hyperdb.py,v 1.87 2003-03-17 22:03:03 kedder Exp $
+# $Id: hyperdb.py,v 1.95 2003-11-16 22:56:46 jlgijsbers Exp $
"""
Hyperdatabase implementation, especially field types.
Implementation
--------------
-All methods except __repr__ and getnode must be implemented by a
-concrete backend Class.
+All methods except __repr__ must be implemented by a concrete backend Database.
'''
raise NotImplementedError
def post_init(self):
- """Called once the schema initialisation has finished."""
+ """Called once the schema initialisation has finished.
+ If 'refresh' is true, we want to rebuild the backend
+ structures.
+ """
+ raise NotImplementedError
+
+ def refresh_database(self):
+ """Called to indicate that the backend should rebuild all tables
+ and structures. Not called in normal usage."""
raise NotImplementedError
def __getattr__(self, classname):
raise NotImplementedError
def addnode(self, classname, nodeid, node):
- '''Add the specified node to its class's db.
- '''
+ """Add the specified node to its class's db.
+ """
raise NotImplementedError
def serialise(self, classname, node):
def getnode(self, classname, nodeid, db=None, cache=1):
'''Get a node from the database.
+
+ 'cache' exists for backwards compatibility, and is not used.
'''
raise NotImplementedError
'''
raise NotImplementedError
- def getnodeids(self, classname, db=None):
- '''Retrieve all the ids of the nodes for a particular Class.
- '''
- raise NotImplementedError
-
def storefile(self, classname, nodeid, property, content):
'''Store the content of the file in the database.
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 backwards compatibility, and is not used.
"""
raise NotImplementedError
+ # 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)
+
+ def getnodeids(self, db=None):
+ '''Retrieve all the ids of the nodes for a particular Class.
'''
- return Node(self, nodeid, cache=cache)
+ raise NotImplementedError
def set(self, nodeid, **propvalues):
"""Modify a property on an existing node of this class.
'''
raise NotImplementedError
+ def safeget(self, nodeid, propname, default=None):
+ """Safely get the value of a property on an existing node of this class.
+
+ Return 'default' if the node doesn't exist.
+ """
+ try:
+ return self.get(nodeid, propname)
+ except IndexError:
+ return default
+
+class HyperdbValueError(ValueError):
+ ''' Error converting a raw value into a Hyperdb value '''
+ pass
+
+def convertLinkValue(db, propname, prop, value, idre=re.compile('\d+')):
+ ''' Convert the link value (may be id or key value) to an id value. '''
+ linkcl = db.classes[prop.classname]
+ if not idre.match(value):
+ if linkcl.getkey():
+ try:
+ value = linkcl.lookup(value)
+ except KeyError, message:
+ raise HyperdbValueError, 'property %s: %r is not a %s.'%(
+ propname, value, prop.classname)
+ else:
+ raise HyperdbValueError, 'you may only enter ID values '\
+ 'for property %s'%propname
+ return value
+
+def fixNewlines(text):
+ """ Homogenise line endings.
+
+ Different web clients send different line ending values, but
+ other systems (eg. email) don't necessarily handle those line
+ endings. Our solution is to convert all line endings to LF.
+ """
+ text = text.replace('\r\n', '\n')
+ return text.replace('\r', '\n')
+
+def rawToHyperdb(db, klass, itemid, propname, value,
+ pwre=re.compile(r'{(\w+)}(.+)')):
+ ''' Convert the raw (user-input) value to a hyperdb-storable value. The
+ value is for the "propname" property on itemid (may be None for a
+ new item) of "klass" in "db".
+
+ The value is usually a string, but in the case of multilink inputs
+ it may be either a list of strings or a string with comma-separated
+ values.
+ '''
+ properties = klass.getprops()
+
+ # ensure it's a valid property name
+ propname = propname.strip()
+ try:
+ proptype = properties[propname]
+ except KeyError:
+ raise HyperdbValueError, '%r is not a property of %s'%(propname,
+ klass.classname)
+
+ # if we got a string, strip it now
+ if isinstance(value, type('')):
+ value = value.strip()
+
+ # convert the input value to a real property value
+ if isinstance(proptype, String):
+ # fix the CRLF/CR -> LF stuff
+ value = fixNewlines(value)
+ if isinstance(proptype, Password):
+ m = pwre.match(value)
+ if m:
+ # password is being given to us encrypted
+ p = password.Password()
+ p.scheme = m.group(1)
+ if p.scheme not in 'SHA crypt plaintext'.split():
+ raise HyperdbValueError, 'property %s: unknown encryption '\
+ 'scheme %r'%(propname, p.scheme)
+ p.password = m.group(2)
+ value = p
+ else:
+ try:
+ value = password.Password(value)
+ except password.PasswordValueError, message:
+ raise HyperdbValueError, 'property %s: %s'%(propname, message)
+ elif isinstance(proptype, Date):
+ try:
+ tz = db.getUserTimezone()
+ value = date.Date(value).local(tz)
+ except ValueError, message:
+ raise HyperdbValueError, 'property %s: %r is an invalid '\
+ 'date (%s)'%(propname, value, message)
+ elif isinstance(proptype, Interval):
+ try:
+ value = date.Interval(value)
+ except ValueError, message:
+ raise HyperdbValueError, 'property %s: %r is an invalid '\
+ 'date interval (%s)'%(propname, value, message)
+ elif isinstance(proptype, Link):
+ if value == '-1' or not value:
+ value = None
+ else:
+ value = convertLinkValue(db, propname, proptype, value)
+
+ elif isinstance(proptype, Multilink):
+ # get the current item value if it's not a new item
+ if itemid and not itemid.startswith('-'):
+ curvalue = klass.get(itemid, propname)
+ else:
+ curvalue = []
+
+ # if the value is a comma-separated string then split it now
+ if isinstance(value, type('')):
+ value = value.split(',')
+
+ # handle each add/remove in turn
+ # keep an extra list for all items that are
+ # definitely in the new list (in case of e.g.
+ # <propname>=A,+B, which should replace the old
+ # list with A,B)
+ set = 1
+ newvalue = []
+ for item in value:
+ item = item.strip()
+
+ # skip blanks
+ if not item: continue
+
+ # handle +/-
+ remove = 0
+ if item.startswith('-'):
+ remove = 1
+ item = item[1:]
+ set = 0
+ elif item.startswith('+'):
+ item = item[1:]
+ set = 0
+
+ # look up the value
+ itemid = convertLinkValue(db, propname, proptype, item)
+
+ # perform the add/remove
+ if remove:
+ try:
+ curvalue.remove(itemid)
+ except ValueError:
+ raise HyperdbValueError, 'property %s: %r is not ' \
+ 'currently an element'%(propname, item)
+ else:
+ newvalue.append(itemid)
+ if itemid not in curvalue:
+ curvalue.append(itemid)
+
+ # that's it, set the new Multilink property value,
+ # or overwrite it completely
+ if set:
+ value = newvalue
+ else:
+ value = curvalue
+
+ # TODO: one day, we'll switch to numeric ids and this will be
+ # unnecessary :(
+ value = [int(x) for x in value]
+ value.sort()
+ value = [str(x) for x in value]
+ elif isinstance(proptype, Boolean):
+ value = value.strip()
+ value = value.lower() in ('yes', 'true', 'on', '1')
+ elif isinstance(proptype, Number):
+ value = value.strip()
+ try:
+ value = float(value)
+ except ValueError:
+ raise HyperdbValueError, 'property %s: %r is not a number'%(
+ propname, value)
+ return value
+
class FileClass:
''' A class that requires the "content" property and stores it on
disk.
def __init__(self, cl, nodeid, cache=1):
self.__dict__['cl'] = cl
self.__dict__['nodeid'] = nodeid
- self.__dict__['cache'] = cache
def keys(self, protected=1):
return self.cl.getprops(protected=protected).keys()
def values(self, protected=1):
l = []
for name in self.cl.getprops(protected=protected).keys():
- l.append(self.cl.get(self.nodeid, name, cache=self.cache))
+ l.append(self.cl.get(self.nodeid, name))
return l
def items(self, protected=1):
l = []
for name in self.cl.getprops(protected=protected).keys():
- l.append((name, self.cl.get(self.nodeid, name, cache=self.cache)))
+ l.append((name, self.cl.get(self.nodeid, name)))
return l
def has_key(self, name):
return self.cl.getprops().has_key(name)
if self.__dict__.has_key(name):
return self.__dict__[name]
try:
- return self.cl.get(self.nodeid, name, cache=self.cache)
+ return self.cl.get(self.nodeid, name)
except KeyError, value:
# we trap this but re-raise it as AttributeError - all other
# exceptions should pass through untrapped
# nope, no such attribute
raise AttributeError, str(value)
def __getitem__(self, name):
- return self.cl.get(self.nodeid, name, cache=self.cache)
+ return self.cl.get(self.nodeid, name)
def __setattr__(self, name, value):
try:
return self.cl.set(self.nodeid, **{name: value})