Code

adding the "minimal" template
[roundup.git] / roundup / backends / back_anydbm.py
index 97d6b893bb5188af14728cc24e4be065c409006d..ea2533a31b86ad70185de6f407f2f9e0952664ac 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.67 2002-09-03 02:53:53 richard Exp $
+#$Id: back_anydbm.py,v 1.86 2002-09-26 03:04:24 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
@@ -73,11 +73,21 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         os.umask(0002)
 
     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():
@@ -121,7 +131,10 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         if __debug__:
             print >>hyperdb.DEBUG, 'getclass', (self, classname)
-        return self.classes[classname]
+        try:
+            return self.classes[classname]
+        except KeyError:
+            raise KeyError, 'There is no class called "%s"'%classname
 
     #
     # Class DBs
@@ -154,7 +167,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if os.path.exists(path):
             db_type = whichdb.whichdb(path)
             if not db_type:
-                raise hyperdb.DatabaseError, "Couldn't identify database type"
+                raise DatabaseError, "Couldn't identify database type"
         elif os.path.exists(path+'.db'):
             # if the path ends in '.db', it's a dbm database, whether
             # anydbm says it's dbhash or not!
@@ -175,14 +188,14 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # new database? let anydbm pick the best dbm
         if not db_type:
             if __debug__:
-                print >>hyperdb.DEBUG, "opendb anydbm.open(%r, 'n')"%path
-            return anydbm.open(path, 'n')
+                print >>hyperdb.DEBUG, "opendb anydbm.open(%r, 'c')"%path
+            return anydbm.open(path, 'c')
 
         # open the database with the correct module
         try:
             dbm = __import__(db_type)
         except ImportError:
-            raise hyperdb.DatabaseError, \
+            raise DatabaseError, \
                 "Couldn't open database - the required module '%s'"\
                 " is not available"%db_type
         if __debug__:
@@ -233,6 +246,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)
@@ -244,6 +266,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)
@@ -447,7 +474,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     #
     # Journal
     #
-    def addjournal(self, classname, nodeid, action, params):
+    def addjournal(self, classname, nodeid, action, params, creator=None,
+            creation=None):
         ''' Journal the Action
         'action' may be:
 
@@ -457,9 +485,9 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''
         if __debug__:
             print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
-                action, params)
+                action, params, creator, creation)
         self.transactions.append((self.doSaveJournal, (classname, nodeid,
-            action, params)))
+            action, params, creator, creation)))
 
     def getjournal(self, classname, nodeid):
         ''' get the journal for id
@@ -496,6 +524,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
@@ -512,21 +541,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()
@@ -565,6 +583,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 = {}
@@ -603,28 +624,22 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             self.databases[db_name] = self.opendb(db_name, 'c')
         return self.databases[db_name]
 
-    def doSaveJournal(self, classname, nodeid, action, params):
-        # handle supply of the special journalling parameters (usually
-        # supplied on importing an existing database)
+    def doSaveJournal(self, classname, nodeid, action, params, creator,
+            creation):
+        # serialise the parameters now if necessary
         if isinstance(params, type({})):
-            if params.has_key('creator'):
-                journaltag = self.user.get(params['creator'], 'username')
-                del params['creator']
-            else:
-                journaltag = self.journaltag
-            if params.has_key('created'):
-                journaldate = params['created'].serialise()
-                del params['created']
-            else:
-                journaldate = date.Date().serialise()
-            if params.has_key('activity'):
-                del params['activity']
-
-            # serialise the parameters now
             if action in ('set', 'create'):
                 params = self.serialise(classname, params)
+
+        # handle supply of the special journalling parameters (usually
+        # supplied on importing an existing database)
+        if creator:
+            journaltag = creator
+        else:
+            journaltag = self.curuserid
+        if creation:
+            journaldate = creation.serialise()
         else:
-            journaltag = self.journaltag
             journaldate = date.Date().serialise()
 
         # create the journal entry
@@ -678,6 +693,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         self.destroyednodes = {}
         self.transactions = []
 
+    def close(self):
+        ''' Nothing to do
+        '''
+        pass
+
 _marker = []
 class Class(hyperdb.Class):
     '''The handle to a particular class of nodes in a hyperdatabase.'''
@@ -803,8 +823,8 @@ class Class(hyperdb.Class):
                 l = []
                 for entry in value:
                     if type(entry) != type(''):
-                        raise ValueError, '"%s" link value (%s) must be '\
-                            'String'%(key, value)
+                        raise ValueError, '"%s" multilink value (%r) '\
+                            'must contain Strings'%(key, value)
                     # if it isn't a number, it's a key
                     if not num_re.match(entry):
                         try:
@@ -884,7 +904,9 @@ class Class(hyperdb.Class):
             proptype = properties[prop]
             value = self.get(nodeid, prop)
             # "marshal" data where needed
-            if isinstance(proptype, hyperdb.Date):
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
                 value = value.get_tuple()
             elif isinstance(proptype, hyperdb.Interval):
                 value = value.get_tuple()
@@ -919,6 +941,9 @@ class Class(hyperdb.Class):
             if propname == 'id':
                 newid = value
                 continue
+            elif value is None:
+                # don't set Nones
+                continue
             elif isinstance(prop, hyperdb.Date):
                 value = date.Date(value)
             elif isinstance(prop, hyperdb.Interval):
@@ -927,12 +952,26 @@ class Class(hyperdb.Class):
                 pwd = password.Password()
                 pwd.unpack(value)
                 value = pwd
-            if value is not None:
-                d[propname] = value
+            d[propname] = value
 
-        # add
+        # add the node and journal
         self.db.addnode(self.classname, newid, d)
-        self.db.addjournal(self.classname, newid, 'create', d)
+
+        # extract the journalling stuff and nuke it
+        if d.has_key('creator'):
+            creator = d['creator']
+            del d['creator']
+        else:
+            creator = None
+        if d.has_key('creation'):
+            creation = d['creation']
+            del d['creation']
+        else:
+            creation = None
+        if d.has_key('activity'):
+            del d['activity']
+        self.db.addjournal(self.classname, newid, 'create', d, creator,
+            creation)
         return newid
 
     def get(self, nodeid, propname, default=_marker, cache=1):
@@ -953,7 +992,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)
@@ -963,6 +1008,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)
@@ -972,21 +1019,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:
-                name = 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 None
-            return self.db.user.lookup(name)
+                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):
@@ -1002,7 +1057,7 @@ class Class(hyperdb.Class):
 
         return d[propname]
 
-    # XXX not in spec
+    # not in spec
     def getnode(self, nodeid, cache=1):
         ''' Return a convenience wrapper for the node.
 
@@ -1082,7 +1137,11 @@ 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
@@ -1355,7 +1414,7 @@ class Class(hyperdb.Class):
         otherwise a KeyError is raised.
         '''
         if not self.key:
-            raise TypeError, 'No key property set'
+            raise TypeError, 'No key property set for class %s'%self.classname
         cldb = self.db.getclassdb(self.classname)
         try:
             for nodeid in self.db.getnodeids(self.classname, cldb):
@@ -1367,20 +1426,24 @@ class Class(hyperdb.Class):
                     return nodeid
         finally:
             cldb.close()
-        raise KeyError, keyvalue
+        raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
+            keyvalue, self.classname)
 
-    # XXX: change from spec - allows multiple props to match
+    # 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()
@@ -1478,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
 
@@ -1602,7 +1669,7 @@ class Class(hyperdb.Class):
             b_id, bn = b
             # sort by group and then sort
             for dir, prop in group, sort:
-                if dir is None: continue
+                if dir is None or prop is None: continue
 
                 # sorting is class-specific
                 propclass = properties[prop]
@@ -1730,7 +1797,7 @@ class Class(hyperdb.Class):
             d['id'] = String()
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
-            d['creator'] = hyperdb.Link("user")
+            d['creator'] = hyperdb.Link('user')
         return d
 
     def addprop(self, **properties):
@@ -1819,7 +1886,7 @@ class FileClass(Class):
 
         # extract the "content" property from the proplist
         i = propnames.index('content')
-        content = proplist[i]
+        content = eval(proplist[i])
         del propnames[i]
         del proplist[i]
 
@@ -1854,8 +1921,7 @@ class FileClass(Class):
             modified.
         '''
         d = Class.getprops(self, protected=protected).copy()
-        if protected:
-            d['content'] = hyperdb.String()
+        d['content'] = hyperdb.String()
         return d
 
     def index(self, nodeid):
@@ -1880,7 +1946,7 @@ class FileClass(Class):
         self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
             mime_type)
 
-# XXX deviation from spec - was called ItemClass
+# deviation from spec - was called ItemClass
 class IssueClass(Class, roundupdb.IssueClass):
     # Overridden methods:
     def __init__(self, db, classname, **properties):
@@ -1904,350 +1970,3 @@ class IssueClass(Class, roundupdb.IssueClass):
         Class.__init__(self, db, classname, **properties)
 
 #
-#$Log: not supported by cvs2svn $
-#Revision 1.66  2002/09/01 04:32:30  richard
-#. Lots of cleanup in the classic html (stylesheet, search page, index page, ...)
-#. Reinstated searching, but not query saving yet
-#. Filtering only allows sorting and grouping by one property - all backends
-#  now implement this behaviour.
-#. Nosy list journalling turned off by default, everything else is on.
-#. Added some convenience methods (reverse, propchanged, [item] accesses, ...)
-#. Did I mention the stylesheet is much cleaner now? :)
-#
-#Revision 1.65  2002/08/30 08:35:45  richard
-#minor edits
-#
-#Revision 1.64  2002/08/22 07:57:11  richard
-#Consistent quoting
-#
-#Revision 1.63  2002/08/22 04:42:28  richard
-#use more robust date stamp comparisons in pack(), make journal smaller too
-#
-#Revision 1.62  2002/08/21 07:07:27  richard
-#In preparing to turn back on link/unlink journal events (by default these
-#are turned off) I've:
-#- fixed back_anydbm so it can journal those events again (had broken it
-#  with recent changes)
-#- changed the serialisation format for dates and intervals to use a
-#  numbers-only (and sign for Intervals) string instead of tuple-of-ints.
-#  Much smaller.
-#
-#Revision 1.61  2002/08/19 02:53:27  richard
-#full database export and import is done
-#
-#Revision 1.60  2002/08/19 00:23:19  richard
-#handle "unset" initial Link values (!)
-#
-#Revision 1.59  2002/08/16 04:28:13  richard
-#added is_retired query to Class
-#
-#Revision 1.58  2002/08/01 15:06:24  gmcm
-#Use same regex to split search terms as used to index text.
-#Fix to back_metakit for not changing journaltag on reopen.
-#Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
-#Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
-#
-#Revision 1.57  2002/07/31 23:57:36  richard
-# . web forms may now unset Link values (like assignedto)
-#
-#Revision 1.56  2002/07/31 22:04:33  richard
-#cleanup
-#
-#Revision 1.55  2002/07/30 08:22:38  richard
-#Session storage in the hyperdb was horribly, horribly inefficient. We use
-#a simple anydbm wrapper now - which could be overridden by the metakit
-#backend or RDB backend if necessary.
-#Much, much better.
-#
-#Revision 1.54  2002/07/26 08:26:59  richard
-#Very close now. The cgi and mailgw now use the new security API. The two
-#templates have been migrated to that setup. Lots of unit tests. Still some
-#issue in the web form for editing Roles assigned to users.
-#
-#Revision 1.53  2002/07/25 07:14:06  richard
-#Bugger it. Here's the current shape of the new security implementation.
-#Still to do:
-# . call the security funcs from cgi and mailgw
-# . change shipped templates to include correct initialisation and remove
-#   the old config vars
-#... that seems like a lot. The bulk of the work has been done though. Honest :)
-#
-#Revision 1.52  2002/07/19 03:36:34  richard
-#Implemented the destroy() method needed by the session database (and possibly
-#others). At the same time, I removed the leading underscores from the hyperdb
-#methods that Really Didn't Need Them.
-#The journal also raises IndexError now for all situations where there is a
-#request for the journal of a node that doesn't have one. It used to return
-#[] in _some_ situations, but not all. This _may_ break code, but the tests
-#pass...
-#
-#Revision 1.51  2002/07/18 23:07:08  richard
-#Unit tests and a few fixes.
-#
-#Revision 1.50  2002/07/18 11:50:58  richard
-#added tests for number type too
-#
-#Revision 1.49  2002/07/18 11:41:10  richard
-#added tests for boolean type, and fixes to anydbm backend
-#
-#Revision 1.48  2002/07/18 11:17:31  gmcm
-#Add Number and Boolean types to hyperdb.
-#Add conversion cases to web, mail & admin interfaces.
-#Add storage/serialization cases to back_anydbm & back_metakit.
-#
-#Revision 1.47  2002/07/14 23:18:20  richard
-#. fixed the journal bloat from multilink changes - we just log the add or
-#  remove operations, not the whole list
-#
-#Revision 1.46  2002/07/14 06:06:34  richard
-#Did some old TODOs
-#
-#Revision 1.45  2002/07/14 04:03:14  richard
-#Implemented a switch to disable journalling for a Class. CGI session
-#database now uses it.
-#
-#Revision 1.44  2002/07/14 02:05:53  richard
-#. all storage-specific code (ie. backend) is now implemented by the backends
-#
-#Revision 1.43  2002/07/10 06:30:30  richard
-#...except of course it's nice to use valid Python syntax
-#
-#Revision 1.42  2002/07/10 06:21:38  richard
-#Be extra safe
-#
-#Revision 1.41  2002/07/10 00:21:45  richard
-#explicit database closing
-#
-#Revision 1.40  2002/07/09 04:19:09  richard
-#Added reindex command to roundup-admin.
-#Fixed reindex on first access.
-#Also fixed reindexing of entries that change.
-#
-#Revision 1.39  2002/07/09 03:02:52  richard
-#More indexer work:
-#- all String properties may now be indexed too. Currently there's a bit of
-#  "issue" specific code in the actual searching which needs to be
-#  addressed. In a nutshell:
-#  + pass 'indexme="yes"' as a String() property initialisation arg, eg:
-#        file = FileClass(db, "file", name=String(), type=String(),
-#            comment=String(indexme="yes"))
-#  + the comment will then be indexed and be searchable, with the results
-#    related back to the issue that the file is linked to
-#- as a result of this work, the FileClass has a default MIME type that may
-#  be overridden in a subclass, or by the use of a "type" property as is
-#  done in the default templates.
-#- the regeneration of the indexes (if necessary) is done once the schema is
-#  set up in the dbinit.
-#
-#Revision 1.38  2002/07/08 06:58:15  richard
-#cleaned up the indexer code:
-# - it splits more words out (much simpler, faster splitter)
-# - removed code we'll never use (roundup.roundup_indexer has the full
-#   implementation, and replaces roundup.indexer)
-# - only index text/plain and rfc822/message (ideas for other text formats to
-#   index are welcome)
-# - added simple unit test for indexer. Needs more tests for regression.
-#
-#Revision 1.37  2002/06/20 23:52:35  richard
-#More informative error message
-#
-#Revision 1.36  2002/06/19 03:07:19  richard
-#Moved the file storage commit into blobfiles where it belongs.
-#
-#Revision 1.35  2002/05/25 07:16:24  rochecompaan
-#Merged search_indexing-branch with HEAD
-#
-#Revision 1.34  2002/05/15 06:21:21  richard
-# . node caching now works, and gives a small boost in performance
-#
-#As a part of this, I cleaned up the DEBUG output and implemented TRACE
-#output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
-#CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
-#(using if __debug__ which is compiled out with -O)
-#
-#Revision 1.33  2002/04/24 10:38:26  rochecompaan
-#All database files are now created group readable and writable.
-#
-#Revision 1.32  2002/04/15 23:25:15  richard
-#. node ids are now generated from a lockable store - no more race conditions
-#
-#We're using the portalocker code by Jonathan Feinberg that was contributed
-#to the ASPN Python cookbook. This gives us locking across Unix and Windows.
-#
-#Revision 1.31  2002/04/03 05:54:31  richard
-#Fixed serialisation problem by moving the serialisation step out of the
-#hyperdb.Class (get, set) into the hyperdb.Database.
-#
-#Also fixed htmltemplate after the showid changes I made yesterday.
-#
-#Unit tests for all of the above written.
-#
-#Revision 1.30.2.1  2002/04/03 11:55:57  rochecompaan
-# . Added feature #526730 - search for messages capability
-#
-#Revision 1.30  2002/02/27 03:40:59  richard
-#Ran it through pychecker, made fixes
-#
-#Revision 1.29  2002/02/25 14:34:31  grubert
-# . use blobfiles in back_anydbm which is used in back_bsddb.
-#   change test_db as dirlist does not work for subdirectories.
-#   ATTENTION: blobfiles now creates subdirectories for files.
-#
-#Revision 1.28  2002/02/16 09:14:17  richard
-# . #514854 ] History: "User" is always ticket creator
-#
-#Revision 1.27  2002/01/22 07:21:13  richard
-#. fixed back_bsddb so it passed the journal tests
-#
-#... it didn't seem happy using the back_anydbm _open method, which is odd.
-#Yet another occurrance of whichdb not being able to recognise older bsddb
-#databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
-#process.
-#
-#Revision 1.26  2002/01/22 05:18:38  rochecompaan
-#last_set_entry was referenced before assignment
-#
-#Revision 1.25  2002/01/22 05:06:08  rochecompaan
-#We need to keep the last 'set' entry in the journal to preserve
-#information on 'activity' for nodes.
-#
-#Revision 1.24  2002/01/21 16:33:20  rochecompaan
-#You can now use the roundup-admin tool to pack the database
-#
-#Revision 1.23  2002/01/18 04:32:04  richard
-#Rollback was breaking because a message hadn't actually been written to the file. Needs
-#more investigation.
-#
-#Revision 1.22  2002/01/14 02:20:15  richard
-# . changed all config accesses so they access either the instance or the
-#   config attriubute on the db. This means that all config is obtained from
-#   instance_config instead of the mish-mash of classes. This will make
-#   switching to a ConfigParser setup easier too, I hope.
-#
-#At a minimum, this makes migration a _little_ easier (a lot easier in the
-#0.5.0 switch, I hope!)
-#
-#Revision 1.21  2002/01/02 02:31:38  richard
-#Sorry for the huge checkin message - I was only intending to implement #496356
-#but I found a number of places where things had been broken by transactions:
-# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
-#   for _all_ roundup-generated smtp messages to be sent to.
-# . the transaction cache had broken the roundupdb.Class set() reactors
-# . newly-created author users in the mailgw weren't being committed to the db
-#
-#Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
-#on when I found that stuff :):
-# . #496356 ] Use threading in messages
-# . detectors were being registered multiple times
-# . added tests for mailgw
-# . much better attaching of erroneous messages in the mail gateway
-#
-#Revision 1.20  2001/12/18 15:30:34  rochecompaan
-#Fixed bugs:
-# .  Fixed file creation and retrieval in same transaction in anydbm
-#    backend
-# .  Cgi interface now renders new issue after issue creation
-# .  Could not set issue status to resolved through cgi interface
-# .  Mail gateway was changing status back to 'chatting' if status was
-#    omitted as an argument
-#
-#Revision 1.19  2001/12/17 03:52:48  richard
-#Implemented file store rollback. As a bonus, the hyperdb is now capable of
-#storing more than one file per node - if a property name is supplied,
-#the file is called designator.property.
-#I decided not to migrate the existing files stored over to the new naming
-#scheme - the FileClass just doesn't specify the property name.
-#
-#Revision 1.18  2001/12/16 10:53:38  richard
-#take a copy of the node dict so that the subsequent set
-#operation doesn't modify the oldvalues structure
-#
-#Revision 1.17  2001/12/14 23:42:57  richard
-#yuck, a gdbm instance tests false :(
-#I've left the debugging code in - it should be removed one day if we're ever
-#_really_ anal about performace :)
-#
-#Revision 1.16  2001/12/12 03:23:14  richard
-#Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
-#incorrectly identifies a dbm file as a dbhash file on my system. This has
-#been submitted to the python bug tracker as issue #491888:
-#https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
-#
-#Revision 1.15  2001/12/12 02:30:51  richard
-#I fixed the problems with people whose anydbm was using the dbm module at the
-#backend. It turns out the dbm module modifies the file name to append ".db"
-#and my check to determine if we're opening an existing or new db just
-#tested os.path.exists() on the filename. Well, no longer! We now perform a
-#much better check _and_ cope with the anydbm implementation module changing
-#too!
-#I also fixed the backends __init__ so only ImportError is squashed.
-#
-#Revision 1.14  2001/12/10 22:20:01  richard
-#Enabled transaction support in the bsddb backend. It uses the anydbm code
-#where possible, only replacing methods where the db is opened (it uses the
-#btree opener specifically.)
-#Also cleaned up some change note generation.
-#Made the backends package work with pydoc too.
-#
-#Revision 1.13  2001/12/02 05:06:16  richard
-#. We now use weakrefs in the Classes to keep the database reference, so
-#  the close() method on the database is no longer needed.
-#  I bumped the minimum python requirement up to 2.1 accordingly.
-#. #487480 ] roundup-server
-#. #487476 ] INSTALL.txt
-#
-#I also cleaned up the change message / post-edit stuff in the cgi client.
-#There's now a clearly marked "TODO: append the change note" where I believe
-#the change note should be added there. The "changes" list will obviously
-#have to be modified to be a dict of the changes, or somesuch.
-#
-#More testing needed.
-#
-#Revision 1.12  2001/12/01 07:17:50  richard
-#. We now have basic transaction support! Information is only written to
-#  the database when the commit() method is called. Only the anydbm
-#  backend is modified in this way - neither of the bsddb backends have been.
-#  The mail, admin and cgi interfaces all use commit (except the admin tool
-#  doesn't have a commit command, so interactive users can't commit...)
-#. Fixed login/registration forwarding the user to the right page (or not,
-#  on a failure)
-#
-#Revision 1.11  2001/11/21 02:34:18  richard
-#Added a target version field to the extended issue schema
-#
-#Revision 1.10  2001/10/09 23:58:10  richard
-#Moved the data stringification up into the hyperdb.Class class' get, set
-#and create methods. This means that the data is also stringified for the
-#journal call, and removes duplication of code from the backends. The
-#backend code now only sees strings.
-#
-#Revision 1.9  2001/10/09 07:25:59  richard
-#Added the Password property type. See "pydoc roundup.password" for
-#implementation details. Have updated some of the documentation too.
-#
-#Revision 1.8  2001/09/29 13:27:00  richard
-#CGI interfaces now spit up a top-level index of all the instances they can
-#serve.
-#
-#Revision 1.7  2001/08/12 06:32:36  richard
-#using isinstance(blah, Foo) now instead of isFooType
-#
-#Revision 1.6  2001/08/07 00:24:42  richard
-#stupid typo
-#
-#Revision 1.5  2001/08/07 00:15:51  richard
-#Added the copyright/license notice to (nearly) all files at request of
-#Bizar Software.
-#
-#Revision 1.4  2001/07/30 01:41:36  richard
-#Makes schema changes mucho easier.
-#
-#Revision 1.3  2001/07/25 01:23:07  richard
-#Added the Roundup spec to the new documentation directory.
-#
-#Revision 1.2  2001/07/23 08:20:44  richard
-#Moved over to using marshal in the bsddb and anydbm backends.
-#roundup-admin now has a "freshen" command that'll load/save all nodes (not
-# retired - mod hyperdb.Class.list() so it lists retired nodes)
-#
-#