Code

Fixed a backlog of bug reports, and worked on python 2.3 compatibility:
[roundup.git] / roundup / backends / back_anydbm.py
index e50a9603c0366e5125470da4489b1e2f485a7336..681698b389d29fe04aef13b101c938d224745e18 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.80 2002-09-17 23:59:59 richard Exp $
+#$Id: back_anydbm.py,v 1.100 2003-02-06 05:43:47 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
@@ -28,7 +28,7 @@ from roundup import hyperdb, date, password, roundupdb, security
 from blobfiles import FileStorage
 from sessions import Sessions
 from roundup.indexer import Indexer
-from locking import acquire_lock, release_lock
+from roundup.backends import locking
 from roundup.hyperdb import String, Password, Date, Interval, Link, \
     Multilink, DatabaseError, Boolean, Number
 
@@ -72,12 +72,28 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # ensure files are group readable and writable
         os.umask(0002)
 
+        # lock it
+        lockfilenm = os.path.join(self.dir, 'lock')
+        self.lockfile = locking.acquire_lock(lockfilenm)
+        self.lockfile.write(str(os.getpid()))
+        self.lockfile.flush()
+
     def post_init(self):
-        '''Called once the schema initialisation has finished.'''
+        ''' Called once the schema initialisation has finished.
+        '''
         # reindex the db if necessary
         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 reindex(self):
         for klass in self.classes.values():
             for nodeid in klass.list():
@@ -193,12 +209,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 mode)
         return dbm.open(path, mode)
 
-    def lockdb(self, name):
-        ''' Lock a database file
-        '''
-        path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
-        return acquire_lock(path)
-
     #
     # Node IDs
     #
@@ -206,7 +216,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         ''' Generate a new id for the given class
         '''
         # open the ids DB - create if if doesn't exist
-        lock = self.lockdb('_ids')
         db = self.opendb('_ids', 'c')
         if db.has_key(classname):
             newid = db[classname] = str(int(db[classname]) + 1)
@@ -215,18 +224,15 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             newid = str(self.getclass(classname).count()+1)
             db[classname] = newid
         db.close()
-        release_lock(lock)
         return newid
 
     def setid(self, classname, setid):
         ''' Set the id counter: used during import of database
         '''
         # open the ids DB - create if if doesn't exist
-        lock = self.lockdb('_ids')
         db = self.opendb('_ids', 'c')
         db[classname] = str(setid)
         db.close()
-        release_lock(lock)
 
     #
     # Nodes
@@ -236,6 +242,15 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         if __debug__:
             print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
+
+        # we'll be supplied these props if we're doing an import
+        if not node.has_key('creator'):
+            # add in the "calculated" properties (dupe so we don't affect
+            # calling code's node assumptions)
+            node = node.copy()
+            node['creator'] = self.curuserid
+            node['creation'] = node['activity'] = date.Date()
+
         self.newnodes.setdefault(classname, {})[nodeid] = 1
         self.cache.setdefault(classname, {})[nodeid] = node
         self.savenode(classname, nodeid, node)
@@ -247,6 +262,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
         self.dirtynodes.setdefault(classname, {})[nodeid] = 1
 
+        # update the activity time (dupe so we don't affect
+        # calling code's node assumptions)
+        node = node.copy()
+        node['activity'] = date.Date()
+
         # can't set without having already loaded the node
         self.cache[classname][nodeid] = node
         self.savenode(classname, nodeid, node)
@@ -342,7 +362,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # get the property spec
             prop = properties[k]
 
-            if isinstance(prop, Password):
+            if isinstance(prop, Password) and v is not None:
                 d[k] = str(v)
             elif isinstance(prop, Date) and v is not None:
                 d[k] = v.serialise()
@@ -373,7 +393,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 d[k] = date.Date(v)
             elif isinstance(prop, Interval) and v is not None:
                 d[k] = date.Interval(v)
-            elif isinstance(prop, Password):
+            elif isinstance(prop, Password) and v is not None:
                 p = password.Password()
                 p.unpack(v)
                 d[k] = p
@@ -500,6 +520,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if __debug__:
             print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
 
+        pack_before = pack_before.serialise()
         for classname in self.getclasses():
             # get the journal db
             db_name = 'journals.%s'%classname
@@ -516,21 +537,10 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                     # unpack the entry
                     (nodeid, date_stamp, self.journaltag, action, 
                         params) = entry
-                    date_stamp = date.Date(date_stamp)
                     # if the entry is after the pack date, _or_ the initial
                     # create entry, then it stays
                     if date_stamp > pack_before or action == 'create':
                         l.append(entry)
-                    elif action == 'set':
-                        # grab the last set entry to keep information on
-                        # activity
-                        last_set_entry = entry
-                if last_set_entry:
-                    date_stamp = last_set_entry[1]
-                    # if the last set entry was made after the pack date
-                    # then it is already in the list
-                    if date_stamp < pack_before:
-                        l.append(last_set_entry)
                 db[key] = marshal.dumps(l)
             if db_type == 'gdbm':
                 db.reorganize()
@@ -545,7 +555,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         if __debug__:
             print >>hyperdb.DEBUG, 'commit', (self,)
-        # TODO: lock the DB
 
         # keep a handle to all the database files opened
         self.databases = {}
@@ -559,7 +568,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         for db in self.databases.values():
             db.close()
         del self.databases
-        # TODO: unlock the DB
 
         # reindex the nodes that request it
         for classname, nodeid in filter(None, reindex.keys()):
@@ -569,6 +577,9 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # save the indexer state
         self.indexer.save_index()
 
+        self.clearCache()
+
+    def clearCache(self):
         # all transactions committed, back to normal
         self.cache = {}
         self.dirtynodes = {}
@@ -619,7 +630,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if creator:
             journaltag = creator
         else:
-            journaltag = self.journaltag
+            journaltag = self.curuserid
         if creation:
             journaldate = creation.serialise()
         else:
@@ -679,7 +690,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def close(self):
         ''' Nothing to do
         '''
-        pass
+        if self.lockfile is not None:
+            locking.release_lock(self.lockfile)
+        if self.lockfile is not None:
+            self.lockfile.close()
+            self.lockfile = None
 
 _marker = []
 class Class(hyperdb.Class):
@@ -830,7 +845,7 @@ class Class(hyperdb.Class):
                             (self.classname, newid, key))
 
             elif isinstance(prop, String):
-                if type(value) != type(''):
+                if type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%key
 
             elif isinstance(prop, Password):
@@ -871,7 +886,7 @@ class Class(hyperdb.Class):
         # done
         self.db.addnode(self.classname, newid, propvalues)
         if self.do_journal:
-            self.db.addjournal(self.classname, newid, 'create', propvalues)
+            self.db.addjournal(self.classname, newid, 'create', {})
 
         self.fireReactors('create', newid, None)
 
@@ -937,7 +952,10 @@ class Class(hyperdb.Class):
                 value = pwd
             d[propname] = value
 
-        # extract the extraneous journalling gumpf and nuke it
+        # add the node and journal
+        self.db.addnode(self.classname, newid, d)
+
+        # extract the journalling stuff and nuke it
         if d.has_key('creator'):
             creator = d['creator']
             del d['creator']
@@ -950,10 +968,7 @@ class Class(hyperdb.Class):
             creation = None
         if d.has_key('activity'):
             del d['activity']
-
-        # add the node and journal
-        self.db.addnode(self.classname, newid, d)
-        self.db.addjournal(self.classname, newid, 'create', d, creator,
+        self.db.addjournal(self.classname, newid, 'create', {}, creator,
             creation)
         return newid
 
@@ -975,7 +990,13 @@ class Class(hyperdb.Class):
         if propname == 'id':
             return nodeid
 
+        # get the node's dict
+        d = self.db.getnode(self.classname, nodeid, cache=cache)
+
+        # check for one of the special props
         if propname == 'creation':
+            if d.has_key('creation'):
+                return d['creation']
             if not self.do_journal:
                 raise ValueError, 'Journalling is disabled for this class'
             journal = self.db.getjournal(self.classname, nodeid)
@@ -985,6 +1006,8 @@ class Class(hyperdb.Class):
                 # on the strange chance that there's no journal
                 return date.Date()
         if propname == 'activity':
+            if d.has_key('activity'):
+                return d['activity']
             if not self.do_journal:
                 raise ValueError, 'Journalling is disabled for this class'
             journal = self.db.getjournal(self.classname, nodeid)
@@ -994,20 +1017,29 @@ class Class(hyperdb.Class):
                 # on the strange chance that there's no journal
                 return date.Date()
         if propname == 'creator':
+            if d.has_key('creator'):
+                return d['creator']
             if not self.do_journal:
                 raise ValueError, 'Journalling is disabled for this class'
             journal = self.db.getjournal(self.classname, nodeid)
             if journal:
-                return self.db.getjournal(self.classname, nodeid)[0][2]
+                num_re = re.compile('^\d+$')
+                value = self.db.getjournal(self.classname, nodeid)[0][2]
+                if num_re.match(value):
+                    return value
+                else:
+                    # old-style "username" journal tag
+                    try:
+                        return self.db.user.lookup(value)
+                    except KeyError:
+                        # user's been retired, return admin
+                        return '1'
             else:
-                return self.db.journaltag
+                return self.db.curuserid
 
         # get the property (raises KeyErorr if invalid)
         prop = self.properties[propname]
 
-        # get the node's dict
-        d = self.db.getnode(self.classname, nodeid, cache=cache)
-
         if not d.has_key(propname):
             if default is _marker:
                 if isinstance(prop, Multilink):
@@ -1103,13 +1135,19 @@ class Class(hyperdb.Class):
             # this will raise the KeyError if the property isn't valid
             # ... we don't use getprops() here because we only care about
             # the writeable properties.
-            prop = self.properties[propname]
+            try:
+                prop = self.properties[propname]
+            except KeyError:
+                raise KeyError, '"%s" has no property named "%s"'%(
+                    self.classname, propname)
 
             # if the value's the same as the existing value, no sense in
             # doing anything
-            if node.has_key(propname) and value == node[propname]:
+            current = node.get(propname, None)
+            if value == current:
                 del propvalues[propname]
                 continue
+            journalvalues[propname] = current
 
             # do stuff based on the prop type
             if isinstance(prop, Link):
@@ -1131,7 +1169,7 @@ class Class(hyperdb.Class):
 
                 if self.do_journal and prop.do_journal:
                     # register the unlink with the old linked node
-                    if node[propname] is not None:
+                    if node.has_key(propname) and node[propname] is not None:
                         self.db.addjournal(link_class, node[propname], 'unlink',
                             (self.classname, nodeid, propname))
 
@@ -1204,7 +1242,7 @@ class Class(hyperdb.Class):
                     journalvalues[propname] = tuple(l)
 
             elif isinstance(prop, String):
-                if value is not None and type(value) != type(''):
+                if value is not None and type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%propname
 
             elif isinstance(prop, Password):
@@ -1245,8 +1283,7 @@ class Class(hyperdb.Class):
         self.db.setnode(self.classname, nodeid, node)
 
         if self.do_journal:
-            propvalues.update(journalvalues)
-            self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+            self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
 
         self.fireReactors('set', nodeid, oldvalues)
 
@@ -1287,7 +1324,7 @@ class Class(hyperdb.Class):
 
     def destroy(self, nodeid):
         '''Destroy a node.
-        
+
         WARNING: this method should never be used except in extremely rare
                  situations where there could never be links to the node being
                  deleted
@@ -1310,7 +1347,7 @@ class Class(hyperdb.Class):
 
         The returned list contains tuples of the form
 
-            (date, tag, action, params)
+            (nodeid, date, tag, action, params)
 
         'date' is a Timestamp object specifying the time of the change and
         'tag' is the journaltag specified when the database was opened.
@@ -1384,24 +1421,27 @@ class Class(hyperdb.Class):
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
                 if node[self.key] == keyvalue:
-                    cldb.close()
                     return nodeid
         finally:
             cldb.close()
-        raise KeyError, keyvalue
+        raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
+            keyvalue, self.classname)
 
     # 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.
 
-        'propspec' consists of keyword args propname={nodeid: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.
+        'propspec' consists of keyword args propname=nodeid or
+                   propname={nodeid: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
-        that "foo" occurs in msg1, msg3 and file7, so we have hits on these issues:
+        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()
@@ -1464,6 +1504,8 @@ class Class(hyperdb.Class):
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
                 for key, value in requirements.items():
+                    if not node.has_key(key):
+                        break
                     if node[key] is None or node[key].lower() != value:
                         break
                 else:
@@ -1489,8 +1531,8 @@ class Class(hyperdb.Class):
         l.sort()
         return l
 
-    def filter(self, search_matches, filterspec, sort, group, 
-            num_re = re.compile('^\d+$')):
+    def filter(self, search_matches, filterspec, sort=(None,None),
+            group=(None,None), num_re = re.compile('^\d+$')):
         ''' 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.
@@ -1499,6 +1541,10 @@ class Class(hyperdb.Class):
             "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.
         '''
         cn = self.classname
 
@@ -1543,7 +1589,7 @@ class Class(hyperdb.Class):
                                 k, entry, self.properties[k].classname)
                     u.append(entry)
                 l.append((MULTILINK, k, u))
-            elif isinstance(propclass, String):
+            elif isinstance(propclass, String) and k != 'id':
                 # simple glob searching
                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
                 v = v.replace('?', '.')
@@ -1555,6 +1601,10 @@ class Class(hyperdb.Class):
                 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:
@@ -1572,6 +1622,10 @@ class Class(hyperdb.Class):
                     continue
                 # apply filter
                 for t, k, v in filterspec:
+                    # handle the id prop
+                    if k == 'id' and v == nodeid:
+                        continue
+
                     # make sure the node has the property
                     if not node.has_key(k):
                         # this node doesn't have this property, so reject it
@@ -1651,9 +1705,9 @@ class Class(hyperdb.Class):
                 if isinstance(propclass, String):
                     # clean up the strings
                     if av and av[0] in string.uppercase:
-                        av = an[prop] = av.lower()
+                        av = av.lower()
                     if bv and bv[0] in string.uppercase:
-                        bv = bn[prop] = bv.lower()
+                        bv = bv.lower()
                 if (isinstance(propclass, String) or
                         isinstance(propclass, Date)):
                     # it might be a string that's really an integer
@@ -1706,12 +1760,15 @@ class Class(hyperdb.Class):
                 # 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 == '+':
-                        r = cmp(len(av), len(bv))
-                        if r != 0: return r
+                        return r
                     elif dir == '-':
-                        r = cmp(len(bv), len(av))
-                        if r != 0: return r
+                        return -r
                 elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
                     if dir == '+':
                         r = cmp(av, bv)
@@ -1751,9 +1808,7 @@ class Class(hyperdb.Class):
             d['id'] = String()
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
-            # can't be a link to user because the user might have been
-            # retired since the journal entry was created
-            d['creator'] = hyperdb.String()
+            d['creator'] = hyperdb.Link('user')
         return d
 
     def addprop(self, **properties):
@@ -1856,13 +1911,12 @@ class FileClass(Class):
     def get(self, nodeid, propname, default=_marker, cache=1):
         ''' trap the content propname and get it from the file
         '''
-
-        poss_msg = 'Possibly a access right configuration problem.'
+        poss_msg = 'Possibly an access right configuration problem.'
         if propname == 'content':
             try:
                 return self.db.getfile(self.classname, nodeid, None)
             except IOError, (strerror):
-                # BUG: by catching this we donot see an error in the log.
+                # XXX by catching this we donot see an error in the log.
                 return 'ERROR reading file: %s%s\n%s\n%s'%(
                         self.classname, nodeid, poss_msg, strerror)
         if default is not _marker: