Code

hyperdb grows a refresh_database() method. There will be a future
[roundup.git] / roundup / backends / back_metakit.py
index 4d1fd4c4785490bff9a2aa0ffa208c56bffddbe8..737d08d3faa6883cfae26f3d8c1af95c8aa668a7 100755 (executable)
@@ -1,3 +1,4 @@
+# $Id: back_metakit.py,v 1.51 2003-10-07 11:58:57 anthonybaxter Exp $
 '''
    Metakit backend for Roundup, originally by Gordon McMillan.
 
@@ -16,7 +17,7 @@
       Interval  ''    convert to None
       Number    0     ambiguious :( - do nothing
       Boolean   0     ambiguious :( - do nothing
-      Link      ''    convert to None
+      Link          convert to None
       Multilink []    actually, mk can handle this one ;)
       Passowrd  ''    convert to None
       ========= ===== ====================================================
 '''
 from roundup import hyperdb, date, password, roundupdb, security
 import metakit
-from sessions import Sessions
+from sessions import Sessions, OneTimeKeys
 import re, marshal, os, sys, weakref, time, calendar
 from roundup import indexer
 import locking
+from roundup.date import Range
 
 _dbs = {}
 
 def Database(config, journaltag=None):
+    ''' Only have a single instance of the Database class for each instance
+    '''
     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):
+class _Database(hyperdb.Database, roundupdb.Database):
     def __init__(self, config, journaltag=None):
         self.config = config
         self.journaltag = journaltag
@@ -60,6 +60,7 @@ class _Database(hyperdb.Database):
         self._db = self.__open()
         self.indexer = Indexer(self.config.DATABASE, self._db)
         self.sessions = Sessions(self.config)
+        self.otks = OneTimeKeys(self.config)
         self.security = security.Security(self)
 
         os.umask(0002)
@@ -68,6 +69,10 @@ class _Database(hyperdb.Database):
         if self.indexer.should_reindex():
             self.reindex()
 
+    def refresh_database(self):
+        # XXX handle refresh
+        self.reindex()
+
     def reindex(self):
         for klass in self.classes.values():
             for nodeid in klass.list():
@@ -76,20 +81,9 @@ class _Database(hyperdb.Database):
 
     # --- 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':
+        if classname == 'transactions':
             return self.dirty
+        # fall back on the classes
         return self.getclass(classname)
     def getclass(self, classname):
         try:
@@ -99,6 +93,7 @@ class _Database(hyperdb.Database):
     def getclasses(self):
         return self.classes.keys()
     # --- end of ping's spec 
+
     # --- exposed methods
     def commit(self):
         if self.dirty:
@@ -145,7 +140,7 @@ class _Database(hyperdb.Database):
         if tblid == -1:
             tblid = self.tables.append(name=tablenm)
         if creator is None:
-            creator = self.curuserid
+            creator = self.getuid()
         else:
             try:
                 creator = int(creator)
@@ -181,9 +176,10 @@ class _Database(hyperdb.Database):
             #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))
+            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)
@@ -214,13 +210,22 @@ class _Database(hyperdb.Database):
 
     # --- internal
     def __open(self):
+        ''' Open the metakit database
+        '''
+        # make the database dir if it doesn't exist
         if not os.path.exists(self.config.DATABASE):
             os.makedirs(self.config.DATABASE)
+
+        # figure the file names
         self.dbnm = db = os.path.join(self.config.DATABASE, 'tracker.mk4')
         lockfilenm = db[:-3]+'lck'
+
+        # get the database lock
         self.lockfile = locking.acquire_lock(lockfilenm)
         self.lockfile.write(str(os.getpid()))
         self.lockfile.flush()
+
+        # see if the schema has changed since last db access
         self.fastopen = 0
         if os.path.exists(db):
             dbtm = os.path.getmtime(db)
@@ -235,27 +240,38 @@ class _Database(hyperdb.Database):
                 else:
                      # can't find schemamod - must be frozen
                     self.fastopen = 1
+
+        # open the db
         db = metakit.storage(db, 1)
         hist = db.view('history')
         tables = db.view('tables')
         if not self.fastopen:
+            # create the database if it's brand new
             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()
+
+        # we now have an open, initialised database
         self.tables = tables
         self.hist = hist
         return db
+
+    def setid(self, classname, maxid):
+        ''' No-op in metakit
+        '''
+        pass
         
 _STRINGTYPE = type('')
 _LISTTYPE = type([])
-_CREATE, _SET, _RETIRE, _LINK, _UNLINK = range(5)
+_CREATE, _SET, _RETIRE, _LINK, _UNLINK, _RESTORE = range(6)
 
 _actionnames = {
     _CREATE : 'create',
     _SET : 'set',
     _RETIRE : 'retire',
+    _RESTORE : 'restore',
     _LINK : 'link',
     _UNLINK : 'unlink',
 }
@@ -278,8 +294,8 @@ class Class:
                               'creator'  : hyperdb.Link('user') }
 
         # event -> list of callables
-        self.auditors = {'create': [], 'set': [], 'retire': []}
-        self.reactors = {'create': [], 'set': [], 'retire': []}
+        self.auditors = {'create': [], 'set': [], 'retire': [], 'restore': []}
+        self.reactors = {'create': [], 'set': [], 'retire': [], 'restore': []}
 
         view = self.__getview()
         self.maxid = 1
@@ -325,6 +341,11 @@ class Class:
     # --- the hyperdb.Class methods
     def create(self, **propvalues):
         self.fireAuditors('create', None, propvalues)
+        newid = self.create_inner(**propvalues)
+        # self.set() (called in self.create_inner()) does reactors)
+        return newid
+
+    def create_inner(self, **propvalues):
         rowdict = {}
         rowdict['id'] = newid = self.maxid
         self.maxid += 1
@@ -338,8 +359,9 @@ class Class:
         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"
+        '''
+            'cache' exists for backwards compatibility, and is not used.
+        '''
 
         view = self.getview()        
         id = int(nodeid)
@@ -568,17 +590,18 @@ class Class:
                 if value is None:
                     setattr(row, key, '')
                 else:
-                    setattr(row, key, str(value))
+                    # kedder: we should store interval values serialized
+                    setattr(row, key, value.serialise())
                 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))
+                    raise TypeError, "%s (%s) is not numeric"%(key, repr(value))
                 setattr(row, key, v)
                 changes[key] = oldvalue
                 propvalues[key] = value
@@ -587,7 +610,7 @@ class Class:
                 if value is None:
                     bv = 0
                 elif value not in (0,1):
-                    raise TypeError, "%s (%s) is not boolean" % (key, repr(value))
+                    raise TypeError, "%s (%s) is not boolean"%(key, repr(value))
                 else:
                     bv = value 
                 setattr(row, key, bv)
@@ -605,8 +628,8 @@ class Class:
             if not row.creation:
                 row.creation = int(time.time())
             if not row.creator:
-                row.creator = self.db.curuserid
-            
+                row.creator = self.db.getuid()
+
         self.db.dirty = 1
         if self.do_journal:
             if isnew:
@@ -626,10 +649,12 @@ class Class:
         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:
@@ -640,6 +665,56 @@ class Class:
         self.db.dirty = 1
         self.fireReactors('retire', nodeid, None)
 
+    def restore(self, nodeid):
+        '''Restpre a retired node.
+
+        Make node available for all operations like it was before retirement.
+        '''
+        if self.db.journaltag is None:
+            raise hyperdb.DatabaseError, 'Database open read-only'
+
+        # check if key property was overrided
+        key = self.getkey()
+        keyvalue = self.get(nodeid, key)
+        try:
+            id = self.lookup(keyvalue)
+        except KeyError:
+            pass
+        else:
+            raise KeyError, "Key property (%s) of retired node clashes with \
+                existing one (%s)" % (key, keyvalue)
+        # Now we can safely restore node
+        self.fireAuditors('restore', 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 = 0
+
+        if self.do_journal:
+            self.db.addjournal(self.classname, nodeid, _RESTORE, {})
+        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('restore', nodeid, None)
+
+    def is_retired(self, nodeid):
+        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]
+        return row._isdel
+
     def history(self, nodeid):
         if not self.do_journal:
             raise ValueError, 'Journalling is disabled for this class'
@@ -734,6 +809,8 @@ class Class:
         for propname, ids in propspec:
             if type(ids) is _STRINGTYPE:
                 ids = {int(ids):1}
+            elif ids is None:
+                ids = {0:1}
             else:
                 d = {}
                 for id in ids.keys():
@@ -774,6 +851,12 @@ class Class:
             l.append(str(row.id))
         return l
 
+    def getnodeids(self):
+        l = []
+        for row in self.getview():
+            l.append(str(row.id))
+        return l
+
     def count(self):
         return len(self.getview())
 
@@ -790,6 +873,7 @@ class Class:
                 raise ValueError, "%s is already a property of %s"%(key,
                     self.classname)
         self.ruprops.update(properties)
+        # Class structure has changed
         self.db.fastopen = 0
         view = self.__getview()
         self.db.commit()
@@ -801,7 +885,11 @@ class Class:
         # 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
+
+        timezone = self.db.getUserTimezone()
+
         where = {'_isdel':0}
+        wherehigh = {}
         mlcriteria = {}
         regexes = {}
         orcriteria = {}
@@ -810,7 +898,9 @@ class Class:
             if prop is None:
                 prop = self.privateprops[propname]
             if isinstance(prop, hyperdb.Multilink):
-                if type(value) is not _LISTTYPE:
+                if value in ('-1', ['-1']):
+                    value = []
+                elif type(value) is not _LISTTYPE:
                     value = [value]
                 # transform keys to ids
                 u = []
@@ -841,11 +931,16 @@ class Class:
                 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)
+                if type(value) is not type([]):
+                    value = [value]
+                m = []
+                for v in value:
+                    # simple glob searching
+                    v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
+                    v = v.replace('?', '.')
+                    v = v.replace('*', '.*?')
+                    m.append(v)
+                regexes[propname] = re.compile('(%s)'%('|'.join(m)), re.I)
             elif propname == 'id':
                 where[propname] = int(value)
             elif isinstance(prop, hyperdb.Boolean):
@@ -855,10 +950,43 @@ class Class:
                     bv = value
                 where[propname] = bv
             elif isinstance(prop, hyperdb.Date):
-                t = date.Date(value).get_tuple()
-                where[propname] = int(calendar.timegm(t))
+                try:
+                    # Try to filter on range of dates
+                    date_rng = Range(value, date.Date, offset=timezone)
+                    if date_rng.from_value:
+                        t = date_rng.from_value.get_tuple()
+                        where[propname] = int(calendar.timegm(t))
+                    else:
+                        # use minimum possible value to exclude items without
+                        # 'prop' property
+                        where[propname] = 0
+                    if date_rng.to_value:
+                        t = date_rng.to_value.get_tuple()
+                        wherehigh[propname] = int(calendar.timegm(t))
+                    else:
+                        wherehigh[propname] = None
+                except ValueError:
+                    # If range creation fails - ignore that search parameter
+                    pass                        
             elif isinstance(prop, hyperdb.Interval):
-                where[propname] = str(date.Interval(value))
+                try:
+                    # Try to filter on range of intervals
+                    date_rng = Range(value, date.Interval)
+                    if date_rng.from_value:
+                        #t = date_rng.from_value.get_tuple()
+                        where[propname] = date_rng.from_value.serialise()
+                    else:
+                        # use minimum possible value to exclude items without
+                        # 'prop' property
+                        where[propname] = '-99999999999999'
+                    if date_rng.to_value:
+                        #t = date_rng.to_value.get_tuple()
+                        wherehigh[propname] = date_rng.to_value.serialise()
+                    else:
+                        wherehigh[propname] = None
+                except ValueError:
+                    # If range creation fails - ignore that search parameter
+                    pass                        
             elif isinstance(prop, hyperdb.Number):
                 where[propname] = int(value)
             else:
@@ -866,7 +994,9 @@ class Class:
         v = self.getview()
         #print "filter start at  %s" % time.time() 
         if where:
-            v = v.select(where)
+            where_higherbound = where.copy()
+            where_higherbound.update(wherehigh)
+            v = v.select(where, where_higherbound)
         #print "filter where at  %s" % time.time() 
 
         if mlcriteria:
@@ -875,6 +1005,8 @@ class Class:
             def ff(row, ml=mlcriteria):
                 for propname, values in ml.items():
                     sv = getattr(row, propname)
+                    if not values and sv:
+                        return 0
                     for id in values:
                         if sv.find(fid=id) == -1:
                             return 0
@@ -1031,6 +1163,10 @@ class Class:
             elif isinstance(proptype, hyperdb.Password):
                 value = str(value)
             l.append(repr(value))
+
+        # append retired flag
+        l.append(self.is_retired(nodeid))
+
         return l
         
     def import_list(self, propnames, proplist):
@@ -1049,20 +1185,64 @@ class Class:
         view = self.getview(1)
         for i in range(len(propnames)):
             value = eval(proplist[i])
+            if not value:
+                continue
+
             propname = propnames[i]
-            prop = properties[propname]
             if propname == 'id':
-                newid = value
-                value = int(value)
-            elif isinstance(prop, hyperdb.Date):
+                newid = value = int(value)
+            elif propname == 'is retired':
+                # is the item retired?
+                if int(value):
+                    d['_isdel'] = 1
+                continue
+            elif value is None:
+                d[propname] = None
+                continue
+
+            prop = properties[propname]
+            if isinstance(prop, hyperdb.Date):
                 value = int(calendar.timegm(value))
             elif isinstance(prop, hyperdb.Interval):
-                value = str(date.Interval(value))
+                value = date.Interval(value).serialise()
+            elif isinstance(prop, hyperdb.Number):
+                value = int(value)
+            elif isinstance(prop, hyperdb.Boolean):
+                value = int(value)
+            elif isinstance(prop, hyperdb.Link) and value:
+                value = int(value)
+            elif isinstance(prop, hyperdb.Multilink):
+                # we handle multilinks separately
+                continue
             d[propname] = value
+
+        # possibly make a new node
+        if not d.has_key('id'):
+            d['id'] = newid = self.maxid
+            self.maxid += 1
+
+        # save off the node
         view.append(d)
-        creator = d.get('creator', None)
-        creation = d.get('creation', None)
-        self.db.addjournal(self.classname, newid, 'create', {}, creator,
+
+        # fix up multilinks
+        ndx = view.find(id=newid)
+        row = view[ndx]
+        for i in range(len(propnames)):
+            value = eval(proplist[i])
+            propname = propnames[i]
+            if propname == 'is retired':
+                continue
+            prop = properties[propname]
+            if not isinstance(prop, hyperdb.Multilink):
+                continue
+            sv = getattr(row, propname)
+            for entry in value:
+                sv.append(int(entry))
+
+        self.db.dirty = 1
+        creator = d.get('creator', 0)
+        creation = d.get('creation', 0)
+        self.db.addjournal(self.classname, str(newid), _CREATE, {}, creator,
             creation)
         return newid
 
@@ -1095,11 +1275,20 @@ class Class:
         self.rbactions.append(action)
     # --- internal
     def __getview(self):
+        ''' Find the interface for a specific Class in the hyperdb.
+
+            This method checks to see whether the schema has changed and
+            re-works the underlying metakit structure if it has.
+        '''
         db = self.db._db
         view = db.view(self.classname)
         mkprops = view.structure()
+
+        # if we have structure in the database, and the structure hasn't
+        # changed
         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:
@@ -1130,7 +1319,7 @@ class Class:
         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:
@@ -1149,7 +1338,7 @@ def _fetchPW(s):
     return p
 
 def _fetchLink(n):
-    ''' Return None if the string is empty ?otherwise ensure it's a string?
+    ''' Return None if the link is 0 - otherwise strify it.
     '''
     return n and str(n) or None
 
@@ -1194,7 +1383,7 @@ _typmap = {
     hyperdb.Boolean   : 'I',
     hyperdb.Number    : 'I',
 }
-class FileClass(Class):
+class FileClass(Class, hyperdb.FileClass):
     ''' like Class but with a content property
     '''
     default_mime_type = 'text/plain'
@@ -1205,20 +1394,24 @@ class FileClass(Class):
         Class.__init__(self, db, classname, **properties)
 
     def get(self, nodeid, propname, default=_marker, cache=1):
-        x = Class.get(self, nodeid, propname, default, cache)
+        x = Class.get(self, nodeid, propname, default)
+        poss_msg = 'Possibly an access right configuration problem.'
         if propname == 'content':
             if x.startswith('file:'):
                 fnm = x[5:]
                 try:
                     x = open(fnm, 'rb').read()
-                except Exception, e:
-                    x = repr(e)
+                except IOError, (strerror):
+                    # XXX by catching this we donot see an error in the log.
+                    return 'ERROR reading file: %s%s\n%s\n%s'%(
+                            self.classname, nodeid, poss_msg, strerror)
         return x
 
     def create(self, **propvalues):
+        self.fireAuditors('create', None, propvalues)
         content = propvalues['content']
         del propvalues['content']
-        newid = Class.create(self, **propvalues)
+        newid = Class.create_inner(self, **propvalues)
         if not content:
             return newid
         nm = bnm = '%s%s' % (self.classname, newid)