Code

svn repository setup
[roundup.git] / roundup / backends / back_anydbm.py
index b79d1454d1443ce82924a46280f6b13d9b849fc7..17dfa78dfbc974766605f483a2bcc7e8ed93c770 100644 (file)
@@ -14,8 +14,8 @@
 # FOR A PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
-# 
-#$Id: back_anydbm.py,v 1.141 2004-04-07 01:12:25 richard Exp $
+#
+#$Id: back_anydbm.py,v 1.211 2008-08-07 05:53:14 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
 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
@@ -33,15 +33,30 @@ try:
 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
+import whichdb, os, marshal, re, weakref, string, copy, time, shutil, logging
+
+from roundup import hyperdb, date, password, roundupdb, security, support
+from roundup.support import reversed
+from roundup.backends import locking
+from roundup.i18n import _
+
 from blobfiles import FileStorage
 from sessions_dbm import Sessions, OneTimeKeys
-from indexer_dbm import Indexer
-from roundup.backends import locking
-from roundup.hyperdb import String, Password, Date, Interval, Link, \
-    Multilink, DatabaseError, Boolean, Number, Node
-from roundup.date import Range
+
+try:
+    from indexer_xapian import Indexer
+except ImportError:
+    from indexer_dbm import Indexer
+
+def db_exists(config):
+    # check for the user db
+    for db in 'nodes.user nodes.user.db'.split():
+        if os.path.exists(os.path.join(config.DATABASE, db)):
+            return 1
+    return 0
+
+def db_nuke(config):
+    shutil.rmtree(config.DATABASE)
 
 #
 # Now the database
@@ -50,7 +65,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     '''A database for storing records containing flexible data types.
 
     Transaction stuff TODO:
-    
+
     - check the timestamp of the class file and nuke the cache if it's
       modified. Do some sort of conflict checking on the dirty stuff.
     - perhaps detect write collisions (related to above)?
@@ -68,20 +83,22 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         entries for any edits done on the database.  If 'journaltag' is
         None, the database is opened in read-only mode: the Class.create(),
         Class.set(), Class.retire(), and Class.restore() methods are
-        disabled.  
-        '''        
+        disabled.
+        '''
+        FileStorage.__init__(self, config.UMASK)
         self.config, self.journaltag = config, journaltag
         self.dir = config.DATABASE
         self.classes = {}
         self.cache = {}         # cache of nodes loaded or created
+        self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0,
+            'filtering': 0}
         self.dirtynodes = {}    # keep track of the dirty nodes by class
         self.newnodes = {}      # keep track of the new nodes by class
         self.destroyednodes = {}# keep track of the destroyed nodes by class
         self.transactions = []
-        self.indexer = Indexer(self.dir)
+        self.indexer = Indexer(self)
         self.security = security.Security(self)
-        # ensure files are group readable and writable
-        os.umask(0002)
+        os.umask(config.UMASK)
 
         # lock it
         lockfilenm = os.path.join(self.dir, 'lock')
@@ -107,14 +124,23 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def getOTKManager(self):
         return OneTimeKeys(self)
 
-    def reindex(self):
-        for klass in self.classes.values():
-            for nodeid in klass.list():
-                klass.index(nodeid)
+    def reindex(self, classname=None, show_progress=False):
+        if classname:
+            classes = [self.getclass(classname)]
+        else:
+            classes = self.classes.values()
+        for klass in classes:
+            if show_progress:
+                for nodeid in support.Progress('Reindex %s'%klass.classname,
+                        klass.list()):
+                    klass.index(nodeid)
+            else:
+                for nodeid in klass.list():
+                    klass.index(nodeid)
         self.indexer.save_index()
 
     def __repr__(self):
-        return '<back_anydbm instance at %x>'%id(self) 
+        return '<back_anydbm instance at %x>'%id(self)
 
     #
     # Classes
@@ -122,20 +148,18 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def __getattr__(self, classname):
         '''A convenient way of calling self.getclass(classname).'''
         if self.classes.has_key(classname):
-            if __debug__:
-                print >>hyperdb.DEBUG, '__getattr__', (self, classname)
             return self.classes[classname]
         raise AttributeError, classname
 
     def addclass(self, cl):
-        if __debug__:
-            print >>hyperdb.DEBUG, 'addclass', (self, cl)
         cn = cl.classname
         if self.classes.has_key(cn):
             raise ValueError, cn
         self.classes[cn] = cl
 
         # add default Edit and View permissions
+        self.security.addPermission(name="Create", klass=cn,
+            description="User is allowed to create "+cn)
         self.security.addPermission(name="Edit", klass=cn,
             description="User is allowed to edit "+cn)
         self.security.addPermission(name="View", klass=cn,
@@ -143,8 +167,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
     def getclasses(self):
         '''Return a list of the names of all existing classes.'''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getclasses', (self,)
         l = self.classes.keys()
         l.sort()
         return l
@@ -154,8 +176,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
         If 'classname' is not a valid class name, a KeyError is raised.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getclass', (self, classname)
         try:
             return self.classes[classname]
         except KeyError:
@@ -167,8 +187,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def clear(self):
         '''Delete all database contents
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'clear', (self,)
+        logging.getLogger('hyperdb').info('clear')
         for cn in self.classes.keys():
             for dummy in 'nodes', 'journals':
                 path = os.path.join(self.dir, 'journals.%s'%cn)
@@ -176,13 +195,17 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                     os.remove(path)
                 elif os.path.exists(path+'.db'):    # dbm appends .db
                     os.remove(path+'.db')
+        # reset id sequences
+        path = os.path.join(os.getcwd(), self.dir, '_ids')
+        if os.path.exists(path):
+            os.remove(path)
+        elif os.path.exists(path+'.db'):    # dbm appends .db
+            os.remove(path+'.db')
 
     def getclassdb(self, classname, mode='r'):
         ''' grab a connection to the class db that will be used for
             multiple actions
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
         return self.opendb('nodes.%s'%classname, mode)
 
     def determine_db_type(self, path):
@@ -192,7 +215,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if os.path.exists(path):
             db_type = whichdb.whichdb(path)
             if not db_type:
-                raise DatabaseError, "Couldn't identify database type"
+                raise hyperdb.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!
@@ -203,9 +227,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         '''Low-level database opener that gets around anydbm/dbm
            eccentricities.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'opendb', (self, name, mode)
-
         # figure the class db type
         path = os.path.join(os.getcwd(), self.dir, name)
         db_type = self.determine_db_type(path)
@@ -213,19 +234,19 @@ 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, 'c')"%path
+                logging.getLogger('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 DatabaseError, \
-                "Couldn't open database - the required module '%s'"\
-                " is not available"%db_type
+            raise hyperdb.DatabaseError, \
+                _("Couldn't open database - the required module '%s'"\
+                " is not available")%db_type
         if __debug__:
-            print >>hyperdb.DEBUG, "opendb %r.open(%r, %r)"%(db_type, path,
-                mode)
+            logging.getLogger('hyperdb').debug("opendb %r.open(%r, %r)"%(db_type, path,
+                mode))
         return dbm.open(path, mode)
 
     #
@@ -259,9 +280,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def addnode(self, classname, nodeid, node):
         ''' add the specified node to its class's db
         '''
-        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
@@ -278,16 +296,8 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def setnode(self, classname, nodeid, node):
         ''' change the specified node
         '''
-        if __debug__:
-            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()
-        node['actor'] = self.getuid()
-
         # can't set without having already loaded the node
         self.cache[classname][nodeid] = node
         self.savenode(classname, nodeid, node)
@@ -296,7 +306,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         ''' perform the saving of data specified by the set/addnode
         '''
         if __debug__:
-            print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
+            logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node))
         self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
 
     def getnode(self, classname, nodeid, db=None, cache=1):
@@ -305,19 +315,18 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             Note the "cache" parameter is not used, and exists purely for
             backward compatibility!
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
-
         # 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)
+                logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid))
+                self.stats['cache_hits'] += 1
             return cache_dict[nodeid]
 
         if __debug__:
-            print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
+            self.stats['cache_misses'] += 1
+            start_t = time.time()
+            logging.getLogger('hyperdb').debug('get %s%s'%(classname, nodeid))
 
         # get from the database and save in the cache
         if db is None:
@@ -340,14 +349,16 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if cache:
             cache_dict[nodeid] = res
 
+        if __debug__:
+            self.stats['get_items'] += (time.time() - start_t)
+
         return res
 
     def destroynode(self, classname, nodeid):
         '''Remove a node from the database. Called exclusively by the
            destroy() method on Class.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
+        logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid))
 
         # remove from cache and newnodes if it's there
         if (self.cache.has_key(classname) and
@@ -367,30 +378,31 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
         # add the destroy commit action
         self.transactions.append((self.doDestroyNode, (classname, nodeid)))
+        self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
 
     def serialise(self, classname, node):
         '''Copy the node contents, converting non-marshallable data into
            marshallable data.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'serialise', classname, node
         properties = self.getclass(classname).getprops()
         d = {}
         for k, v in node.items():
-            # if the property doesn't exist, or is the "retired" flag then
-            # it won't be in the properties dict
-            if not properties.has_key(k):
+            if k == self.RETIRED_FLAG:
                 d[k] = v
                 continue
 
+            # if the property doesn't exist then we really don't care
+            if not properties.has_key(k):
+                continue
+
             # get the property spec
             prop = properties[k]
 
-            if isinstance(prop, Password) and v is not None:
+            if isinstance(prop, hyperdb.Password) and v is not None:
                 d[k] = str(v)
-            elif isinstance(prop, Date) and v is not None:
+            elif isinstance(prop, hyperdb.Date) and v is not None:
                 d[k] = v.serialise()
-            elif isinstance(prop, Interval) and v is not None:
+            elif isinstance(prop, hyperdb.Interval) and v is not None:
                 d[k] = v.serialise()
             else:
                 d[k] = v
@@ -399,8 +411,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def unserialise(self, classname, node):
         '''Decode the marshalled node data
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'unserialise', classname, node
         properties = self.getclass(classname).getprops()
         d = {}
         for k, v in node.items():
@@ -413,11 +423,11 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             # get the property spec
             prop = properties[k]
 
-            if isinstance(prop, Date) and v is not None:
+            if isinstance(prop, hyperdb.Date) and v is not None:
                 d[k] = date.Date(v)
-            elif isinstance(prop, Interval) and v is not None:
+            elif isinstance(prop, hyperdb.Interval) and v is not None:
                 d[k] = date.Interval(v)
-            elif isinstance(prop, Password) and v is not None:
+            elif isinstance(prop, hyperdb.Password) and v is not None:
                 p = password.Password()
                 p.unpack(v)
                 d[k] = p
@@ -428,17 +438,10 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def hasnode(self, classname, nodeid, db=None):
         ''' determine if the database has a given node
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
-
         # try the cache
         cache = self.cache.setdefault(classname, {})
         if cache.has_key(nodeid):
-            if __debug__:
-                print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
             return 1
-        if __debug__:
-            print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
 
         # not in the cache - check the database
         if db is None:
@@ -447,9 +450,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         return res
 
     def countnodes(self, classname, db=None):
-        if __debug__:
-            print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
-
         count = 0
 
         # include the uncommitted nodes
@@ -480,18 +480,23 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             'create' or 'set' -- 'params' is a dictionary of property values
             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
             'retire' -- 'params' is None
+
+            'creator' -- the user performing the action, which defaults to
+            the current user.
         '''
         if __debug__:
-            print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
-                action, params, creator, creation)
+            logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname,
+                nodeid, action, params, creator, creation))
+        if creator is None:
+            creator = self.getuid()
         self.transactions.append((self.doSaveJournal, (classname, nodeid,
             action, params, creator, creation)))
 
     def setjournal(self, classname, nodeid, journal):
         '''Set the journal to the "journal" list.'''
         if __debug__:
-            print >>hyperdb.DEBUG, 'setjournal', (self, classname, nodeid,
-                journal)
+            logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname,
+                nodeid, journal))
         self.transactions.append((self.doSetJournal, (classname, nodeid,
             journal)))
 
@@ -501,9 +506,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             Raise IndexError if the node doesn't exist (as per history()'s
             API)
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
-
         # our journal result
         res = []
 
@@ -554,11 +556,9 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     def pack(self, pack_before):
         ''' Delete all journal entries except "create" before 'pack_before'.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
-
         pack_before = pack_before.serialise()
         for classname in self.getclasses():
+            packed = 0
             # get the journal db
             db_name = 'journals.%s'%classname
             path = os.path.join(os.getcwd(), self.dir, classname)
@@ -572,26 +572,41 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 last_set_entry = None
                 for entry in journal:
                     # unpack the entry
-                    (nodeid, date_stamp, self.journaltag, action, 
+                    (nodeid, date_stamp, self.journaltag, action,
                         params) = entry
                     # 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)
+                    else:
+                        packed += 1
                 db[key] = marshal.dumps(l)
+
+                logging.getLogger('hyperdb').info('packed %d %s items'%(packed,
+                    classname))
+
             if db_type == 'gdbm':
                 db.reorganize()
             db.close()
-            
+
 
     #
     # Basic transaction support
     #
-    def commit(self):
+    def commit(self, fail_ok=False):
         ''' Commit the current transactions.
+
+        Save all data changed since the database was opened or since the
+        last commit() or rollback().
+
+        fail_ok indicates that the commit is allowed to fail. This is used
+        in the web interface when committing cleaning of the session
+        database. We don't care if there's a concurrency issue there.
+
+        The only backend this seems to affect is postgres.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'commit', (self,)
+        logging.getLogger('hyperdb').info('commit %s transactions'%(
+            len(self.transactions)))
 
         # keep a handle to all the database files opened
         self.databases = {}
@@ -607,9 +622,13 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
                 db.close()
             del self.databases
 
+        # clear the transactions list now so the blobfile implementation
+        # doesn't think there's still pending file commits when it tries
+        # to access the file data
+        self.transactions = []
+
         # reindex the nodes that request it
         for classname, nodeid in filter(None, reindex.keys()):
-            print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
             self.getclass(classname).index(nodeid)
 
         # save the indexer state
@@ -635,10 +654,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         return self.databases[db_name]
 
     def doSaveNode(self, classname, nodeid, node):
-        if __debug__:
-            print >>hyperdb.DEBUG, 'doSaveNode', (self, classname, nodeid,
-                node)
-
         db = self.getCachedClassDB(classname)
 
         # now save the marshalled data
@@ -665,10 +680,7 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
 
         # handle supply of the special journalling parameters (usually
         # supplied on importing an existing database)
-        if creator:
-            journaltag = creator
-        else:
-            journaltag = self.getuid()
+        journaltag = creator
         if creation:
             journaldate = creation.serialise()
         else:
@@ -677,9 +689,6 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         # create the journal entry
         entry = (nodeid, journaldate, journaltag, action, params)
 
-        if __debug__:
-            print >>hyperdb.DEBUG, 'doSaveJournal', entry
-
         db = self.getCachedJournalDB(classname)
 
         # now insert the journal entry
@@ -700,14 +709,12 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
             if isinstance(params, type({})):
                 if action in ('set', 'create'):
                     params = self.serialise(classname, params)
+            journaldate = journaldate.serialise()
             l.append((nodeid, journaldate, journaltag, action, params))
         db = self.getCachedJournalDB(classname)
         db[nodeid] = marshal.dumps(l)
 
     def doDestroyNode(self, classname, nodeid):
-        if __debug__:
-            print >>hyperdb.DEBUG, 'doDestroyNode', (self, classname, nodeid)
-
         # delete from the class database
         db = self.getCachedClassDB(classname)
         if db.has_key(nodeid):
@@ -718,14 +725,12 @@ class Database(FileStorage, hyperdb.Database, roundupdb.Database):
         if db.has_key(nodeid):
             del db[nodeid]
 
-        # return the classname, nodeid so we reindex this content
-        return (classname, nodeid)
-
     def rollback(self):
         ''' Reverse all actions from the current transaction.
         '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'rollback', (self, )
+        logging.getLogger('hyperdb').info('rollback %s transactions'%(
+            len(self.transactions)))
+
         for method, args in self.transactions:
             # delete temporary files
             if method == self.doStoreFile:
@@ -748,32 +753,6 @@ _marker = []
 class Class(hyperdb.Class):
     '''The handle to a particular class of nodes in a hyperdatabase.'''
 
-    def __init__(self, db, classname, **properties):
-        '''Create a new class with a given name and property specification.
-
-        'classname' must not collide with the name of an existing class,
-        or a ValueError is raised.  The keyword arguments in 'properties'
-        must map names to property objects, or a TypeError is raised.
-        '''
-        for name in 'creation activity creator actor'.split():
-            if properties.has_key(name):
-                raise ValueError, '"creation", "activity", "creator" and '\
-                    '"actor" are reserved'
-
-        self.classname = classname
-        self.properties = properties
-        self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
-        self.key = ''
-
-        # should we journal changes (default yes)
-        self.do_journal = 1
-
-        # do the db-related init stuff
-        db.addclass(self)
-
-        self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
-        self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
-
     def enableJournalling(self):
         '''Turn journalling on for this class
         '''
@@ -793,13 +772,13 @@ class Class(hyperdb.Class):
 
         The values of arguments must be acceptable for the types of their
         corresponding properties or a TypeError is raised.
-        
+
         If this class has a key property, it must be present and its value
         must not collide with other key strings or a ValueError is raised.
-        
+
         Any other properties on this class that are missing from the
         'propvalues' dictionary are set to None.
-        
+
         If an id in a link or multilink property does not refer to a valid
         node, an IndexError is raised.
 
@@ -818,7 +797,7 @@ class Class(hyperdb.Class):
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         if propvalues.has_key('creation') or propvalues.has_key('activity'):
             raise KeyError, '"creation" and "activity" are reserved'
@@ -843,7 +822,7 @@ class Class(hyperdb.Class):
                 raise KeyError, '"%s" has no property "%s"'%(self.classname,
                     key)
 
-            if value is not None and isinstance(prop, Link):
+            if value is not None and isinstance(prop, hyperdb.Link):
                 if type(value) != type(''):
                     raise ValueError, 'link value must be String'
                 link_class = self.properties[key].classname
@@ -865,9 +844,11 @@ class Class(hyperdb.Class):
                     self.db.addjournal(link_class, value, 'link',
                         (self.classname, newid, key))
 
-            elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
+            elif isinstance(prop, hyperdb.Multilink):
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of ids'%key
 
                 # clean up and validate the list of links
                 link_class = self.properties[key].classname
@@ -897,30 +878,32 @@ class Class(hyperdb.Class):
                         self.db.addjournal(link_class, nodeid, 'link',
                             (self.classname, newid, key))
 
-            elif isinstance(prop, String):
+            elif isinstance(prop, hyperdb.String):
                 if type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%key
-                self.db.indexer.add_text((self.classname, newid, key), value)
+                if prop.indexme:
+                    self.db.indexer.add_text((self.classname, newid, key),
+                        value)
 
-            elif isinstance(prop, Password):
+            elif isinstance(prop, hyperdb.Password):
                 if not isinstance(value, password.Password):
                     raise TypeError, 'new property "%s" not a Password'%key
 
-            elif isinstance(prop, Date):
+            elif isinstance(prop, hyperdb.Date):
                 if value is not None and not isinstance(value, date.Date):
                     raise TypeError, 'new property "%s" not a Date'%key
 
-            elif isinstance(prop, Interval):
+            elif isinstance(prop, hyperdb.Interval):
                 if value is not None and not isinstance(value, date.Interval):
                     raise TypeError, 'new property "%s" not an Interval'%key
 
-            elif value is not None and isinstance(prop, Number):
+            elif value is not None and isinstance(prop, hyperdb.Number):
                 try:
                     float(value)
                 except ValueError:
                     raise TypeError, 'new property "%s" not numeric'%key
 
-            elif value is not None and isinstance(prop, Boolean):
+            elif value is not None and isinstance(prop, hyperdb.Boolean):
                 try:
                     int(value)
                 except ValueError:
@@ -932,10 +915,8 @@ class Class(hyperdb.Class):
                 continue
             if key == self.key:
                 raise ValueError, 'key property "%s" is required'%key
-            if isinstance(prop, Multilink):
+            if isinstance(prop, hyperdb.Multilink):
                 propvalues[key] = []
-            else:
-                propvalues[key] = None
 
         # done
         self.db.addnode(self.classname, newid, propvalues)
@@ -1031,7 +1012,7 @@ class Class(hyperdb.Class):
 
         if not d.has_key(propname):
             if default is _marker:
-                if isinstance(prop, Multilink):
+                if isinstance(prop, hyperdb.Multilink):
                     return []
                 else:
                     return None
@@ -1039,14 +1020,14 @@ class Class(hyperdb.Class):
                 return default
 
         # return a dupe of the list so code doesn't get confused
-        if isinstance(prop, Multilink):
+        if isinstance(prop, hyperdb.Multilink):
             return d[propname][:]
 
         return d[propname]
 
     def set(self, nodeid, **propvalues):
         '''Modify a property on an existing node of this class.
-        
+
         'nodeid' must be the id of an existing node of this class or an
         IndexError is raised.
 
@@ -1067,9 +1048,16 @@ class Class(hyperdb.Class):
         '''
         self.fireAuditors('set', nodeid, propvalues)
         oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
+        for name,prop in self.getprops(protected=0).items():
+            if oldvalues.has_key(name):
+                continue
+            if isinstance(prop, hyperdb.Multilink):
+                oldvalues[name] = []
+            else:
+                oldvalues[name] = None
         propvalues = self.set_inner(nodeid, **propvalues)
         self.fireReactors('set', nodeid, oldvalues)
-        return propvalues        
+        return propvalues
 
     def set_inner(self, nodeid, **propvalues):
         ''' Called by set, in-between the audit and react calls.
@@ -1084,7 +1072,7 @@ class Class(hyperdb.Class):
             raise KeyError, '"id" is reserved'
 
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         if node.has_key(self.db.RETIRED_FLAG):
@@ -1122,7 +1110,7 @@ class Class(hyperdb.Class):
             journalvalues[propname] = current
 
             # do stuff based on the prop type
-            if isinstance(prop, Link):
+            if isinstance(prop, hyperdb.Link):
                 link_class = prop.classname
                 # if it isn't a number, it's a key
                 if value is not None and not isinstance(value, type('')):
@@ -1150,9 +1138,11 @@ class Class(hyperdb.Class):
                         self.db.addjournal(link_class, value, 'link',
                             (self.classname, nodeid, propname))
 
-            elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of'\
+            elif isinstance(prop, hyperdb.Multilink):
+                if value is None:
+                    value = []
+                if not hasattr(value, '__iter__'):
+                    raise TypeError, 'new property "%s" not an iterable of'\
                         ' ids'%propname
                 link_class = self.properties[propname].classname
                 l = []
@@ -1213,35 +1203,36 @@ class Class(hyperdb.Class):
                 if l:
                     journalvalues[propname] = tuple(l)
 
-            elif isinstance(prop, String):
+            elif isinstance(prop, hyperdb.String):
                 if value is not None and type(value) != type('') and type(value) != type(u''):
                     raise TypeError, 'new property "%s" not a string'%propname
-                self.db.indexer.add_text((self.classname, nodeid, propname),
-                    value)
+                if prop.indexme:
+                    self.db.indexer.add_text((self.classname, nodeid, propname),
+                        value)
 
-            elif isinstance(prop, Password):
+            elif isinstance(prop, hyperdb.Password):
                 if not isinstance(value, password.Password):
                     raise TypeError, 'new property "%s" not a Password'%propname
                 propvalues[propname] = value
 
-            elif value is not None and isinstance(prop, Date):
+            elif value is not None and isinstance(prop, hyperdb.Date):
                 if not isinstance(value, date.Date):
                     raise TypeError, 'new property "%s" not a Date'% propname
                 propvalues[propname] = value
 
-            elif value is not None and isinstance(prop, Interval):
+            elif value is not None and isinstance(prop, hyperdb.Interval):
                 if not isinstance(value, date.Interval):
                     raise TypeError, 'new property "%s" not an '\
                         'Interval'%propname
                 propvalues[propname] = value
 
-            elif value is not None and isinstance(prop, Number):
+            elif value is not None and isinstance(prop, hyperdb.Number):
                 try:
                     float(value)
                 except ValueError:
                     raise TypeError, 'new property "%s" not numeric'%propname
 
-            elif value is not None and isinstance(prop, Boolean):
+            elif value is not None and isinstance(prop, hyperdb.Boolean):
                 try:
                     int(value)
                 except ValueError:
@@ -1253,6 +1244,10 @@ class Class(hyperdb.Class):
         if not propvalues:
             return propvalues
 
+        # update the activity time
+        node['activity'] = date.Date()
+        node['actor'] = self.db.getuid()
+
         # do the set, and journal it
         self.db.setnode(self.classname, nodeid, node)
 
@@ -1263,10 +1258,10 @@ class Class(hyperdb.Class):
 
     def retire(self, nodeid):
         '''Retire a node.
-        
+
         The properties on the node remain available from the get() method,
         and the node's id is never reused.
-        
+
         Retired nodes are not returned by the find(), list(), or lookup()
         methods, and other nodes may reuse the values of their key properties.
 
@@ -1274,7 +1269,7 @@ class Class(hyperdb.Class):
         to modify the "creation" or "activity" properties cause a KeyError.
         '''
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         self.fireAuditors('retire', nodeid, None)
 
@@ -1292,7 +1287,7 @@ class Class(hyperdb.Class):
         Make node available for all operations like it was before retirement.
         '''
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
 
         node = self.db.getnode(self.classname, nodeid)
         # check if key property was overrided
@@ -1338,7 +1333,7 @@ class Class(hyperdb.Class):
         support the session storage of the cgi interface.
         '''
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
         self.db.destroynode(self.classname, nodeid)
 
     def history(self, nodeid):
@@ -1373,7 +1368,7 @@ class Class(hyperdb.Class):
         property doesn't exist, KeyError is raised.
         '''
         prop = self.getprops()[propname]
-        if not isinstance(prop, String):
+        if not isinstance(prop, hyperdb.String):
             raise TypeError, 'key properties must be String'
         self.key = propname
 
@@ -1381,31 +1376,6 @@ class Class(hyperdb.Class):
         '''Return the name of the key property for this class or None.'''
         return self.key
 
-    def labelprop(self, default_to_id=0):
-        '''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
-        '''
-        k = self.getkey()
-        if  k:
-            return k
-        props = self.getprops()
-        if props.has_key('name'):
-            return 'name'
-        elif props.has_key('title'):
-            return 'title'
-        if default_to_id:
-            return 'id'
-        props = props.keys()
-        props.sort()
-        return props[0]
-
     # TODO: set up a separate index db file for this? profile?
     def lookup(self, keyvalue):
         '''Locate a particular node by its key property and return its id.
@@ -1423,6 +1393,8 @@ class Class(hyperdb.Class):
                 node = self.db.getnode(self.classname, nodeid, cldb)
                 if node.has_key(self.db.RETIRED_FLAG):
                     continue
+                if not node.has_key(self.key):
+                    continue
                 if node[self.key] == keyvalue:
                     return nodeid
         finally:
@@ -1432,26 +1404,25 @@ class Class(hyperdb.Class):
 
     # change from spec - allows multiple props to match
     def find(self, **propspec):
-        '''Get the ids of items in this class which link to the given items.
+        '''Get the ids of nodes in this class which link to the given nodes.
 
-        'propspec' consists of keyword args propname=itemid or
-                   propname={itemid:1, }
+        '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 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:
+        Any node in this class whose 'propname' property links to any of
+        the nodeids will be returned. Examples::
 
+            db.issue.find(messages='1')
             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
         '''
         propspec = propspec.items()
         for propname, itemids in propspec:
             # check the prop is OK
             prop = self.properties[propname]
-            if not isinstance(prop, Link) and not isinstance(prop, Multilink):
+            if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink):
                 raise TypeError, "'%s' not a Link/Multilink property"%propname
 
         # ok, now do the find
@@ -1463,19 +1434,23 @@ class Class(hyperdb.Class):
                 if item.has_key(self.db.RETIRED_FLAG):
                     continue
                 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(itemids) is not type({}):
                         itemids = {itemids:1}
 
+                    # special case if the item doesn't have this property
+                    if not item.has_key(propname):
+                        if itemids.has_key(None):
+                            l.append(id)
+                            break
+                        continue
+
                     # grab the property definition and its value on this item
                     prop = self.properties[propname]
                     value = item[propname]
-                    if isinstance(prop, Link) and itemids.has_key(value):
+                    if isinstance(prop, hyperdb.Link) and itemids.has_key(value):
                         l.append(id)
                         break
-                    elif isinstance(prop, Multilink):
+                    elif isinstance(prop, hyperdb.Multilink):
                         hit = 0
                         for v in value:
                             if itemids.has_key(v):
@@ -1493,12 +1468,12 @@ class Class(hyperdb.Class):
         properties in a caseless search.
 
         If the property is not a String property, a TypeError is raised.
-        
+
         The return is a list of the id of all nodes that match.
         '''
         for propname in requirements.keys():
             prop = self.properties[propname]
-            if not isinstance(prop, String):
+            if not isinstance(prop, hyperdb.String):
                 raise TypeError, "'%s' not a String property"%propname
             requirements[propname] = requirements[propname].lower()
         l = []
@@ -1536,32 +1511,47 @@ class Class(hyperdb.Class):
         l.sort()
         return l
 
-    def getnodeids(self, db=None):
+    def getnodeids(self, db=None, retired=None):
         ''' Return a list of ALL nodeids
-        '''
-        if __debug__:
-            print >>hyperdb.DEBUG, 'getnodeids', (self, self.classname, db)
 
+            Set retired=None to get all nodes. Otherwise it'll get all the
+            retired or non-retired nodes, depending on the flag.
+        '''
         res = []
 
         # start off with the new nodes
         if self.db.newnodes.has_key(self.classname):
             res += self.db.newnodes[self.classname].keys()
 
+        must_close = False
         if db is None:
             db = self.db.getclassdb(self.classname)
-        res = res + db.keys()
+            must_close = True
+        try:
+            res = res + db.keys()
 
-        # remove the uncommitted, destroyed nodes
-        if self.db.destroyednodes.has_key(self.classname):
-            for nodeid in self.db.destroyednodes[self.classname].keys():
-                if db.has_key(nodeid):
-                    res.remove(nodeid)
+            # remove the uncommitted, destroyed nodes
+            if self.db.destroyednodes.has_key(self.classname):
+                for nodeid in self.db.destroyednodes[self.classname].keys():
+                    if db.has_key(nodeid):
+                        res.remove(nodeid)
 
+            # check retired flag
+            if retired is False or retired is True:
+                l = []
+                for nodeid in res:
+                    node = self.db.getnode(self.classname, nodeid, db)
+                    is_ret = node.has_key(self.db.RETIRED_FLAG)
+                    if retired == is_ret:
+                        l.append(nodeid)
+                res = l
+        finally:
+            if must_close:
+                db.close()
         return res
 
-    def filter(self, search_matches, filterspec, sort=(None,None),
-            group=(None,None), num_re = re.compile('^\d+$')):
+    def _filter(self, search_matches, filterspec, proptree,
+            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.
@@ -1571,30 +1561,32 @@ 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}
+        "search_matches" is {nodeid: marker} or None
 
-        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. Unless the property
-        is a Multilink, in which case the item's property list must
-        match the filterspec list.
+        The filter must match all properties specificed. If the property
+        value to match is a list:
+
+        1. String properties must match all elements in the list, and
+        2. Other properties must match any of the elements in the list.
         """
+        if __debug__:
+            start_t = time.time()
+
         cn = self.classname
 
         # optimise filterspec
         l = []
         props = self.getprops()
-        LINK = 0
-        MULTILINK = 1
-        STRING = 2
-        DATE = 3
-        INTERVAL = 4
-        OTHER = 6
-        
-        timezone = self.db.getUserTimezone()
+        LINK = 'spec:link'
+        MULTILINK = 'spec:multilink'
+        STRING = 'spec:string'
+        DATE = 'spec:date'
+        INTERVAL = 'spec:interval'
+        OTHER = 'spec:other'
+
         for k, v in filterspec.items():
             propclass = props[k]
-            if isinstance(propclass, Link):
+            if isinstance(propclass, hyperdb.Link):
                 if type(v) is not type([]):
                     v = [v]
                 u = []
@@ -1604,55 +1596,64 @@ class Class(hyperdb.Class):
                         entry = None
                     u.append(entry)
                 l.append((LINK, k, u))
-            elif isinstance(propclass, Multilink):
+            elif isinstance(propclass, hyperdb.Multilink):
                 # the value -1 is a special "not set" sentinel
                 if v in ('-1', ['-1']):
                     v = []
                 elif type(v) is not type([]):
                     v = [v]
                 l.append((MULTILINK, k, v))
-            elif isinstance(propclass, String) and k != 'id':
+            elif isinstance(propclass, hyperdb.String) and k != 'id':
                 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):
+                    l.append((STRING, k, re.compile(v, re.I)))
+            elif isinstance(propclass, hyperdb.Date):
                 try:
-                    date_rng = Range(v, date.Date, offset=timezone)
+                    date_rng = propclass.range_from_raw(v, self.db)
                     l.append((DATE, k, date_rng))
                 except ValueError:
                     # If range creation fails - ignore that search parameter
                     pass
-            elif isinstance(propclass, Interval):
+            elif isinstance(propclass, hyperdb.Interval):
                 try:
-                    intv_rng = Range(v, date.Interval)
+                    intv_rng = date.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
+
+            elif isinstance(propclass, hyperdb.Boolean):
+                if type(v) != type([]):
+                    v = v.split(',')
+                bv = []
+                for val in v:
+                    if type(val) is type(''):
+                        bv.append(val.lower() in ('yes', 'true', 'on', '1'))
+                    else:
+                        bv.append(val)
                 l.append((OTHER, k, bv))
-            elif isinstance(propclass, Number):
-                l.append((OTHER, k, int(v)))
-            else:
-                l.append((OTHER, k, v))
-        filterspec = l
 
+            elif k == 'id':
+                if type(v) != type([]):
+                    v = v.split(',')
+                l.append((OTHER, k, [str(int(val)) for val in v]))
+
+            elif isinstance(propclass, hyperdb.Number):
+                if type(v) != type([]):
+                    v = v.split(',')
+                l.append((OTHER, k, [float(val) for val in v]))
+
+        filterspec = l
+        
         # now, find all the nodes that are active and pass filtering
-        l = []
+        matches = []
         cldb = self.db.getclassdb(cn)
+        t = 0
         try:
             # TODO: only full-scan once (use items())
             for nodeid in self.getnodeids(cldb):
@@ -1662,171 +1663,152 @@ class Class(hyperdb.Class):
                 # apply filter
                 for t, k, v in filterspec:
                     # handle the id prop
-                    if k == 'id' and v == nodeid:
+                    if k == 'id':
+                        if nodeid not in v:
+                            break
                         continue
 
-                    # make sure the node has the property
-                    if not node.has_key(k):
-                        # this node doesn't have this property, so reject it
-                        break
+                    # get the node value
+                    nv = node.get(k, None)
+
+                    match = 0
 
                     # now apply the property filter
                     if t == LINK:
                         # link - if this node's property doesn't appear in the
                         # filterspec's nodeid list, skip it
-                        if node[k] not in v:
-                            break
+                        match = nv in v
                     elif t == MULTILINK:
                         # multilink - if any of the nodeids required by the
                         # filterspec aren't in this node's property, then skip
                         # it
-                        have = node[k]
-                        # check for matching the absence of multilink values
-                        if not v and have:
-                            break
+                        nv = node.get(k, [])
 
-                        # othewise, make sure this node has each of the
-                        # required values
-                        for want in v:
-                            if want not in have:
-                                break
+                        # check for matching the absence of multilink values
+                        if not v:
+                            match = not nv
                         else:
-                            continue
-                        break
+                            # othewise, make sure this node has each of the
+                            # required values
+                            for want in v:
+                                if want in nv:
+                                    match = 1
+                                    break
                     elif t == STRING:
-                        if node[k] is None:
-                            break
+                        if nv is None:
+                            nv = ''
                         # RE search
-                        if not v.search(node[k]):
-                            break
+                        match = v.search(nv)
                     elif t == DATE or t == INTERVAL:
-                        if node[k] is None:
-                            break
-                        if v.to_value:
-                            if not (v.from_value <= node[k] and v.to_value >= node[k]):
-                                break
+                        if nv is None:
+                            match = v is None
                         else:
-                            if not (v.from_value <= node[k]):
-                                break
+                            if v.to_value:
+                                if v.from_value <= nv and v.to_value >= nv:
+                                    match = 1
+                            else:
+                                if v.from_value <= nv:
+                                    match = 1
                     elif t == OTHER:
                         # straight value comparison for the other types
-                        if node[k] != v:
-                            break
+                        match = nv in v
+                    if not match:
+                        break
                 else:
-                    l.append((nodeid, node))
+                    matches.append([nodeid, node])
+
+            # filter based on full text search
+            if search_matches is not None:
+                k = []
+                for v in matches:
+                    if search_matches.has_key(v[0]):
+                        k.append(v)
+                matches = k
+
+            # add sorting information to the proptree
+            JPROPS = {'actor':1, 'activity':1, 'creator':1, 'creation':1}
+            children = []
+            if proptree:
+                children = proptree.sortable_children()
+            for pt in children:
+                dir = pt.sort_direction
+                prop = pt.name
+                assert (dir and prop)
+                propclass = props[prop]
+                pt.sort_ids = []
+                is_pointer = isinstance(propclass,(hyperdb.Link,
+                    hyperdb.Multilink))
+                if not is_pointer:
+                    pt.sort_result = []
+                try:
+                    # cache the opened link class db, if needed.
+                    lcldb = None
+                    # cache the linked class items too
+                    lcache = {}
+
+                    for entry in matches:
+                        itemid = entry[-2]
+                        item = entry[-1]
+                        # handle the properties that might be "faked"
+                        # also, handle possible missing properties
+                        try:
+                            v = item[prop]
+                        except KeyError:
+                            if JPROPS.has_key(prop):
+                                # force lookup of the special journal prop
+                                v = self.get(itemid, prop)
+                            else:
+                                # the node doesn't have a value for this
+                                # property
+                                v = None
+                                if isinstance(propclass, hyperdb.Multilink):
+                                    v = []
+                                if prop == 'id':
+                                    v = int (itemid)
+                                pt.sort_ids.append(v)
+                                if not is_pointer:
+                                    pt.sort_result.append(v)
+                                continue
+
+                        # missing (None) values are always sorted first
+                        if v is None:
+                            pt.sort_ids.append(v)
+                            if not is_pointer:
+                                pt.sort_result.append(v)
+                            continue
+
+                        if isinstance(propclass, hyperdb.Link):
+                            lcn = propclass.classname
+                            link = self.db.classes[lcn]
+                            key = link.orderprop()
+                            child = pt.propdict[key]
+                            if key!='id':
+                                if not lcache.has_key(v):
+                                    # open the link class db if it's not already
+                                    if lcldb is None:
+                                        lcldb = self.db.getclassdb(lcn)
+                                    lcache[v] = self.db.getnode(lcn, v, lcldb)
+                                r = lcache[v][key]
+                                child.propdict[key].sort_ids.append(r)
+                            else:
+                                child.propdict[key].sort_ids.append(v)
+                        pt.sort_ids.append(v)
+                        if not is_pointer:
+                            r = propclass.sort_repr(pt.parent.cls, v, pt.name)
+                            pt.sort_result.append(r)
+                finally:
+                    # if we opened the link class db, close it now
+                    if lcldb is not None:
+                        lcldb.close()
+                del lcache
         finally:
             cldb.close()
-        l.sort()
-
-        # filter based on full text search
-        if search_matches is not None:
-            k = []
-            for v in l:
-                if search_matches.has_key(v[0]):
-                    k.append(v)
-            l = k
-
-        # now, sort the result
-        def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
-                db = self.db, cl=self):
-            a_id, an = a
-            b_id, bn = b
-            # sort by group and then sort
-            for dir, prop in group, sort:
-                if dir is None or prop is None: continue
-
-                # sorting is class-specific
-                propclass = properties[prop]
-
-                # handle the properties that might be "faked"
-                # also, handle possible missing properties
-                try:
-                    if not an.has_key(prop):
-                        an[prop] = cl.get(a_id, prop)
-                    av = an[prop]
-                except KeyError:
-                    # the node doesn't have a value for this property
-                    if isinstance(propclass, Multilink): av = []
-                    else: av = ''
-                try:
-                    if not bn.has_key(prop):
-                        bn[prop] = cl.get(b_id, prop)
-                    bv = bn[prop]
-                except KeyError:
-                    # the node doesn't have a value for this property
-                    if isinstance(propclass, Multilink): bv = []
-                    else: bv = ''
-
-                # String and Date values are sorted in the natural way
-                if isinstance(propclass, String):
-                    # clean up the strings
-                    if av and av[0] in string.uppercase:
-                        av = av.lower()
-                    if bv and bv[0] in string.uppercase:
-                        bv = bv.lower()
-                if (isinstance(propclass, String) or
-                        isinstance(propclass, Date)):
-                    # it might be a string that's really an integer
-                    try:
-                        av = int(av)
-                        bv = int(bv)
-                    except:
-                        pass
-                    if dir == '+':
-                        r = cmp(av, bv)
-                        if r != 0: return r
-                    elif dir == '-':
-                        r = cmp(bv, av)
-                        if r != 0: return r
-
-                # Link properties are sorted according to the value of
-                # the "order" property on the linked nodes if it is
-                # present; or otherwise on the key string of the linked
-                # nodes; or finally on  the node ids.
-                elif isinstance(propclass, Link):
-                    link = db.classes[propclass.classname]
-                    if av is None and bv is not None: return -1
-                    if av is not None and bv is None: return 1
-                    if av is None and bv is None: continue
-                    if link.getprops().has_key('order'):
-                        if dir == '+':
-                            r = cmp(link.get(av, 'order'),
-                                link.get(bv, 'order'))
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(link.get(bv, 'order'),
-                                link.get(av, 'order'))
-                            if r != 0: return r
-                    elif link.getkey():
-                        key = link.getkey()
-                        if dir == '+':
-                            r = cmp(link.get(av, key), link.get(bv, key))
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(link.get(bv, key), link.get(av, key))
-                            if r != 0: return r
-                    else:
-                        if dir == '+':
-                            r = cmp(av, bv)
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(bv, av)
-                            if r != 0: return r
 
-                else:
-                    # all other types just compare
-                    if dir == '+':
-                        r = cmp(av, bv)
-                    elif dir == '-':
-                        r = cmp(bv, av)
-                    if r != 0: return r
-                    
-            # end for dir, prop in sort, group:
-            # if all else fails, compare the ids
-            return cmp(a[0], b[0])
-
-        l.sort(sortfun)
-        return [i[0] for i in l]
+        # pull the id out of the individual entries
+        matches = [entry[-2] for entry in matches]
+        if __debug__:
+            self.db.stats['filtering'] += (time.time() - start_t)
+        return matches
 
     def count(self):
         '''Get the number of nodes in this class.
@@ -1851,7 +1833,7 @@ class Class(hyperdb.Class):
         '''
         d = self.properties.copy()
         if protected:
-            d['id'] = String()
+            d['id'] = hyperdb.String()
             d['creation'] = hyperdb.Date()
             d['activity'] = hyperdb.Date()
             d['creator'] = hyperdb.Link('user')
@@ -1884,36 +1866,6 @@ class Class(hyperdb.Class):
                     continue
                 self.db.indexer.add_text((self.classname, nodeid, prop), value)
 
-
-    #
-    # Detector interface
-    #
-    def audit(self, event, detector):
-        '''Register a detector
-        '''
-        l = self.auditors[event]
-        if detector not in l:
-            self.auditors[event].append(detector)
-
-    def fireAuditors(self, action, nodeid, newvalues):
-        '''Fire all registered auditors.
-        '''
-        for audit in self.auditors[action]:
-            audit(self.db, self, nodeid, newvalues)
-
-    def react(self, event, detector):
-        '''Register a detector
-        '''
-        l = self.reactors[event]
-        if detector not in l:
-            self.reactors[event].append(detector)
-
-    def fireReactors(self, action, nodeid, oldvalues):
-        '''Fire all registered reactors.
-        '''
-        for react in self.reactors[action]:
-            react(self.db, self, nodeid, oldvalues)
-
     #
     # import / export support
     #
@@ -1951,7 +1903,7 @@ class Class(hyperdb.Class):
             Return the nodeid of the node imported.
         '''
         if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
+            raise hyperdb.DatabaseError, _('Database open read-only')
         properties = self.getprops()
 
         # make the new node's property map
@@ -2010,52 +1962,65 @@ class Class(hyperdb.Class):
             for nodeid, date, user, action, params in self.history(nodeid):
                 date = date.get_tuple()
                 if action == 'set':
+                    export_data = {}
                     for propname, value in params.items():
+                        if not properties.has_key(propname):
+                            # property no longer in the schema
+                            continue
+
                         prop = properties[propname]
                         # make sure the params are eval()'able
                         if value is None:
                             pass
-                        elif isinstance(prop, Date):
-                            value = value.get_tuple()
-                        elif isinstance(prop, Interval):
-                            value = value.get_tuple()
-                        elif isinstance(prop, Password):
+                        elif isinstance(prop, hyperdb.Date):
+                            # this is a hack - some dates are stored as strings
+                            if not isinstance(value, type('')):
+                                value = value.get_tuple()
+                        elif isinstance(prop, hyperdb.Interval):
+                            # hack too - some intervals are stored as strings
+                            if not isinstance(value, type('')):
+                                value = value.get_tuple()
+                        elif isinstance(prop, hyperdb.Password):
                             value = str(value)
-                        params[propname] = value
+                        export_data[propname] = value
+                    params = export_data
                 l = [nodeid, date, user, action, params]
                 r.append(map(repr, l))
         return r
 
     def import_journals(self, entries):
         '''Import a class's journal.
-        
+
         Uses setjournal() to set the journal for each item.'''
         properties = self.getprops()
         d = {}
         for l in entries:
             l = map(eval, l)
-            nodeid, date, user, action, params = l
+            nodeid, jdate, user, action, params = l
             r = d.setdefault(nodeid, [])
             if action == 'set':
                 for propname, value in params.items():
                     prop = properties[propname]
                     if value is None:
                         pass
-                    elif isinstance(prop, Date):
+                    elif isinstance(prop, hyperdb.Date):
+                        if type(value) == type(()):
+                            print _('WARNING: invalid date tuple %r')%(value,)
+                            value = date.Date( "2000-1-1" )
                         value = date.Date(value)
-                    elif isinstance(prop, Interval):
+                    elif isinstance(prop, hyperdb.Interval):
                         value = date.Interval(value)
-                    elif isinstance(prop, Password):
+                    elif isinstance(prop, hyperdb.Password):
                         pwd = password.Password()
                         pwd.unpack(value)
                         value = pwd
                     params[propname] = value
-            r.append((nodeid, date.Date(date), user, action, params))
+            r.append((nodeid, date.Date(jdate), user, action, params))
 
         for nodeid, l in d.items():
             self.db.setjournal(self.classname, nodeid, l)
 
-class FileClass(Class, hyperdb.FileClass):
+class FileClass(hyperdb.FileClass, Class):
     '''This class defines a large chunk of data. To support this, it has a
        mandatory String property "content" which is typically saved off
        externally to the hyperdb.
@@ -2064,7 +2029,15 @@ class FileClass(Class, hyperdb.FileClass):
        "default_mime_type" class attribute, which may be overridden by each
        node if the class defines a "type" String property.
     '''
-    default_mime_type = 'text/plain'
+    def __init__(self, db, classname, **properties):
+        '''The newly-created class automatically includes the "content"
+        and "type" properties.
+        '''
+        if not properties.has_key('content'):
+            properties['content'] = hyperdb.String(indexme='yes')
+        if not properties.has_key('type'):
+            properties['type'] = hyperdb.String()
+        Class.__init__(self, db, classname, **properties)
 
     def create(self, **propvalues):
         ''' Snarf the "content" propvalue and store in a file
@@ -2083,34 +2056,12 @@ class FileClass(Class, hyperdb.FileClass):
         # do the database create
         newid = self.create_inner(**propvalues)
 
-        # and index!
-        self.db.indexer.add_text((self.classname, newid, 'content'), content,
-            mime_type)
-
-        # fire reactors
-        self.fireReactors('create', newid, None)
-
         # store off the content as a file
         self.db.storefile(self.classname, newid, None, content)
-        return newid
-
-    def import_list(self, propnames, proplist):
-        ''' Trap the "content" property...
-        '''
-        # dupe this list so we don't affect others
-        propnames = propnames[:]
 
-        # extract the "content" property from the proplist
-        i = propnames.index('content')
-        content = eval(proplist[i])
-        del propnames[i]
-        del proplist[i]
-
-        # do the normal import
-        newid = Class.import_list(self, propnames, proplist)
+        # fire reactors
+        self.fireReactors('create', newid, None)
 
-        # save off the "content" file
-        self.db.storefile(self.classname, newid, None, content)
         return newid
 
     def get(self, nodeid, propname, default=_marker, cache=1):
@@ -2123,7 +2074,7 @@ class FileClass(Class, hyperdb.FileClass):
             try:
                 return self.db.getfile(self.classname, nodeid, None)
             except IOError, (strerror):
-                # XXX by catching this we donot see an error in the log.
+                # XXX by catching this we don't 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:
@@ -2135,7 +2086,16 @@ class FileClass(Class, hyperdb.FileClass):
         ''' Snarf the "content" propvalue and update it in a file
         '''
         self.fireAuditors('set', itemid, propvalues)
+
+        # create the oldvalues dict - fill in any missing values
         oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
+        for name,prop in self.getprops(protected=0).items():
+            if oldvalues.has_key(name):
+                continue
+            if isinstance(prop, hyperdb.Multilink):
+                oldvalues[name] = []
+            else:
+                oldvalues[name] = None
 
         # now remove the content property so it's not stored in the db
         content = None
@@ -2143,33 +2103,42 @@ class FileClass(Class, hyperdb.FileClass):
             content = propvalues['content']
             del propvalues['content']
 
-        # do the database create
+        # do the database update
         propvalues = self.set_inner(itemid, **propvalues)
 
         # do content?
         if content:
-            # store and index
+            # store and possibly index
             self.db.storefile(self.classname, itemid, None, content)
-            mime_type = propvalues.get('type', self.get(itemid, 'type'))
-            if not mime_type:
-                mime_type = self.default_mime_type
-            self.db.indexer.add_text((self.classname, itemid, 'content'),
-                content, mime_type)
+            if self.properties['content'].indexme:
+                mime_type = self.get(itemid, 'type', self.default_mime_type)
+                self.db.indexer.add_text((self.classname, itemid, 'content'),
+                    content, mime_type)
+            propvalues['content'] = content
 
         # fire reactors
         self.fireReactors('set', itemid, oldvalues)
         return propvalues
 
-    def getprops(self, protected=1):
-        ''' In addition to the actual properties on the node, these methods
-            provide the "content" property. If the "protected" flag is true,
-            we include protected properties - those which may not be
-            modified.
-        '''
-        d = Class.getprops(self, protected=protected).copy()
-        d['content'] = hyperdb.String()
-        return d
+    def index(self, nodeid):
+        ''' Add (or refresh) the node to search indexes.
 
+        Use the content-type property for the content property.
+        '''
+        # find all the String properties that have indexme
+        for prop, propclass in self.getprops().items():
+            if prop == 'content' and propclass.indexme:
+                mime_type = self.get(nodeid, 'type', self.default_mime_type)
+                self.db.indexer.add_text((self.classname, nodeid, 'content'),
+                    str(self.get(nodeid, 'content')), mime_type)
+            elif isinstance(propclass, hyperdb.String) and propclass.indexme:
+                # index them under (classname, nodeid, property)
+                try:
+                    value = str(self.get(nodeid, prop))
+                except IndexError:
+                    # node has been destroyed
+                    continue
+                self.db.indexer.add_text((self.classname, nodeid, prop), value)
 
 # deviation from spec - was called ItemClass
 class IssueClass(Class, roundupdb.IssueClass):
@@ -2194,4 +2163,4 @@ class IssueClass(Class, roundupdb.IssueClass):
             properties['superseder'] = hyperdb.Multilink(classname)
         Class.__init__(self, db, classname, **properties)
 
-#
+# vim: set et sts=4 sw=4 :