diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py
index a524b1937563f7fbb03db15ba5b8adcb391144e5..4bac87fc63039f2f258e2a26737990063e101f3b 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.89 2003-10-07 11:58:57 anthonybaxter Exp $
+# $Id: hyperdb.py,v 1.96 2004-02-11 23:55:08 richard Exp $
+"""Hyperdatabase implementation, especially field types.
"""
-Hyperdatabase implementation, especially field types.
-"""
+__docformat__ = 'restructuredtext'
# standard python modules
import sys, os, time, re
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 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):
'''
return node
- def getnode(self, classname, nodeid, db=None, cache=1):
+ def getnode(self, classname, nodeid):
'''Get a node from the database.
'cache' exists for backwards compatibility, and is not used.
'''
raise NotImplementedError
- def hasnode(self, classname, nodeid, db=None):
+ def hasnode(self, classname, nodeid):
'''Determine if the database has a given node.
'''
raise NotImplementedError
- def countnodes(self, classname, db=None):
+ def countnodes(self, classname):
'''Count the number of nodes that exist for a particular Class.
'''
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.
"""
raise NotImplementedError
- def getnode(self, nodeid, cache=1):
+ # not in spec
+ def getnode(self, nodeid):
''' Return a convenience wrapper for the node.
'nodeid' must be the id of an existing node of this class or an
'''
return Node(self, nodeid)
+ def getnodeids(self, retired=None):
+ '''Retrieve all the ids of the nodes for a particular Class.
+ '''
+ raise NotImplementedError
+
def set(self, nodeid, **propvalues):
"""Modify a property on an existing node of this class.
WARNING: this method should never be used except in extremely rare
situations where there could never be links to the node being
deleted
+
WARNING: use retire() instead
+
WARNING: the properties of this node will not be available ever again
+
WARNING: really, use retire() instead
Well, I think that's enough warnings. This method exists mostly to
raise NotImplementedError
def labelprop(self, default_to_id=0):
- ''' Return the property name for a label for the given node.
+ """Return the property name for a label for the given node.
This method attempts to generate a consistent label for the node.
It tries the following in order:
- 1. key property
- 2. "name" property
- 3. "title" property
- 4. first property from the sorted property name list
- '''
+
+ 1. key property
+ 2. "name" property
+ 3. "title" property
+ 4. first property from the sorted property name list
+ """
raise NotImplementedError
def lookup(self, keyvalue):
def filter(self, search_matches, filterspec, sort=(None,None),
group=(None,None)):
- ''' 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.
-
- "filterspec" is {propname: value(s)}
- "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.
- '''
+ """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.
+
+ "filterspec" is {propname: value(s)}
+
+ "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.
+ """
raise NotImplementedError
def count(self):
'''
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.