Code

- Remove implementations of Class.getnode from back_anydbm and rdbms_common,
[roundup.git] / roundup / backends / back_anydbm.py
index b4124439ace17d12eb955e8d4edf3535a4e4a941..d6aebc43da3d022ad9d1a4cc8123de59a2cccdb8 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.114 2003-03-26 04:56:21 richard Exp $
+#$Id: back_anydbm.py,v 1.132 2003-11-16 18:41:40 jlgijsbers 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
@@ -23,7 +23,17 @@ versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
 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
@@ -88,14 +98,9 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         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():
@@ -251,7 +256,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # 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
@@ -283,17 +288,20 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
     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)
@@ -481,6 +489,25 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         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:
@@ -489,15 +516,23 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             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
@@ -547,15 +582,16 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # 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()):
@@ -618,7 +654,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if creator:
             journaltag = creator
         else:
-            journaltag = self.curuserid
+            journaltag = self.getuid()
         if creation:
             journaldate = creation.serialise()
         else:
@@ -904,7 +940,7 @@ class Class(hyperdb.Class):
             l.append(repr(value))
 
         # append retired flag
-        l.append(self.is_retired(nodeid))
+        l.append(repr(self.is_retired(nodeid)))
 
         return l
 
@@ -940,7 +976,7 @@ class Class(hyperdb.Class):
                     d[self.db.RETIRED_FLAG] = 1
                 continue
             elif value is None:
-                # don't set Nones
+                d[propname] = None
                 continue
 
             prop = properties[propname]
@@ -985,10 +1021,7 @@ class Class(hyperdb.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 backward compatibility, and is not used.
 
         Attempts to get the "creation" or "activity" properties should
         do the right thing.
@@ -997,7 +1030,7 @@ class Class(hyperdb.Class):
             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':
@@ -1041,7 +1074,7 @@ class Class(hyperdb.Class):
                         # 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]
@@ -1061,20 +1094,6 @@ class Class(hyperdb.Class):
 
         return d[propname]
 
-    # 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.
-        '''
-        return Node(self, nodeid, cache=cache)
-
     def set(self, nodeid, **propvalues):
         '''Modify a property on an existing node of this class.
         
@@ -1111,14 +1130,7 @@ class Class(hyperdb.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):
@@ -1462,23 +1474,23 @@ class Class(hyperdb.Class):
 
     # 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):
@@ -1489,24 +1501,26 @@ class Class(hyperdb.Class):
         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
@@ -1614,6 +1628,7 @@ class Class(hyperdb.Class):
         MULTILINK = 1
         STRING = 2
         DATE = 3
+        INTERVAL = 4
         OTHER = 6
         
         timezone = self.db.getUserTimezone()
@@ -1640,7 +1655,7 @@ class Class(hyperdb.Class):
                 l.append((LINK, k, u))
             elif isinstance(propclass, Multilink):
                 # the value -1 is a special "not set" sentinel
-                if v == '-1':
+                if v in ('-1', ['-1']):
                     v = []
                 elif type(v) is not type([]):
                     v = [v]
@@ -1659,29 +1674,38 @@ class Class(hyperdb.Class):
                 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                            
+                    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))
-            # kedder: dates are filtered by ranges
-            #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:
@@ -1732,16 +1756,19 @@ class Class(hyperdb.Class):
                             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
-                    elif t == DATE:
-                        if node[k] is None: break
                         if v.to_value:
-                            if not (v.from_value < node[k] and v.to_value > node[k]):
+                            if not (v.from_value <= node[k] and v.to_value >= node[k]):
                                 break
                         else:
-                            if not (v.from_value < node[k]):
+                            if not (v.from_value <= node[k]):
                                 break
                     elif t == OTHER:
                         # straight value comparison for the other types
@@ -1848,20 +1875,6 @@ class Class(hyperdb.Class):
                             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 r:
-                        if dir == '+':
-                            return r
-                        else:
-                            return -r
-
                 else:
                     # all other types just compare
                     if dir == '+':
@@ -2016,7 +2029,9 @@ class FileClass(Class, hyperdb.FileClass):
         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':
@@ -2027,9 +2042,9 @@ class FileClass(Class, hyperdb.FileClass):
                 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