Code

Added 'Users may only edit their issues' customisation example.
[roundup.git] / roundup / hyperdb.py
index 52c9accf7659747e30f0608cb2f98aabf3a2d94c..c8ca0a57ff593596bf02f5b1e125f3e93e8ab15f 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: hyperdb.py,v 1.85 2003-02-18 01:57:38 richard Exp $
+# $Id: hyperdb.py,v 1.95 2003-11-16 22:56:46 jlgijsbers Exp $
 
 """
 Hyperdatabase implementation, especially field types.
@@ -163,8 +163,7 @@ transaction.
 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.
 
 '''
 
@@ -188,7 +187,15 @@ concrete backend Class.
         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):
@@ -223,8 +230,8 @@ concrete backend Class.
         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):
@@ -245,6 +252,8 @@ concrete backend Class.
 
     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
 
@@ -258,11 +267,6 @@ concrete backend 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.
         
@@ -365,25 +369,25 @@ class Class:
         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, cache=cache)
+        return Node(self, nodeid)
+
+    def getnodeids(self, db=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.
@@ -416,6 +420,13 @@ class Class:
         """
         raise NotImplementedError
 
+    def restore(self, nodeid):
+        '''Restpre a retired node.
+
+        Make node available for all operations like it was before retirement.
+        '''
+        raise NotImplementedError
+    
     def is_retired(self, nodeid):
         '''Return true if the node is rerired
         '''
@@ -561,6 +572,181 @@ class 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.
@@ -573,26 +759,30 @@ class Node:
     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)
+    def get(self, name, default=None): 
+        if self.has_key(name):
+            return self[name]
+        else:
+            return default
     def __getattr__(self, 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
@@ -600,7 +790,7 @@ class Node:
         # 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})