diff roundup/backends/back_metakit.py @ 1356:83f33642d220 maint-0.5

[[Metadata associated with this commit was garbled during conversion from CVS to Subversion.]]
author Richard Jones <richard@users.sourceforge.net>
date Thu, 09 Jan 2003 22:59:22 +0000
parents
children ad9647a6d9fc
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/roundup/backends/back_metakit.py	Thu Jan 09 22:59:22 2003 +0000
@@ -0,0 +1,1332 @@
+from roundup import hyperdb, date, password, roundupdb, security
+import metakit
+from sessions import Sessions
+import re, marshal, os, sys, weakref, time, calendar
+from roundup import indexer
+import locking
+
+_dbs = {}
+
+def Database(config, journaltag=None):
+    db = _dbs.get(config.DATABASE, None)
+    if db is None or db._db is None:
+        db = _Database(config, journaltag)
+        _dbs[config.DATABASE] = db
+    else:
+        db.journaltag = journaltag
+        try:
+            delattr(db, 'curuserid')
+        except AttributeError:
+            pass
+    return db
+
+class _Database(hyperdb.Database):
+    def __init__(self, config, journaltag=None):
+        self.config = config
+        self.journaltag = journaltag
+        self.classes = {}
+        self.dirty = 0
+        self.lockfile = None
+        self._db = self.__open()
+        self.indexer = Indexer(self.config.DATABASE, self._db)
+        self.sessions = Sessions(self.config)
+        self.security = security.Security(self)
+
+        os.umask(0002)
+
+    def post_init(self):
+        if self.indexer.should_reindex():
+            self.reindex()
+
+    def reindex(self):
+        for klass in self.classes.values():
+            for nodeid in klass.list():
+                klass.index(nodeid)
+        self.indexer.save_index()
+
+    # --- defined in ping's spec
+    def __getattr__(self, classname):
+        if classname == 'curuserid':
+            if self.journaltag is None:
+                return None
+
+            try:
+                self.curuserid = x = int(self.classes['user'].lookup(self.journaltag))
+            except KeyError:
+                if self.journaltag == 'admin':
+                    self.curuserid = x = 1
+                else:
+                    x = 0
+            return x
+        elif classname == 'transactions':
+            return self.dirty
+        return self.getclass(classname)
+    def getclass(self, classname):
+        try:
+            return self.classes[classname]
+        except KeyError:
+            raise KeyError, 'There is no class called "%s"'%classname
+    def getclasses(self):
+        return self.classes.keys()
+    # --- end of ping's spec 
+    # --- exposed methods
+    def commit(self):
+        if self.dirty:
+            self._db.commit()
+            for cl in self.classes.values():
+                cl._commit()
+            self.indexer.save_index()
+        self.dirty = 0
+    def rollback(self):
+        if self.dirty:
+            for cl in self.classes.values():
+                cl._rollback()
+            self._db.rollback()
+            self._db = None
+            self._db = metakit.storage(self.dbnm, 1)
+            self.hist = self._db.view('history')
+            self.tables = self._db.view('tables')
+            self.indexer.rollback()
+            self.indexer.datadb = self._db
+        self.dirty = 0
+    def clearCache(self):
+        for cl in self.classes.values():
+            cl._commit()
+    def clear(self):
+        for cl in self.classes.values():
+            cl._clear()
+    def hasnode(self, classname, nodeid):
+        return self.getclass(classname).hasnode(nodeid)
+    def pack(self, pack_before):
+        mindate = int(calendar.timegm(pack_before.get_tuple()))
+        i = 0
+        while i < len(self.hist):
+            if self.hist[i].date < mindate and self.hist[i].action != _CREATE:
+                self.hist.delete(i)
+            else:
+                i = i + 1
+    def addclass(self, cl):
+        self.classes[cl.classname] = cl
+        if self.tables.find(name=cl.classname) < 0:
+            self.tables.append(name=cl.classname)
+    def addjournal(self, tablenm, nodeid, action, params, creator=None,
+                creation=None):
+        tblid = self.tables.find(name=tablenm)
+        if tblid == -1:
+            tblid = self.tables.append(name=tablenm)
+        if creator is None:
+            creator = self.curuserid
+        else:
+            try:
+                creator = int(creator)
+            except TypeError:
+                creator = int(self.getclass('user').lookup(creator))
+        if creation is None:
+            creation = int(time.time())
+        elif isinstance(creation, date.Date):
+            creation = int(calendar.timegm(creation.get_tuple()))
+        # tableid:I,nodeid:I,date:I,user:I,action:I,params:B
+        self.hist.append(tableid=tblid,
+                         nodeid=int(nodeid),
+                         date=creation,
+                         action=action,
+                         user = creator,
+                         params = marshal.dumps(params))
+    def getjournal(self, tablenm, nodeid):
+        rslt = []
+        tblid = self.tables.find(name=tablenm)
+        if tblid == -1:
+            return rslt
+        q = self.hist.select(tableid=tblid, nodeid=int(nodeid))
+        if len(q) == 0:
+            raise IndexError, "no history for id %s in %s" % (nodeid, tablenm)
+        i = 0
+        #userclass = self.getclass('user')
+        for row in q:
+            try:
+                params = marshal.loads(row.params)
+            except ValueError:
+                print "history couldn't unmarshal %r" % row.params
+                params = {}
+            #usernm = userclass.get(str(row.user), 'username')
+            dt = date.Date(time.gmtime(row.date))
+            #rslt.append((nodeid, dt, usernm, _actionnames[row.action], params))
+            rslt.append((nodeid, dt, str(row.user), _actionnames[row.action], params))
+        return rslt
+            
+    def destroyjournal(self, tablenm, nodeid):
+        nodeid = int(nodeid)
+        tblid = self.tables.find(name=tablenm)
+        if tblid == -1:
+            return 
+        i = 0
+        hist = self.hist
+        while i < len(hist):
+            if hist[i].tableid == tblid and hist[i].nodeid == nodeid:
+                hist.delete(i)
+            else:
+                i = i + 1
+        self.dirty = 1
+        
+    def close(self):
+        for cl in self.classes.values():
+            cl.db = None
+        self._db = None
+        if self.lockfile is not None:
+            locking.release_lock(self.lockfile)
+        if _dbs.has_key(self.config.DATABASE):
+            del _dbs[self.config.DATABASE]
+        if self.lockfile is not None:
+            self.lockfile.close()
+            self.lockfile = None
+        self.classes = {}
+        self.indexer = None
+
+    # --- internal
+    def __open(self):
+        if not os.path.exists(self.config.DATABASE):
+            os.makedirs(self.config.DATABASE)
+        self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
+        lockfilenm = db[:-3]+'lck'
+        self.lockfile = locking.acquire_lock(lockfilenm)
+        self.lockfile.write(str(os.getpid()))
+        self.lockfile.flush()
+        self.fastopen = 0
+        if os.path.exists(db):
+            dbtm = os.path.getmtime(db)
+            pkgnm = self.config.__name__.split('.')[0]
+            schemamod = sys.modules.get(pkgnm+'.dbinit', None)
+            if schemamod:
+                if os.path.exists(schemamod.__file__):
+                    schematm = os.path.getmtime(schemamod.__file__)
+                    if schematm < dbtm:
+                        # found schema mod - it's older than the db
+                        self.fastopen = 1
+                else:
+                     # can't find schemamod - must be frozen
+                    self.fastopen = 1
+        db = metakit.storage(db, 1)
+        hist = db.view('history')
+        tables = db.view('tables')
+        if not self.fastopen:
+            if not hist.structure():
+                hist = db.getas('history[tableid:I,nodeid:I,date:I,user:I,action:I,params:B]')
+            if not tables.structure():
+                tables = db.getas('tables[name:S]')
+            db.commit()
+        self.tables = tables
+        self.hist = hist
+        return db
+        
+_STRINGTYPE = type('')
+_LISTTYPE = type([])
+_CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
+
+_actionnames = {
+    _CREATE : 'create',
+    _SET : 'set',
+    _RETIRE : 'retire',
+    _LINK : 'link',
+    _UNLINK : 'unlink',
+}
+
+_marker = []
+
+_ALLOWSETTINGPRIVATEPROPS = 0
+
+class Class:    
+    privateprops = None
+    def __init__(self, db, classname, **properties):
+        #self.db = weakref.proxy(db)
+        self.db = db
+        self.classname = classname
+        self.keyname = None
+        self.ruprops = properties
+        self.privateprops = { 'id' : hyperdb.String(),
+                              'activity' : hyperdb.Date(),
+                              'creation' : hyperdb.Date(),
+                              'creator'  : hyperdb.Link('user') }
+
+        # event -> list of callables
+        self.auditors = {'create': [], 'set': [], 'retire': []}
+        self.reactors = {'create': [], 'set': [], 'retire': []}
+
+        view = self.__getview()
+        self.maxid = 1
+        if view:
+            self.maxid = view[-1].id + 1
+        self.uncommitted = {}
+        self.rbactions = []
+
+        # people reach inside!!
+        self.properties = self.ruprops
+        self.db.addclass(self)
+        self.idcache = {}
+
+        # default is to journal changes
+        self.do_journal = 1
+
+    def enableJournalling(self):
+        '''Turn journalling on for this class
+        '''
+        self.do_journal = 1
+
+    def disableJournalling(self):
+        '''Turn journalling off for this class
+        '''
+        self.do_journal = 0
+        
+    # --- the roundup.Class methods
+    def audit(self, event, detector):
+        l = self.auditors[event]
+        if detector not in l:
+            self.auditors[event].append(detector)
+    def fireAuditors(self, action, nodeid, newvalues):
+        for audit in self.auditors[action]:
+            audit(self.db, self, nodeid, newvalues)
+    def fireReactors(self, action, nodeid, oldvalues):
+        for react in self.reactors[action]:
+            react(self.db, self, nodeid, oldvalues)
+    def react(self, event, detector):
+        l = self.reactors[event]
+        if detector not in l:
+            self.reactors[event].append(detector)
+
+    # --- the hyperdb.Class methods
+    def create(self, **propvalues):
+        self.fireAuditors('create', None, propvalues)
+        rowdict = {}
+        rowdict['id'] = newid = self.maxid
+        self.maxid += 1
+        ndx = self.getview(1).append(rowdict)
+        propvalues['#ISNEW'] = 1
+        try:
+            self.set(str(newid), **propvalues)
+        except Exception:
+            self.maxid -= 1
+            raise
+        return str(newid)
+    
+    def get(self, nodeid, propname, default=_marker, cache=1):
+        # default and cache aren't in the spec
+        # cache=0 means "original value"
+
+        view = self.getview()        
+        id = int(nodeid)
+        if cache == 0:
+            oldnode = self.uncommitted.get(id, None)
+            if oldnode and oldnode.has_key(propname):
+                return oldnode[propname]
+        ndx = self.idcache.get(id, None)
+        if ndx is None:
+            ndx = view.find(id=id)
+            if ndx < 0:
+                raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+            self.idcache[id] = ndx
+        try:
+            raw = getattr(view[ndx], propname)
+        except AttributeError:
+            raise KeyError, propname
+        rutyp = self.ruprops.get(propname, None)
+        if rutyp is None:
+            rutyp = self.privateprops[propname]
+        converter = _converters.get(rutyp.__class__, None)
+        if converter:
+            raw = converter(raw)
+        return raw
+        
+    def set(self, nodeid, **propvalues):
+        isnew = 0
+        if propvalues.has_key('#ISNEW'):
+            isnew = 1
+            del propvalues['#ISNEW']
+        if not isnew:
+            self.fireAuditors('set', nodeid, propvalues)
+        if not propvalues:
+            return propvalues
+        if propvalues.has_key('id'):
+            raise KeyError, '"id" is reserved'
+        if self.db.journaltag is None:
+            raise hyperdb.DatabaseError, 'Database open read-only'
+        view = self.getview(1)
+
+        # node must exist & not be retired
+        id = int(nodeid)
+        ndx = view.find(id=id)
+        if ndx < 0:
+            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+        row = view[ndx]
+        if row._isdel:
+            raise IndexError, "%s has no node %s" % (self.classname, nodeid)
+        oldnode = self.uncommitted.setdefault(id, {})
+        changes = {}
+
+        for key, value in propvalues.items():
+            # 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.
+            if _ALLOWSETTINGPRIVATEPROPS:
+                prop = self.ruprops.get(key, None)
+                if not prop:
+                    prop = self.privateprops[key]
+            else:
+                prop = self.ruprops[key]
+            converter = _converters.get(prop.__class__, lambda v: v)
+            # if the value's the same as the existing value, no sense in
+            # doing anything
+            oldvalue = converter(getattr(row, key))
+            if  value == oldvalue:
+                del propvalues[key]
+                continue
+            
+            # check to make sure we're not duplicating an existing key
+            if key == self.keyname:
+                iv = self.getindexview(1)
+                ndx = iv.find(k=value)
+                if ndx == -1:
+                    iv.append(k=value, i=row.id)
+                    if not isnew:
+                        ndx = iv.find(k=oldvalue)
+                        if ndx > -1:
+                            iv.delete(ndx)
+                else:
+                    raise ValueError, 'node with key "%s" exists'%value
+
+            # do stuff based on the prop type
+            if isinstance(prop, hyperdb.Link):
+                link_class = prop.classname
+                # must be a string or None
+                if value is not None and not isinstance(value, type('')):
+                    raise ValueError, 'property "%s" link value be a string'%(
+                        key)
+                # Roundup sets to "unselected" by passing None
+                if value is None:
+                    value = 0   
+                # if it isn't a number, it's a key
+                try:
+                    int(value)
+                except ValueError:
+                    try:
+                        value = self.db.getclass(link_class).lookup(value)
+                    except (TypeError, KeyError):
+                        raise IndexError, 'new property "%s": %s not a %s'%(
+                            key, value, prop.classname)
+
+                if (value is not None and
+                        not self.db.getclass(link_class).hasnode(value)):
+                    raise IndexError, '%s has no node %s'%(link_class, value)
+
+                setattr(row, key, int(value))
+                changes[key] = oldvalue
+                
+                if self.do_journal and prop.do_journal:
+                    # register the unlink with the old linked node
+                    if oldvalue:
+                        self.db.addjournal(link_class, value, _UNLINK,
+                            (self.classname, str(row.id), key))
+
+                    # register the link with the newly linked node
+                    if value:
+                        self.db.addjournal(link_class, value, _LINK,
+                            (self.classname, str(row.id), key))
+
+            elif isinstance(prop, hyperdb.Multilink):
+                if value is not None and type(value) != _LISTTYPE:
+                    raise TypeError, 'new property "%s" not a list of ids'%key
+                link_class = prop.classname
+                l = []
+                if value is None:
+                    value = []
+                for entry in value:
+                    if type(entry) != _STRINGTYPE:
+                        raise ValueError, 'new property "%s" link value ' \
+                            'must be a string'%key
+                    # if it isn't a number, it's a key
+                    try:
+                        int(entry)
+                    except ValueError:
+                        try:
+                            entry = self.db.getclass(link_class).lookup(entry)
+                        except (TypeError, KeyError):
+                            raise IndexError, 'new property "%s": %s not a %s'%(
+                                key, entry, prop.classname)
+                    l.append(entry)
+                propvalues[key] = value = l
+
+                # handle removals
+                rmvd = []
+                for id in oldvalue:
+                    if id not in value:
+                        rmvd.append(id)
+                        # register the unlink with the old linked node
+                        if self.do_journal and prop.do_journal:
+                            self.db.addjournal(link_class, id, _UNLINK,
+                                (self.classname, str(row.id), key))
+
+                # handle additions
+                adds = []
+                for id in value:
+                    if id not in oldvalue:
+                        if not self.db.getclass(link_class).hasnode(id):
+                            raise IndexError, '%s has no node %s'%(
+                                link_class, id)
+                        adds.append(id)
+                        # register the link with the newly linked node
+                        if self.do_journal and prop.do_journal:
+                            self.db.addjournal(link_class, id, _LINK,
+                                (self.classname, str(row.id), key))
+                            
+                sv = getattr(row, key)
+                i = 0
+                while i < len(sv):
+                    if str(sv[i].fid) in rmvd:
+                        sv.delete(i)
+                    else:
+                        i += 1
+                for id in adds:
+                    sv.append(fid=int(id))
+                changes[key] = oldvalue
+                if not rmvd and not adds:
+                    del propvalues[key]
+                    
+            elif isinstance(prop, hyperdb.String):
+                if value is not None and type(value) != _STRINGTYPE:
+                    raise TypeError, 'new property "%s" not a string'%key
+                if value is None:
+                    value = ''
+                setattr(row, key, value)
+                changes[key] = oldvalue
+                if hasattr(prop, 'isfilename') and prop.isfilename:
+                    propvalues[key] = os.path.basename(value)
+                if prop.indexme:
+                    self.db.indexer.add_text((self.classname, nodeid, key),
+                        value, 'text/plain')
+
+            elif isinstance(prop, hyperdb.Password):
+                if value is not None and not isinstance(value, password.Password):
+                    raise TypeError, 'new property "%s" not a Password'% key
+                if value is None:
+                    value = ''
+                setattr(row, key, str(value))
+                changes[key] = str(oldvalue)
+                propvalues[key] = str(value)
+
+            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
+                if value is None:
+                    setattr(row, key, 0)
+                else:
+                    setattr(row, key, int(calendar.timegm(value.get_tuple())))
+                changes[key] = str(oldvalue)
+                propvalues[key] = str(value)
+
+            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
+                if value is None:
+                    setattr(row, key, '')
+                else:
+                    setattr(row, key, str(value))
+                changes[key] = str(oldvalue)
+                propvalues[key] = str(value)
+                
+            elif isinstance(prop, hyperdb.Number):
+                if value is None:
+                    value = 0
+                try:
+                    v = int(value)
+                except ValueError:
+                    raise TypeError, "%s (%s) is not numeric" % (key, repr(value))
+                setattr(row, key, v)
+                changes[key] = oldvalue
+                propvalues[key] = value
+
+            elif isinstance(prop, hyperdb.Boolean):
+                if value is None:
+                    bv = 0
+                elif value not in (0,1):
+                    raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
+                else:
+                    bv = value 
+                setattr(row, key, bv)
+                changes[key] = oldvalue
+                propvalues[key] = value
+
+            oldnode[key] = oldvalue
+
+        # nothing to do?
+        if not propvalues:
+            return propvalues
+        if not propvalues.has_key('activity'):
+            row.activity = int(time.time())
+        if isnew:
+            if not row.creation:
+                row.creation = int(time.time())
+            if not row.creator:
+                row.creator = self.db.curuserid
+            
+        self.db.dirty = 1
+        if self.do_journal:
+            if isnew:
+                self.db.addjournal(self.classname, nodeid, _CREATE, {})
+                self.fireReactors('create', nodeid, None)
+            else:
+                self.db.addjournal(self.classname, nodeid, _SET, changes)
+                self.fireReactors('set', nodeid, oldnode)
+
+        return propvalues
+    
+    def retire(self, nodeid):
+        if self.db.journaltag is None:
+            raise hyperdb.DatabaseError, 'Database open read-only'
+        self.fireAuditors('retire', nodeid, None)
+        view = self.getview(1)
+        ndx = view.find(id=int(nodeid))
+        if ndx < 0:
+            raise KeyError, "nodeid %s not found" % nodeid
+        row = view[ndx]
+        oldvalues = self.uncommitted.setdefault(row.id, {})
+        oldval = oldvalues['_isdel'] = row._isdel
+        row._isdel = 1
+        if self.do_journal:
+            self.db.addjournal(self.classname, nodeid, _RETIRE, {})
+        if self.keyname:
+            iv = self.getindexview(1)
+            ndx = iv.find(k=getattr(row, self.keyname),i=row.id)
+            if ndx > -1:
+                iv.delete(ndx)
+        self.db.dirty = 1
+        self.fireReactors('retire', nodeid, None)
+
+    def history(self, nodeid):
+        if not self.do_journal:
+            raise ValueError, 'Journalling is disabled for this class'
+        return self.db.getjournal(self.classname, nodeid)
+
+    def setkey(self, propname):
+        if self.keyname:
+            if propname == self.keyname:
+                return
+            raise ValueError, "%s already indexed on %s"%(self.classname,
+                self.keyname)
+        prop = self.properties.get(propname, None)
+        if prop is None:
+            prop = self.privateprops.get(propname, None)
+        if prop is None:
+            raise KeyError, "no property %s" % propname
+        if not isinstance(prop, hyperdb.String):
+            raise TypeError, "%s is not a String" % propname
+
+        # first setkey for this run
+        self.keyname = propname
+        iv = self.db._db.view('_%s' % self.classname)
+        if self.db.fastopen and iv.structure():
+            return
+
+        # very first setkey ever
+        self.db.dirty = 1
+        iv = self.db._db.getas('_%s[k:S,i:I]' % self.classname)
+        iv = iv.ordered(1)
+        for row in self.getview():
+            iv.append(k=getattr(row, propname), i=row.id)
+        self.db.commit()
+
+    def getkey(self):
+        return self.keyname
+
+    def lookup(self, keyvalue):
+        if type(keyvalue) is not _STRINGTYPE:
+            raise TypeError, "%r is not a string" % keyvalue
+        iv = self.getindexview()
+        if iv:
+            ndx = iv.find(k=keyvalue)
+            if ndx > -1:
+                return str(iv[ndx].i)
+        else:
+            view = self.getview()
+            ndx = view.find({self.keyname:keyvalue, '_isdel':0})
+            if ndx > -1:
+                return str(view[ndx].id)
+        raise KeyError, keyvalue
+
+    def destroy(self, id):
+        view = self.getview(1)
+        ndx = view.find(id=int(id))
+        if ndx > -1:
+            if self.keyname:
+                keyvalue = getattr(view[ndx], self.keyname)
+                iv = self.getindexview(1)
+                if iv:
+                    ivndx = iv.find(k=keyvalue)
+                    if ivndx > -1:
+                        iv.delete(ivndx)
+            view.delete(ndx)
+            self.db.destroyjournal(self.classname, id)
+            self.db.dirty = 1
+        
+    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.
+
+        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:
+
+            db.issue.find(messages={'1':1,'3':1}, files={'7':1})
+
+        """
+        propspec = propspec.items()
+        for propname, nodeid in propspec:
+            # check the prop is OK
+            prop = self.ruprops[propname]
+            if (not isinstance(prop, hyperdb.Link) and
+                    not isinstance(prop, hyperdb.Multilink)):
+                raise TypeError, "'%s' not a Link/Multilink property"%propname
+
+        vws = []
+        for propname, ids in propspec:
+            if type(ids) is _STRINGTYPE:
+                ids = {int(ids):1}
+            else:
+                d = {}
+                for id in ids.keys():
+                    d[int(id)] = 1
+                ids = d
+            prop = self.ruprops[propname]
+            view = self.getview()
+            if isinstance(prop, hyperdb.Multilink):
+                def ff(row, nm=propname, ids=ids):
+                    sv = getattr(row, nm)
+                    for sr in sv:
+                        if ids.has_key(sr.fid):
+                            return 1
+                    return 0
+            else:
+                def ff(row, nm=propname, ids=ids):
+                    return ids.has_key(getattr(row, nm))
+            ndxview = view.filter(ff)
+            vws.append(ndxview.unique())
+
+        # handle the empty match case
+        if not vws:
+            return []
+
+        ndxview = vws[0]
+        for v in vws[1:]:
+            ndxview = ndxview.union(v)
+        view = self.getview().remapwith(ndxview)
+        rslt = []
+        for row in view:
+            rslt.append(str(row.id))
+        return rslt
+            
+
+    def list(self):
+        l = []
+        for row in self.getview().select(_isdel=0):
+            l.append(str(row.id))
+        return l
+
+    def count(self):
+        return len(self.getview())
+
+    def getprops(self, protected=1):
+        # protected is not in ping's spec
+        allprops = self.ruprops.copy()
+        if protected and self.privateprops is not None:
+            allprops.update(self.privateprops)
+        return allprops
+
+    def addprop(self, **properties):
+        for key in properties.keys():
+            if self.ruprops.has_key(key):
+                raise ValueError, "%s is already a property of %s"%(key,
+                    self.classname)
+        self.ruprops.update(properties)
+        self.db.fastopen = 0
+        view = self.__getview()
+        self.db.commit()
+    # ---- end of ping's spec
+
+    def filter(self, search_matches, filterspec, sort=(None,None),
+            group=(None,None)):
+        # search_matches is None or a set (dict of {nodeid: {propname:[nodeid,...]}})
+        # filterspec is a dict {propname:value}
+        # sort and group are (dir, prop) where dir is '+', '-' or None
+        #                    and prop is a prop name or None
+        where = {'_isdel':0}
+        mlcriteria = {}
+        regexes = {}
+        orcriteria = {}
+        for propname, value in filterspec.items():
+            prop = self.ruprops.get(propname, None)
+            if prop is None:
+                prop = self.privateprops[propname]
+            if isinstance(prop, hyperdb.Multilink):
+                if type(value) is not _LISTTYPE:
+                    value = [value]
+                # transform keys to ids
+                u = []
+                for item in value:
+                    try:
+                        item = int(item)
+                    except (TypeError, ValueError):
+                        item = int(self.db.getclass(prop.classname).lookup(item))
+                    if item == -1:
+                        item = 0
+                    u.append(item)
+                mlcriteria[propname] = u
+            elif isinstance(prop, hyperdb.Link):
+                if type(value) is not _LISTTYPE:
+                    value = [value]
+                # transform keys to ids
+                u = []
+                for item in value:
+                    try:
+                        item = int(item)
+                    except (TypeError, ValueError):
+                        item = int(self.db.getclass(prop.classname).lookup(item))
+                    if item == -1:
+                        item = 0
+                    u.append(item)
+                if len(u) == 1:
+                    where[propname] = u[0]
+                else:
+                    orcriteria[propname] = u
+            elif isinstance(prop, hyperdb.String):
+                # simple glob searching
+                v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', value)
+                v = v.replace('?', '.')
+                v = v.replace('*', '.*?')
+                regexes[propname] = re.compile(v, re.I)
+            elif propname == 'id':
+                where[propname] = int(value)
+            elif isinstance(prop, hyperdb.Boolean):
+                if type(value) is _STRINGTYPE:
+                    bv = value.lower() in ('yes', 'true', 'on', '1')
+                else:
+                    bv = value
+                where[propname] = bv
+            elif isinstance(prop, hyperdb.Date):
+                t = date.Date(value).get_tuple()
+                where[propname] = int(calendar.timegm(t))
+            elif isinstance(prop, hyperdb.Interval):
+                where[propname] = str(date.Interval(value))
+            elif isinstance(prop, hyperdb.Number):
+                where[propname] = int(value)
+            else:
+                where[propname] = str(value)
+        v = self.getview()
+        #print "filter start at  %s" % time.time() 
+        if where:
+            v = v.select(where)
+        #print "filter where at  %s" % time.time() 
+
+        if mlcriteria:
+            # multilink - if any of the nodeids required by the
+            # filterspec aren't in this node's property, then skip it
+            def ff(row, ml=mlcriteria):
+                for propname, values in ml.items():
+                    sv = getattr(row, propname)
+                    for id in values:
+                        if sv.find(fid=id) == -1:
+                            return 0
+                return 1
+            iv = v.filter(ff)
+            v = v.remapwith(iv)
+
+        #print "filter mlcrit at %s" % time.time() 
+        
+        if orcriteria:
+            def ff(row, crit=orcriteria):
+                for propname, allowed in crit.items():
+                    val = getattr(row, propname)
+                    if val not in allowed:
+                        return 0
+                return 1
+            
+            iv = v.filter(ff)
+            v = v.remapwith(iv)
+        
+        #print "filter orcrit at %s" % time.time() 
+        if regexes:
+            def ff(row, r=regexes):
+                for propname, regex in r.items():
+                    val = getattr(row, propname)
+                    if not regex.search(val):
+                        return 0
+                return 1
+            
+            iv = v.filter(ff)
+            v = v.remapwith(iv)
+        #print "filter regexs at %s" % time.time() 
+        
+        if sort or group:
+            sortspec = []
+            rev = []
+            for dir, propname in group, sort:
+                if propname is None: continue
+                isreversed = 0
+                if dir == '-':
+                    isreversed = 1
+                try:
+                    prop = getattr(v, propname)
+                except AttributeError:
+                    print "MK has no property %s" % propname
+                    continue
+                propclass = self.ruprops.get(propname, None)
+                if propclass is None:
+                    propclass = self.privateprops.get(propname, None)
+                    if propclass is None:
+                        print "Schema has no property %s" % propname
+                        continue
+                if isinstance(propclass, hyperdb.Link):
+                    linkclass = self.db.getclass(propclass.classname)
+                    lv = linkclass.getview()
+                    lv = lv.rename('id', propname)
+                    v = v.join(lv, prop, 1)
+                    if linkclass.getprops().has_key('order'):
+                        propname = 'order'
+                    else:
+                        propname = linkclass.labelprop()
+                    prop = getattr(v, propname)
+                if isreversed:
+                    rev.append(prop)
+                sortspec.append(prop)
+            v = v.sortrev(sortspec, rev)[:] #XXX Metakit bug
+        #print "filter sort   at %s" % time.time() 
+            
+        rslt = []
+        for row in v:
+            id = str(row.id)
+            if search_matches is not None:
+                if search_matches.has_key(id):
+                    rslt.append(id)
+            else:
+                rslt.append(id)
+        return rslt
+    
+    def hasnode(self, nodeid):
+        return int(nodeid) < self.maxid
+    
+    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]
+
+    def stringFind(self, **requirements):
+        """Locate a particular node by matching a set of its String
+        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 isinstance(not prop, hyperdb.String):
+                raise TypeError, "'%s' not a String property"%propname
+            requirements[propname] = requirements[propname].lower()
+        requirements['_isdel'] = 0
+        
+        l = []
+        for row in self.getview().select(requirements):
+            l.append(str(row.id))
+        return l
+
+    def addjournal(self, nodeid, action, params):
+        self.db.addjournal(self.classname, nodeid, action, params)
+
+    def index(self, nodeid):
+        ''' Add (or refresh) the node to search indexes '''
+        # find all the String properties that have indexme
+        for prop, propclass in self.getprops().items():
+            if isinstance(propclass, hyperdb.String) and propclass.indexme:
+                # index them under (classname, nodeid, property)
+                self.db.indexer.add_text((self.classname, nodeid, prop),
+                                str(self.get(nodeid, prop)))
+
+    def export_list(self, propnames, nodeid):
+        ''' Export a node - generate a list of CSV-able data in the order
+            specified by propnames for the given node.
+        '''
+        properties = self.getprops()
+        l = []
+        for prop in propnames:
+            proptype = properties[prop]
+            value = self.get(nodeid, prop)
+            # "marshal" data where needed
+            if value is None:
+                pass
+            elif isinstance(proptype, hyperdb.Date):
+                value = value.get_tuple()
+            elif isinstance(proptype, hyperdb.Interval):
+                value = value.get_tuple()
+            elif isinstance(proptype, hyperdb.Password):
+                value = str(value)
+            l.append(repr(value))
+        return l
+        
+    def import_list(self, propnames, proplist):
+        ''' Import a node - all information including "id" is present and
+            should not be sanity checked. Triggers are not triggered. The
+            journal should be initialised using the "creator" and "creation"
+            information.
+
+            Return the nodeid of the node imported.
+        '''
+        if self.db.journaltag is None:
+            raise hyperdb.DatabaseError, 'Database open read-only'
+        properties = self.getprops()
+
+        d = {}
+        view = self.getview(1)
+        for i in range(len(propnames)):
+            value = eval(proplist[i])
+            propname = propnames[i]
+            prop = properties[propname]
+            if propname == 'id':
+                newid = value
+                value = int(value)
+            elif isinstance(prop, hyperdb.Date):
+                value = int(calendar.timegm(value))
+            elif isinstance(prop, hyperdb.Interval):
+                value = str(date.Interval(value))
+            d[propname] = value
+        view.append(d)
+        creator = d.get('creator', None)
+        creation = d.get('creation', None)
+        self.db.addjournal(self.classname, newid, 'create', {}, creator,
+            creation)
+        return newid
+
+    # --- used by Database
+    def _commit(self):
+        """ called post commit of the DB.
+            interested subclasses may override """
+        self.uncommitted = {}
+        self.rbactions = []
+        self.idcache = {}
+    def _rollback(self):  
+        """ called pre rollback of the DB.
+            interested subclasses may override """
+        for action in self.rbactions:
+            action()
+        self.rbactions = []
+        self.uncommitted = {}
+        self.idcache = {}
+    def _clear(self):
+        view = self.getview(1)
+        if len(view):
+            view[:] = []
+            self.db.dirty = 1
+        iv = self.getindexview(1)
+        if iv:
+            iv[:] = []
+    def rollbackaction(self, action):
+        """ call this to register a callback called on rollback
+            callback is removed on end of transaction """
+        self.rbactions.append(action)
+    # --- internal
+    def __getview(self):
+        db = self.db._db
+        view = db.view(self.classname)
+        mkprops = view.structure()
+        if mkprops and self.db.fastopen:
+            return view.ordered(1)
+        # is the definition the same?
+        for nm, rutyp in self.ruprops.items():
+            for mkprop in mkprops:
+                if mkprop.name == nm:
+                    break
+            else:
+                mkprop = None
+            if mkprop is None:
+                break
+            if _typmap[rutyp.__class__] != mkprop.type:
+                break
+        else:
+            return view.ordered(1)
+        # need to create or restructure the mk view
+        # id comes first, so MK will order it for us
+        self.db.dirty = 1
+        s = ["%s[id:I" % self.classname]
+        for nm, rutyp in self.ruprops.items():
+            mktyp = _typmap[rutyp.__class__]
+            s.append('%s:%s' % (nm, mktyp))
+            if mktyp == 'V':
+                s[-1] += ('[fid:I]')
+        s.append('_isdel:I,activity:I,creation:I,creator:I]')
+        v = self.db._db.getas(','.join(s))
+        self.db.commit()
+        return v.ordered(1)
+    def getview(self, RW=0):
+        return self.db._db.view(self.classname).ordered(1)
+    def getindexview(self, RW=0):
+        return self.db._db.view("_%s" % self.classname).ordered(1)
+    
+def _fetchML(sv):
+    l = []
+    for row in sv:
+        if row.fid:
+            l.append(str(row.fid))
+    return l
+
+def _fetchPW(s):
+    p = password.Password()
+    p.unpack(s)
+    return p
+
+def _fetchLink(n):
+    return n and str(n) or None
+
+def _fetchDate(n):
+    return date.Date(time.gmtime(n))
+
+_converters = {
+    hyperdb.Date   : _fetchDate,
+    hyperdb.Link   : _fetchLink,
+    hyperdb.Multilink : _fetchML,
+    hyperdb.Interval  : date.Interval,
+    hyperdb.Password  : _fetchPW,
+    hyperdb.Boolean   : lambda n: n,
+    hyperdb.Number    : lambda n: n,
+    hyperdb.String    : str,
+}                
+
+class FileName(hyperdb.String):
+    isfilename = 1            
+
+_typmap = {
+    FileName : 'S',
+    hyperdb.String : 'S',
+    hyperdb.Date   : 'I',
+    hyperdb.Link   : 'I',
+    hyperdb.Multilink : 'V',
+    hyperdb.Interval  : 'S',
+    hyperdb.Password  : 'S',
+    hyperdb.Boolean   : 'I',
+    hyperdb.Number    : 'I',
+}
+class FileClass(Class):
+    ''' like Class but with a content property
+    '''
+    default_mime_type = 'text/plain'
+    def __init__(self, db, classname, **properties):
+        properties['content'] = FileName()
+        if not properties.has_key('type'):
+            properties['type'] = hyperdb.String()
+        Class.__init__(self, db, classname, **properties)
+
+    def get(self, nodeid, propname, default=_marker, cache=1):
+        x = Class.get(self, nodeid, propname, default, cache)
+        if propname == 'content':
+            if x.startswith('file:'):
+                fnm = x[5:]
+                try:
+                    x = open(fnm, 'rb').read()
+                except Exception, e:
+                    x = repr(e)
+        return x
+
+    def create(self, **propvalues):
+        content = propvalues['content']
+        del propvalues['content']
+        newid = Class.create(self, **propvalues)
+        if not content:
+            return newid
+        nm = bnm = '%s%s' % (self.classname, newid)
+        sd = str(int(int(newid) / 1000))
+        d = os.path.join(self.db.config.DATABASE, 'files', self.classname, sd)
+        if not os.path.exists(d):
+            os.makedirs(d)
+        nm = os.path.join(d, nm)
+        open(nm, 'wb').write(content)
+        self.set(newid, content = 'file:'+nm)
+        mimetype = propvalues.get('type', self.default_mime_type)
+        self.db.indexer.add_text((self.classname, newid, 'content'), content,
+            mimetype)
+        def undo(fnm=nm, action1=os.remove, indexer=self.db.indexer):
+            action1(fnm)
+        self.rollbackaction(undo)
+        return newid
+
+    def index(self, nodeid):
+        Class.index(self, nodeid)
+        mimetype = self.get(nodeid, 'type')
+        if not mimetype:
+            mimetype = self.default_mime_type
+        self.db.indexer.add_text((self.classname, nodeid, 'content'),
+                    self.get(nodeid, 'content'), mimetype)
+ 
+class IssueClass(Class, roundupdb.IssueClass):
+    ''' The newly-created class automatically includes the "messages",
+        "files", "nosy", and "superseder" properties.  If the 'properties'
+        dictionary attempts to specify any of these properties or a
+        "creation" or "activity" property, a ValueError is raised.
+    '''
+    def __init__(self, db, classname, **properties):
+        if not properties.has_key('title'):
+            properties['title'] = hyperdb.String(indexme='yes')
+        if not properties.has_key('messages'):
+            properties['messages'] = hyperdb.Multilink("msg")
+        if not properties.has_key('files'):
+            properties['files'] = hyperdb.Multilink("file")
+        if not properties.has_key('nosy'):
+            # note: journalling is turned off as it really just wastes
+            # space. this behaviour may be overridden in an instance
+            properties['nosy'] = hyperdb.Multilink("user", do_journal="no")
+        if not properties.has_key('superseder'):
+            properties['superseder'] = hyperdb.Multilink(classname)
+        Class.__init__(self, db, classname, **properties)
+        
+CURVERSION = 2
+
+class Indexer(indexer.Indexer):
+    disallows = {'THE':1, 'THIS':1, 'ZZZ':1, 'THAT':1, 'WITH':1}
+    def __init__(self, path, datadb):
+        self.path = os.path.join(path, 'index.mk4')
+        self.db = metakit.storage(self.path, 1)
+        self.datadb = datadb
+        self.reindex = 0
+        v = self.db.view('version')
+        if not v.structure():
+            v = self.db.getas('version[vers:I]')
+            self.db.commit()
+            v.append(vers=CURVERSION)
+            self.reindex = 1
+        elif v[0].vers != CURVERSION:
+            v[0].vers = CURVERSION
+            self.reindex = 1
+        if self.reindex:
+            self.db.getas('ids[tblid:I,nodeid:I,propid:I,ignore:I]')
+            self.db.getas('index[word:S,hits[pos:I]]')
+            self.db.commit()
+            self.reindex = 1
+        self.changed = 0
+        self.propcache = {}
+
+    def force_reindex(self):
+        v = self.db.view('ids')
+        v[:] = []
+        v = self.db.view('index')
+        v[:] = []
+        self.db.commit()
+        self.reindex = 1
+
+    def should_reindex(self):
+        return self.reindex
+
+    def _getprops(self, classname):
+        props = self.propcache.get(classname, None)
+        if props is None:
+            props = self.datadb.view(classname).structure()
+            props = [prop.name for prop in props]
+            self.propcache[classname] = props
+        return props
+
+    def _getpropid(self, classname, propname):
+        return self._getprops(classname).index(propname)
+
+    def _getpropname(self, classname, propid):
+        return self._getprops(classname)[propid]
+
+    def add_text(self, identifier, text, mime_type='text/plain'):
+        if mime_type != 'text/plain':
+            return
+        classname, nodeid, property = identifier
+        tbls = self.datadb.view('tables')
+        tblid = tbls.find(name=classname)
+        if tblid < 0:
+            raise KeyError, "unknown class %r"%classname
+        nodeid = int(nodeid)
+        propid = self._getpropid(classname, property)
+        ids = self.db.view('ids')
+        oldpos = ids.find(tblid=tblid,nodeid=nodeid,propid=propid,ignore=0)
+        if oldpos > -1:
+            ids[oldpos].ignore = 1
+            self.changed = 1
+        pos = ids.append(tblid=tblid,nodeid=nodeid,propid=propid)
+        
+        wordlist = re.findall(r'\b\w{2,25}\b', text.upper())
+        words = {}
+        for word in wordlist:
+	    if not self.disallows.has_key(word):
+            	words[word] = 1
+        words = words.keys()
+        
+        index = self.db.view('index').ordered(1)
+        for word in words:
+            ndx = index.find(word=word)
+            if ndx < 0:
+                index.append(word=word)
+                ndx = index.find(word=word)
+            index[ndx].hits.append(pos=pos)
+            self.changed = 1
+
+    def find(self, wordlist):
+        hits = None
+        index = self.db.view('index').ordered(1)
+        for word in wordlist:
+            word = word.upper()
+            if not 2 < len(word) < 26:
+                continue
+            ndx = index.find(word=word)
+            if ndx < 0:
+                return {}
+            if hits is None:
+                hits = index[ndx].hits
+            else:
+                hits = hits.intersect(index[ndx].hits)
+            if len(hits) == 0:
+                return {}
+        if hits is None:
+            return {}
+        rslt = {}
+        ids = self.db.view('ids').remapwith(hits)
+        tbls = self.datadb.view('tables')
+        for i in range(len(ids)):
+            hit = ids[i]
+            if not hit.ignore:
+                classname = tbls[hit.tblid].name
+                nodeid = str(hit.nodeid)
+                property = self._getpropname(classname, hit.propid)
+                rslt[i] = (classname, nodeid, property)
+        return rslt
+
+    def save_index(self):
+        if self.changed:
+            self.db.commit()
+        self.changed = 0
+
+    def rollback(self):
+        if self.changed:
+            self.db.rollback()
+            self.db = metakit.storage(self.path, 1)
+        self.changed = 0
+

Roundup Issue Tracker: http://roundup-tracker.org/