Code

. all storage-specific code (ie. backend) is now implemented by the backends
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 14 Jul 2002 02:05:54 +0000 (02:05 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Sun, 14 Jul 2002 02:05:54 +0000 (02:05 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@870 57a73879-2fb5-44c3-a270-3262357dd7e2

13 files changed:
CHANGES.txt
doc/upgrading.txt
roundup/backends/back_anydbm.py
roundup/backends/back_bsddb.py
roundup/backends/back_bsddb3.py
roundup/hyperdb.py
roundup/init.py
roundup/roundupdb.py
roundup/templates/classic/dbinit.py
roundup/templates/extended/dbinit.py
test/test_db.py
test/test_init.py
test/test_schema.py

index e751d803ba44d5463c43dece329cc9a1778b6e1d..3558ca1e69a4f04632bb8e2507a185d0e07eff6e 100644 (file)
@@ -37,7 +37,7 @@ Feature:
  . added sorting of checklist HTML display
  . switched to using a session-based web login
  . made mailgw handle set and modify operations on multilinks (bug #579094)
-
+ . all storage-specific code (ie. backend) is now implemented by the backends
 
 2002-06-24 0.4.2
 Fixed:
index 2be9d303103e3bd74a0d904963a8299e84a086fe..e768d857f384a2d79b8460ee9e761722451391b3 100644 (file)
@@ -16,6 +16,8 @@ Migrating from 0.4.x to 0.5.0
 TODO: mention stuff about indexing
 TODO: mention that the dbinit needs the db.post_init() method call for
 reindexing
+TODO: dbinit now imports classes from selct_db
+TODO: select_db needs fixing to include Class, FileClass and IssueClass
 
 
 Migrating from 0.4.1 to 0.4.2
index 4e1ee10f0642763492c2fb62f801d2fd948f0f79..0acff4ac8fa5999b13dbbe8ecf878a5ae09fc68f 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_anydbm.py,v 1.43 2002-07-10 06:30:30 richard Exp $
+#$Id: back_anydbm.py,v 1.44 2002-07-14 02:05:53 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
@@ -23,16 +23,18 @@ versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
 serious bugs, and is not available)
 '''
 
-import whichdb, anydbm, os, marshal
-from roundup import hyperdb, date
+import whichdb, anydbm, os, marshal, re, weakref, string, copy
+from roundup import hyperdb, date, password, roundupdb
 from blobfiles import FileStorage
 from roundup.indexer import Indexer
 from locking import acquire_lock, release_lock
+from roundup.hyperdb import String, Password, Date, Interval, Link, \
+    Multilink, DatabaseError
 
 #
 # Now the database
 #
-class Database(FileStorage, hyperdb.Database):
+class Database(FileStorage, hyperdb.Database, roundupdb.Database):
     """A database for storing records containing flexible data types.
 
     Transaction stuff TODO:
@@ -268,6 +270,63 @@ class Database(FileStorage, hyperdb.Database):
 
         return res
 
+    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):
+                d[k] = v
+                continue
+
+            # get the property spec
+            prop = properties[k]
+
+            if isinstance(prop, Password):
+                d[k] = str(v)
+            elif isinstance(prop, Date) and v is not None:
+                d[k] = v.get_tuple()
+            elif isinstance(prop, Interval) and v is not None:
+                d[k] = v.get_tuple()
+            else:
+                d[k] = v
+        return d
+
+    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():
+            # 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):
+                d[k] = v
+                continue
+
+            # get the property spec
+            prop = properties[k]
+
+            if isinstance(prop, Date) and v is not None:
+                d[k] = date.Date(v)
+            elif isinstance(prop, Interval) and v is not None:
+                d[k] = date.Interval(v)
+            elif isinstance(prop, Password):
+                p = password.Password()
+                p.unpack(v)
+                d[k] = p
+            else:
+                d[k] = v
+        return d
+
     def hasnode(self, classname, nodeid, db=None):
         ''' determine if the database has a given node
         '''
@@ -509,8 +568,1037 @@ class Database(FileStorage, hyperdb.Database):
         self.newnodes = {}
         self.transactions = []
 
+_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.
+        """
+        if (properties.has_key('creation') or properties.has_key('activity')
+                or properties.has_key('creator')):
+            raise ValueError, '"creation", "activity" and "creator" are '\
+                'reserved'
+
+        self.classname = classname
+        self.properties = properties
+        self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
+        self.key = ''
+
+        # do the db-related init stuff
+        db.addclass(self)
+
+        self.auditors = {'create': [], 'set': [], 'retire': []}
+        self.reactors = {'create': [], 'set': [], 'retire': []}
+
+    def __repr__(self):
+        '''Slightly more useful representation
+        '''
+        return '<hypderdb.Class "%s">'%self.classname
+
+    # Editing nodes:
+
+    def create(self, **propvalues):
+        """Create a new node of this class and return its id.
+
+        The keyword arguments in 'propvalues' map property names to values.
+
+        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.
+
+        These operations trigger detectors and can be vetoed.  Attempts
+        to modify the "creation" or "activity" properties cause a KeyError.
+        """
+        if propvalues.has_key('id'):
+            raise KeyError, '"id" is reserved'
+
+        if self.db.journaltag is None:
+            raise DatabaseError, 'Database open read-only'
+
+        if propvalues.has_key('creation') or propvalues.has_key('activity'):
+            raise KeyError, '"creation" and "activity" are reserved'
+
+        self.fireAuditors('create', None, propvalues)
+
+        # new node's id
+        newid = self.db.newid(self.classname)
+
+        # validate propvalues
+        num_re = re.compile('^\d+$')
+        for key, value in propvalues.items():
+            if key == self.key:
+                try:
+                    self.lookup(value)
+                except KeyError:
+                    pass
+                else:
+                    raise ValueError, 'node with key "%s" exists'%value
+
+            # try to handle this property
+            try:
+                prop = self.properties[key]
+            except KeyError:
+                raise KeyError, '"%s" has no property "%s"'%(self.classname,
+                    key)
+
+            if isinstance(prop, Link):
+                if type(value) != type(''):
+                    raise ValueError, 'link value must be String'
+                link_class = self.properties[key].classname
+                # if it isn't a number, it's a key
+                if not num_re.match(value):
+                    try:
+                        value = self.db.classes[link_class].lookup(value)
+                    except (TypeError, KeyError):
+                        raise IndexError, 'new property "%s": %s not a %s'%(
+                            key, value, link_class)
+                elif not self.db.hasnode(link_class, value):
+                    raise IndexError, '%s has no node %s'%(link_class, value)
+
+                # save off the value
+                propvalues[key] = value
+
+                # register the link with the newly linked node
+                if self.properties[key].do_journal:
+                    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
+
+                # clean up and validate the list of links
+                link_class = self.properties[key].classname
+                l = []
+                for entry in value:
+                    if type(entry) != type(''):
+                        raise ValueError, '"%s" link value (%s) must be '\
+                            'String'%(key, value)
+                    # if it isn't a number, it's a key
+                    if not num_re.match(entry):
+                        try:
+                            entry = self.db.classes[link_class].lookup(entry)
+                        except (TypeError, KeyError):
+                            raise IndexError, 'new property "%s": %s not a %s'%(
+                                key, entry, self.properties[key].classname)
+                    l.append(entry)
+                value = l
+                propvalues[key] = value
+
+                # handle additions
+                for id in value:
+                    if not self.db.hasnode(link_class, id):
+                        raise IndexError, '%s has no node %s'%(link_class, id)
+                    # register the link with the newly linked node
+                    if self.properties[key].do_journal:
+                        self.db.addjournal(link_class, id, 'link',
+                            (self.classname, newid, key))
+
+            elif isinstance(prop, String):
+                if type(value) != type(''):
+                    raise TypeError, 'new property "%s" not a string'%key
+
+            elif isinstance(prop, Password):
+                if not isinstance(value, password.Password):
+                    raise TypeError, 'new property "%s" not a Password'%key
+
+            elif isinstance(prop, 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):
+                if value is not None and not isinstance(value, date.Interval):
+                    raise TypeError, 'new property "%s" not an Interval'%key
+
+        # make sure there's data where there needs to be
+        for key, prop in self.properties.items():
+            if propvalues.has_key(key):
+                continue
+            if key == self.key:
+                raise ValueError, 'key property "%s" is required'%key
+            if isinstance(prop, Multilink):
+                propvalues[key] = []
+            else:
+                # TODO: None isn't right here, I think...
+                propvalues[key] = None
+
+        # done
+        self.db.addnode(self.classname, newid, propvalues)
+        self.db.addjournal(self.classname, newid, 'create', propvalues)
+
+        self.fireReactors('create', newid, None)
+
+        return newid
+
+    def get(self, nodeid, propname, default=_marker, cache=1):
+        """Get the value of 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.  'propname' must be the name of a property
+        of this class or a KeyError is raised.
+
+        'cache' indicates whether the transaction cache should be queried
+        for the node. If the node has been modified and you need to
+        determine what its values prior to modification are, you need to
+        set cache=0.
+
+        Attempts to get the "creation" or "activity" properties should
+        do the right thing.
+        """
+        if propname == 'id':
+            return nodeid
+
+        if propname == 'creation':
+            journal = self.db.getjournal(self.classname, nodeid)
+            if journal:
+                return self.db.getjournal(self.classname, nodeid)[0][1]
+            else:
+                # on the strange chance that there's no journal
+                return date.Date()
+        if propname == 'activity':
+            journal = self.db.getjournal(self.classname, nodeid)
+            if journal:
+                return self.db.getjournal(self.classname, nodeid)[-1][1]
+            else:
+                # on the strange chance that there's no journal
+                return date.Date()
+        if propname == 'creator':
+            journal = self.db.getjournal(self.classname, nodeid)
+            if journal:
+                name = self.db.getjournal(self.classname, nodeid)[0][2]
+            else:
+                return None
+            return self.db.user.lookup(name)
+
+        # get the property (raises KeyErorr if invalid)
+        prop = self.properties[propname]
+
+        # get the node's dict
+        d = self.db.getnode(self.classname, nodeid, cache=cache)
+
+        if not d.has_key(propname):
+            if default is _marker:
+                if isinstance(prop, Multilink):
+                    return []
+                else:
+                    # TODO: None isn't right here, I think...
+                    return None
+            else:
+                return default
+
+        return d[propname]
+
+    # XXX not in spec
+    def getnode(self, nodeid, cache=1):
+        ''' Return a convenience wrapper for the node.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        'cache' indicates whether the transaction cache should be queried
+        for the node. If the node has been modified and you need to
+        determine what its values prior to modification are, you need to
+        set cache=0.
+        '''
+        return Node(self, nodeid, cache=cache)
+
+    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.
+
+        Each key in 'propvalues' must be the name of a property of this
+        class or a KeyError is raised.
+
+        All values in 'propvalues' must be acceptable types for their
+        corresponding properties or a TypeError is raised.
+
+        If the value of the key property is set, it must not collide with
+        other key strings or a ValueError is raised.
+
+        If the value of a Link or Multilink property contains an invalid
+        node id, a ValueError is raised.
+
+        These operations trigger detectors and can be vetoed.  Attempts
+        to modify the "creation" or "activity" properties cause a KeyError.
+        """
+        if not propvalues:
+            return
+
+        if propvalues.has_key('creation') or propvalues.has_key('activity'):
+            raise KeyError, '"creation" and "activity" are reserved'
+
+        if propvalues.has_key('id'):
+            raise KeyError, '"id" is reserved'
+
+        if self.db.journaltag is None:
+            raise DatabaseError, 'Database open read-only'
+
+        self.fireAuditors('set', nodeid, propvalues)
+        # Take a copy of the node dict so that the subsequent set
+        # operation doesn't modify the oldvalues structure.
+        try:
+            # try not using the cache initially
+            oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
+                cache=0))
+        except IndexError:
+            # this will be needed if somone does a create() and set()
+            # with no intervening commit()
+            oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
+
+        node = self.db.getnode(self.classname, nodeid)
+        if node.has_key(self.db.RETIRED_FLAG):
+            raise IndexError
+        num_re = re.compile('^\d+$')
+        for key, value in propvalues.items():
+            # check to make sure we're not duplicating an existing key
+            if key == self.key and node[key] != value:
+                try:
+                    self.lookup(value)
+                except KeyError:
+                    pass
+                else:
+                    raise ValueError, 'node with key "%s" exists'%value
+
+            # this will raise the KeyError if the property isn't valid
+            # ... we don't use getprops() here because we only care about
+            # the writeable properties.
+            prop = self.properties[key]
+
+            # if the value's the same as the existing value, no sense in
+            # doing anything
+            if node.has_key(key) and value == node[key]:
+                del propvalues[key]
+                continue
+
+            # do stuff based on the prop type
+            if isinstance(prop, Link):
+                link_class = self.properties[key].classname
+                # if it isn't a number, it's a key
+                if type(value) != type(''):
+                    raise ValueError, 'link value must be String'
+                if not num_re.match(value):
+                    try:
+                        value = self.db.classes[link_class].lookup(value)
+                    except (TypeError, KeyError):
+                        raise IndexError, 'new property "%s": %s not a %s'%(
+                            key, value, self.properties[key].classname)
+
+                if not self.db.hasnode(link_class, value):
+                    raise IndexError, '%s has no node %s'%(link_class, value)
+
+                if self.properties[key].do_journal:
+                    # register the unlink with the old linked node
+                    if node[key] is not None:
+                        self.db.addjournal(link_class, node[key], 'unlink',
+                            (self.classname, nodeid, key))
+
+                    # register the link with the newly linked node
+                    if value is not None:
+                        self.db.addjournal(link_class, value, 'link',
+                            (self.classname, nodeid, key))
+
+            elif isinstance(prop, Multilink):
+                if type(value) != type([]):
+                    raise TypeError, 'new property "%s" not a list of ids'%key
+                link_class = self.properties[key].classname
+                l = []
+                for entry in value:
+                    # if it isn't a number, it's a key
+                    if type(entry) != type(''):
+                        raise ValueError, 'new property "%s" link value ' \
+                            'must be a string'%key
+                    if not num_re.match(entry):
+                        try:
+                            entry = self.db.classes[link_class].lookup(entry)
+                        except (TypeError, KeyError):
+                            raise IndexError, 'new property "%s": %s not a %s'%(
+                                key, entry, self.properties[key].classname)
+                    l.append(entry)
+                value = l
+                propvalues[key] = value
+
+                # handle removals
+                if node.has_key(key):
+                    l = node[key]
+                else:
+                    l = []
+                for id in l[:]:
+                    if id in value:
+                        continue
+                    # register the unlink with the old linked node
+                    if self.properties[key].do_journal:
+                        self.db.addjournal(link_class, id, 'unlink',
+                            (self.classname, nodeid, key))
+                    l.remove(id)
+
+                # handle additions
+                for id in value:
+                    if not self.db.hasnode(link_class, id):
+                        raise IndexError, '%s has no node %s'%(
+                            link_class, id)
+                    if id in l:
+                        continue
+                    # register the link with the newly linked node
+                    if self.properties[key].do_journal:
+                        self.db.addjournal(link_class, id, 'link',
+                            (self.classname, nodeid, key))
+                    l.append(id)
+
+            elif isinstance(prop, String):
+                if value is not None and type(value) != type(''):
+                    raise TypeError, 'new property "%s" not a string'%key
+
+            elif isinstance(prop, Password):
+                if not isinstance(value, password.Password):
+                    raise TypeError, 'new property "%s" not a Password'% key
+                propvalues[key] = value
+
+            elif value is not None and isinstance(prop, Date):
+                if not isinstance(value, date.Date):
+                    raise TypeError, 'new property "%s" not a Date'% key
+                propvalues[key] = value
+
+            elif value is not None and isinstance(prop, Interval):
+                if not isinstance(value, date.Interval):
+                    raise TypeError, 'new property "%s" not an Interval'% key
+                propvalues[key] = value
+
+            node[key] = value
+
+        # nothing to do?
+        if not propvalues:
+            return
+
+        # do the set, and journal it
+        self.db.setnode(self.classname, nodeid, node)
+        self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+
+        self.fireReactors('set', nodeid, oldvalues)
+
+    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.
+
+        These operations trigger detectors and can be vetoed.  Attempts
+        to modify the "creation" or "activity" properties cause a KeyError.
+        """
+        if self.db.journaltag is None:
+            raise DatabaseError, 'Database open read-only'
+
+        self.fireAuditors('retire', nodeid, None)
+
+        node = self.db.getnode(self.classname, nodeid)
+        node[self.db.RETIRED_FLAG] = 1
+        self.db.setnode(self.classname, nodeid, node)
+        self.db.addjournal(self.classname, nodeid, 'retired', None)
+
+        self.fireReactors('retire', nodeid, None)
+
+    def history(self, nodeid):
+        """Retrieve the journal of edits on a particular node.
+
+        'nodeid' must be the id of an existing node of this class or an
+        IndexError is raised.
+
+        The returned list contains tuples of the form
+
+            (date, tag, action, params)
+
+        'date' is a Timestamp object specifying the time of the change and
+        'tag' is the journaltag specified when the database was opened.
+        """
+        return self.db.getjournal(self.classname, nodeid)
+
+    # Locating nodes:
+    def hasnode(self, nodeid):
+        '''Determine if the given nodeid actually exists
+        '''
+        return self.db.hasnode(self.classname, nodeid)
+
+    def setkey(self, propname):
+        """Select a String property of this class to be the key property.
+
+        'propname' must be the name of a String property of this class or
+        None, or a TypeError is raised.  The values of the key property on
+        all existing nodes must be unique or a ValueError is raised.
+        """
+        # TODO: validate that the property is a String!
+        self.key = propname
+
+    def getkey(self):
+        """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.
+
+        If this class has no key property, a TypeError is raised.  If the
+        'keyvalue' matches one of the values for the key property among
+        the nodes in this class, the matching node's id is returned;
+        otherwise a KeyError is raised.
+        """
+        cldb = self.db.getclassdb(self.classname)
+        try:
+            for nodeid in self.db.getnodeids(self.classname, cldb):
+                node = self.db.getnode(self.classname, nodeid, cldb)
+                if node.has_key(self.db.RETIRED_FLAG):
+                    continue
+                if node[self.key] == keyvalue:
+                    cldb.close()
+                    return nodeid
+        finally:
+            cldb.close()
+        raise KeyError, keyvalue
+
+    # XXX: change from spec - allows multiple props to match
+    def find(self, **propspec):
+        """Get the ids of nodes in this class which link to the given nodes.
+
+        'propspec' consists of keyword args propname={nodeid:1,}   
+          'propname' must be the name of a property in this class, or a
+            KeyError is raised.  That property must be a Link or Multilink
+            property, or a TypeError is raised.
+
+        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, nodeids in propspec:
+            # check the prop is OK
+            prop = self.properties[propname]
+            if not isinstance(prop, Link) and not isinstance(prop, Multilink):
+                raise TypeError, "'%s' not a Link/Multilink property"%propname
+            #XXX edit is expensive and of questionable use
+            #for nodeid in nodeids:
+            #    if not self.db.hasnode(prop.classname, nodeid):
+            #        raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
+
+        # ok, now do the find
+        cldb = self.db.getclassdb(self.classname)
+        l = []
+        try:
+            for id in self.db.getnodeids(self.classname, db=cldb):
+                node = self.db.getnode(self.classname, id, db=cldb)
+                if node.has_key(self.db.RETIRED_FLAG):
+                    continue
+                for propname, nodeids in propspec:
+                    # can't test if the node doesn't have this property
+                    if not node.has_key(propname):
+                        continue
+                    if type(nodeids) is type(''):
+                        nodeids = {nodeids:1}
+                    prop = self.properties[propname]
+                    value = node[propname]
+                    if isinstance(prop, Link) and nodeids.has_key(value):
+                        l.append(id)
+                        break
+                    elif isinstance(prop, Multilink):
+                        hit = 0
+                        for v in value:
+                            if nodeids.has_key(v):
+                                l.append(id)
+                                hit = 1
+                                break
+                        if hit:
+                            break
+        finally:
+            cldb.close()
+        return l
+
+    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, String):
+                raise TypeError, "'%s' not a String property"%propname
+            requirements[propname] = requirements[propname].lower()
+        l = []
+        cldb = self.db.getclassdb(self.classname)
+        try:
+            for nodeid in self.db.getnodeids(self.classname, cldb):
+                node = self.db.getnode(self.classname, nodeid, cldb)
+                if node.has_key(self.db.RETIRED_FLAG):
+                    continue
+                for key, value in requirements.items():
+                    if node[key] and node[key].lower() != value:
+                        break
+                else:
+                    l.append(nodeid)
+        finally:
+            cldb.close()
+        return l
+
+    def list(self):
+        """Return a list of the ids of the active nodes in this class."""
+        l = []
+        cn = self.classname
+        cldb = self.db.getclassdb(cn)
+        try:
+            for nodeid in self.db.getnodeids(cn, cldb):
+                node = self.db.getnode(cn, nodeid, cldb)
+                if node.has_key(self.db.RETIRED_FLAG):
+                    continue
+                l.append(nodeid)
+        finally:
+            cldb.close()
+        l.sort()
+        return l
+
+    # XXX not in spec
+    def filter(self, search_matches, filterspec, sort, group, 
+            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
+        '''
+        cn = self.classname
+
+        # optimise filterspec
+        l = []
+        props = self.getprops()
+        for k, v in filterspec.items():
+            propclass = props[k]
+            if isinstance(propclass, Link):
+                if type(v) is not type([]):
+                    v = [v]
+                # replace key values with node ids
+                u = []
+                link_class =  self.db.classes[propclass.classname]
+                for entry in v:
+                    if entry == '-1': entry = None
+                    elif not num_re.match(entry):
+                        try:
+                            entry = link_class.lookup(entry)
+                        except (TypeError,KeyError):
+                            raise ValueError, 'property "%s": %s not a %s'%(
+                                k, entry, self.properties[k].classname)
+                    u.append(entry)
+
+                l.append((0, k, u))
+            elif isinstance(propclass, Multilink):
+                if type(v) is not type([]):
+                    v = [v]
+                # replace key values with node ids
+                u = []
+                link_class =  self.db.classes[propclass.classname]
+                for entry in v:
+                    if not num_re.match(entry):
+                        try:
+                            entry = link_class.lookup(entry)
+                        except (TypeError,KeyError):
+                            raise ValueError, 'new property "%s": %s not a %s'%(
+                                k, entry, self.properties[k].classname)
+                    u.append(entry)
+                l.append((1, k, u))
+            elif isinstance(propclass, String):
+                # simple glob searching
+                v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
+                v = v.replace('?', '.')
+                v = v.replace('*', '.*?')
+                l.append((2, k, re.compile(v, re.I)))
+            else:
+                l.append((6, k, v))
+        filterspec = l
+
+        # now, find all the nodes that are active and pass filtering
+        l = []
+        cldb = self.db.getclassdb(cn)
+        try:
+            for nodeid in self.db.getnodeids(cn, cldb):
+                node = self.db.getnode(cn, nodeid, cldb)
+                if node.has_key(self.db.RETIRED_FLAG):
+                    continue
+                # apply filter
+                for t, k, v in filterspec:
+                    # this node doesn't have this property, so reject it
+                    if not node.has_key(k): break
+
+                    if t == 0 and node[k] not in v:
+                        # link - if this node'd property doesn't appear in the
+                        # filterspec's nodeid list, skip it
+                        break
+                    elif t == 1:
+                        # multilink - if any of the nodeids required by the
+                        # filterspec aren't in this node's property, then skip
+                        # it
+                        for value in v:
+                            if value not in node[k]:
+                                break
+                        else:
+                            continue
+                        break
+                    elif t == 2 and (node[k] is None or not v.search(node[k])):
+                        # RE search
+                        break
+                    elif t == 6 and node[k] != v:
+                        # straight value comparison for the other types
+                        break
+                else:
+                    l.append((nodeid, node))
+        finally:
+            cldb.close()
+        l.sort()
+
+        # filter based on full text search
+        if search_matches is not None:
+            k = []
+            l_debug = []
+            for v in l:
+                l_debug.append(v[0])
+                if search_matches.has_key(v[0]):
+                    k.append(v)
+            l = k
+
+        # optimise sort
+        m = []
+        for entry in sort:
+            if entry[0] != '-':
+                m.append(('+', entry))
+            else:
+                m.append((entry[0], entry[1:]))
+        sort = m
+
+        # optimise group
+        m = []
+        for entry in group:
+            if entry[0] != '-':
+                m.append(('+', entry))
+            else:
+                m.append((entry[0], entry[1:]))
+        group = m
+        # 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 list in group, sort:
+                for dir, prop in list:
+                    # 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 = an[prop] = av.lower()
+                        if bv and bv[0] in string.uppercase:
+                            bv = bn[prop] = 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
+
+                    # Multilink properties are sorted according to how many
+                    # links are present.
+                    elif isinstance(propclass, Multilink):
+                        if dir == '+':
+                            r = cmp(len(av), len(bv))
+                            if r != 0: return r
+                        elif dir == '-':
+                            r = cmp(len(bv), len(av))
+                            if r != 0: return r
+                # end for dir, prop in list:
+            # end for list 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]
+
+    def count(self):
+        """Get the number of nodes in this class.
+
+        If the returned integer is 'numnodes', the ids of all the nodes
+        in this class run from 1 to numnodes, and numnodes+1 will be the
+        id of the next node to be created in this class.
+        """
+        return self.db.countnodes(self.classname)
+
+    # Manipulating properties:
+
+    def getprops(self, protected=1):
+        """Return a dictionary mapping property names to property objects.
+           If the "protected" flag is true, we include protected properties -
+           those which may not be modified.
+
+           In addition to the actual properties on the node, these
+           methods provide the "creation" and "activity" properties. If the
+           "protected" flag is true, we include protected properties - those
+           which may not be modified.
+        """
+        d = self.properties.copy()
+        if protected:
+            d['id'] = String()
+            d['creation'] = hyperdb.Date()
+            d['activity'] = hyperdb.Date()
+            d['creator'] = hyperdb.Link("user")
+        return d
+
+    def addprop(self, **properties):
+        """Add properties to this class.
+
+        The keyword arguments in 'properties' must map names to property
+        objects, or a TypeError is raised.  None of the keys in 'properties'
+        may collide with the names of existing properties, or a ValueError
+        is raised before any properties have been added.
+        """
+        for key in properties.keys():
+            if self.properties.has_key(key):
+                raise ValueError, key
+        self.properties.update(properties)
+
+    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, String) and propclass.indexme:
+                # and index them under (classname, nodeid, property)
+                self.db.indexer.add_text((self.classname, nodeid, prop),
+                    str(self.get(nodeid, prop)))
+
+    #
+    # 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)
+
+class 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.
+
+       The default MIME type of this data is defined by the
+       "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 create(self, **propvalues):
+        ''' snaffle the file propvalue and store in a file
+        '''
+        content = propvalues['content']
+        del propvalues['content']
+        newid = Class.create(self, **propvalues)
+        self.db.storefile(self.classname, newid, None, content)
+        return newid
+
+    def get(self, nodeid, propname, default=_marker, cache=1):
+        ''' trap the content propname and get it from the file
+        '''
+
+        poss_msg = 'Possibly a access right configuration problem.'
+        if propname == 'content':
+            try:
+                return self.db.getfile(self.classname, nodeid, None)
+            except IOError, (strerror):
+                # BUG: by catching this we donot see an error in the log.
+                return 'ERROR reading file: %s%s\n%s\n%s'%(
+                        self.classname, nodeid, poss_msg, strerror)
+        if default is not _marker:
+            return Class.get(self, nodeid, propname, default, cache=cache)
+        else:
+            return Class.get(self, nodeid, propname, cache=cache)
+
+    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()
+        if protected:
+            d['content'] = hyperdb.String()
+        return d
+
+    def index(self, nodeid):
+        ''' Index the node in the search index.
+
+            We want to index the content in addition to the normal String
+            property indexing.
+        '''
+        # perform normal indexing
+        Class.index(self, nodeid)
+
+        # get the content to index
+        content = self.get(nodeid, 'content')
+
+        # figure the mime type
+        if self.properties.has_key('type'):
+            mime_type = self.get(nodeid, 'type')
+        else:
+            mime_type = self.default_mime_type
+
+        # and index!
+        self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
+            mime_type)
+
+# XXX deviation from spec - was called ItemClass
+class IssueClass(Class, roundupdb.IssueClass):
+    # Overridden methods:
+    def __init__(self, db, classname, **properties):
+        """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.
+        """
+        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'):
+            properties['nosy'] = hyperdb.Multilink("user")
+        if not properties.has_key('superseder'):
+            properties['superseder'] = hyperdb.Multilink(classname)
+        Class.__init__(self, db, classname, **properties)
+
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.43  2002/07/10 06:30:30  richard
+#...except of course it's nice to use valid Python syntax
+#
 #Revision 1.42  2002/07/10 06:21:38  richard
 #Be extra safe
 #
index 6f8edd7112c1bafcab95999ad2751745251d093c..530a691a80a6d135933c3c4bca0f276d97ebbe2f 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_bsddb.py,v 1.18 2002-05-15 06:21:21 richard Exp $
+#$Id: back_bsddb.py,v 1.19 2002-07-14 02:05:53 richard Exp $
 '''
 This module defines a backend that saves the hyperdatabase in BSDDB.
 '''
@@ -24,12 +24,12 @@ import bsddb, os, marshal
 from roundup import hyperdb, date
 
 # these classes are so similar, we just use the anydbm methods
-import back_anydbm
+from back_anydbm import Database, Class, FileClass, IssueClass
 
 #
 # Now the database
 #
-class Database(back_anydbm.Database):
+class Database(Database):
     """A database for storing records containing flexible data types."""
     #
     # Class DBs
@@ -119,6 +119,14 @@ class Database(back_anydbm.Database):
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.18  2002/05/15 06:21:21  richard
+# . node caching now works, and gives a small boost in performance
+#
+#As a part of this, I cleaned up the DEBUG output and implemented TRACE
+#output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
+#CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
+#(using if __debug__ which is compiled out with -O)
+#
 #Revision 1.17  2002/04/03 05:54:31  richard
 #Fixed serialisation problem by moving the serialisation step out of the
 #hyperdb.Class (get, set) into the hyperdb.Database.
index 287b2abdbab421638fff89691351e95b0bbb45ec..bce357a1e0e22cfb31a0a96cc32eeb98d07ed131 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-#$Id: back_bsddb3.py,v 1.13 2002-07-08 06:41:03 richard Exp $
+#$Id: back_bsddb3.py,v 1.14 2002-07-14 02:05:54 richard Exp $
 
 import bsddb3, os, marshal
 from roundup import hyperdb, date
 
 # these classes are so similar, we just use the anydbm methods
-import back_anydbm
+from back_anydbm import Database, Class, FileClass, IssueClass
 
 #
 # Now the database
 #
-class Database(back_anydbm.Database):
+class Database(Database):
     """A database for storing records containing flexible data types."""
     #
     # Class DBs
@@ -115,6 +115,9 @@ class Database(back_anydbm.Database):
 
 #
 #$Log: not supported by cvs2svn $
+#Revision 1.13  2002/07/08 06:41:03  richard
+#Was reopening the database with 'n'.
+#
 #Revision 1.12  2002/05/21 05:52:11  richard
 #Well whadya know, bsddb3 works again.
 #The backend is implemented _exactly_ the same as bsddb - so there's no
index af70c2d4761b6a46ac17667ac2d02df184f0ff02..3f6429f82b510adb18f6af68bc30ecf5d17dc11d 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: hyperdb.py,v 1.74 2002-07-10 00:24:10 richard Exp $
+# $Id: hyperdb.py,v 1.75 2002-07-14 02:05:53 richard Exp $
 
 __doc__ = """
 Hyperdatabase implementation, especially field types.
 """
 
 # standard python modules
-import sys, re, string, weakref, os, time
+import sys, os, time, re
 
 # roundup modules
 import date, password
@@ -110,15 +110,26 @@ class Multilink:
         ' more useful for dumps '
         return '<%s to "%s">'%(self.__class__, self.classname)
 
-class DatabaseError(ValueError):
-    '''Error to be raised when there is some problem in the database code
-    '''
+#
+# Support for splitting designators
+#
+class DesignatorError(ValueError):
     pass
-
+def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
+    ''' Take a foo123 and return ('foo', 123)
+    '''
+    m = dre.match(designator)
+    if m is None:
+        raise DesignatorError, '"%s" not a node designator'%designator
+    return m.group(1), m.group(2)
 
 #
 # the base Database class
 #
+class DatabaseError(ValueError):
+    '''Error to be raised when there is some problem in the database code
+    '''
+    pass
 class Database:
     '''A database for storing records containing flexible data types.
 
@@ -137,6 +148,13 @@ in-database value is returned in preference to the in-transaction value.
 This is necessary to determine if any values have changed during a
 transaction.
 
+
+Implementation
+--------------
+
+All methods except __repr__ and getnode must be implemented by a
+concrete backend Class.
+
 '''
 
     # flag to set on retired entries
@@ -203,29 +221,7 @@ transaction.
         '''Copy the node contents, converting non-marshallable data into
            marshallable data.
         '''
-        if __debug__:
-            print >>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):
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, Password):
-                d[k] = str(v)
-            elif isinstance(prop, Date) and v is not None:
-                d[k] = v.get_tuple()
-            elif isinstance(prop, Interval) and v is not None:
-                d[k] = v.get_tuple()
-            else:
-                d[k] = v
-        return d
+        return node
 
     def setnode(self, classname, nodeid, node):
         '''Change the specified node.
@@ -235,31 +231,7 @@ transaction.
     def unserialise(self, classname, node):
         '''Decode the marshalled node data
         '''
-        if __debug__:
-            print >>DEBUG, 'unserialise', 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):
-                d[k] = v
-                continue
-
-            # get the property spec
-            prop = properties[k]
-
-            if isinstance(prop, Date) and v is not None:
-                d[k] = date.Date(v)
-            elif isinstance(prop, Interval) and v is not None:
-                d[k] = date.Interval(v)
-            elif isinstance(prop, Password):
-                p = password.Password()
-                p.unpack(v)
-                d[k] = p
-            else:
-                d[k] = v
-        return d
+        return node
 
     def getnode(self, classname, nodeid, db=None, cache=1):
         '''Get a node from the database.
@@ -330,12 +302,15 @@ transaction.
         '''
         raise NotImplementedError
 
-_marker = []
 #
 # The base Class class
 #
 class Class:
-    """The handle to a particular class of nodes in a hyperdatabase."""
+    """ The handle to a particular class of nodes in a hyperdatabase.
+        
+        All methods except __repr__ and getnode must be implemented by a
+        concrete backend Class.
+    """
 
     def __init__(self, db, classname, **properties):
         """Create a new class with a given name and property specification.
@@ -344,13 +319,7 @@ class Class:
         or a ValueError is raised.  The keyword arguments in 'properties'
         must map names to property objects, or a TypeError is raised.
         """
-        self.classname = classname
-        self.properties = properties
-        self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
-        self.key = ''
-
-        # do the db-related init stuff
-        db.addclass(self)
+        raise NotImplementedError
 
     def __repr__(self):
         '''Slightly more useful representation
@@ -376,118 +345,9 @@ class Class:
         If an id in a link or multilink property does not refer to a valid
         node, an IndexError is raised.
         """
-        if propvalues.has_key('id'):
-            raise KeyError, '"id" is reserved'
-
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-
-        # new node's id
-        newid = self.db.newid(self.classname)
-
-        # validate propvalues
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            if key == self.key:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            # try to handle this property
-            try:
-                prop = self.properties[key]
-            except KeyError:
-                raise KeyError, '"%s" has no property "%s"'%(self.classname,
-                    key)
-
-            if isinstance(prop, Link):
-                if type(value) != type(''):
-                    raise ValueError, 'link value must be String'
-                link_class = self.properties[key].classname
-                # if it isn't a number, it's a key
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError, 'new property "%s": %s not a %s'%(
-                            key, value, link_class)
-                elif not self.db.hasnode(link_class, value):
-                    raise IndexError, '%s has no node %s'%(link_class, value)
-
-                # save off the value
-                propvalues[key] = value
-
-                # register the link with the newly linked node
-                if self.properties[key].do_journal:
-                    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
-
-                # clean up and validate the list of links
-                link_class = self.properties[key].classname
-                l = []
-                for entry in value:
-                    if type(entry) != type(''):
-                        raise ValueError, '"%s" link value (%s) must be String' % (key, value)
-                    # if it isn't a number, it's a key
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise IndexError, '%s has no node %s'%(link_class, id)
-                    # register the link with the newly linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'link',
-                            (self.classname, newid, key))
-
-            elif isinstance(prop, String):
-                if type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif isinstance(prop, Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError, 'new property "%s" not a Password'%key
-
-            elif isinstance(prop, 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):
-                if value is not None and not isinstance(value, date.Interval):
-                    raise TypeError, 'new property "%s" not an Interval'%key
-
-        # make sure there's data where there needs to be
-        for key, prop in self.properties.items():
-            if propvalues.has_key(key):
-                continue
-            if key == self.key:
-                raise ValueError, 'key property "%s" is required'%key
-            if isinstance(prop, Multilink):
-                propvalues[key] = []
-            else:
-                # TODO: None isn't right here, I think...
-                propvalues[key] = None
-
-        # done
-        self.db.addnode(self.classname, newid, propvalues)
-        self.db.addjournal(self.classname, newid, 'create', propvalues)
-        return newid
+        raise NotImplementedError
 
+    _marker = []
     def get(self, nodeid, propname, default=_marker, cache=1):
         """Get the value of a property on an existing node of this class.
 
@@ -500,26 +360,7 @@ class Class:
         determine what its values prior to modification are, you need to
         set cache=0.
         """
-        if propname == 'id':
-            return nodeid
-
-        # get the property (raises KeyErorr if invalid)
-        prop = self.properties[propname]
-
-        # get the node's dict
-        d = self.db.getnode(self.classname, nodeid, cache=cache)
-
-        if not d.has_key(propname):
-            if default is _marker:
-                if isinstance(prop, Multilink):
-                    return []
-                else:
-                    # TODO: None isn't right here, I think...
-                    return None
-            else:
-                return default
-
-        return d[propname]
+        raise NotImplementedError
 
     # XXX not in spec
     def getnode(self, nodeid, cache=1):
@@ -553,142 +394,7 @@ class Class:
         If the value of a Link or Multilink property contains an invalid
         node id, a ValueError is raised.
         """
-        if not propvalues:
-            return
-
-        if propvalues.has_key('id'):
-            raise KeyError, '"id" is reserved'
-
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-
-        node = self.db.getnode(self.classname, nodeid)
-        if node.has_key(self.db.RETIRED_FLAG):
-            raise IndexError
-        num_re = re.compile('^\d+$')
-        for key, value in propvalues.items():
-            # check to make sure we're not duplicating an existing key
-            if key == self.key and node[key] != value:
-                try:
-                    self.lookup(value)
-                except KeyError:
-                    pass
-                else:
-                    raise ValueError, 'node with key "%s" exists'%value
-
-            # this will raise the KeyError if the property isn't valid
-            # ... we don't use getprops() here because we only care about
-            # the writeable properties.
-            prop = self.properties[key]
-
-            # if the value's the same as the existing value, no sense in
-            # doing anything
-            if node.has_key(key) and value == node[key]:
-                del propvalues[key]
-                continue
-
-            # do stuff based on the prop type
-            if isinstance(prop, Link):
-                link_class = self.properties[key].classname
-                # if it isn't a number, it's a key
-                if type(value) != type(''):
-                    raise ValueError, 'link value must be String'
-                if not num_re.match(value):
-                    try:
-                        value = self.db.classes[link_class].lookup(value)
-                    except (TypeError, KeyError):
-                        raise IndexError, 'new property "%s": %s not a %s'%(
-                            key, value, self.properties[key].classname)
-
-                if not self.db.hasnode(link_class, value):
-                    raise IndexError, '%s has no node %s'%(link_class, value)
-
-                if self.properties[key].do_journal:
-                    # register the unlink with the old linked node
-                    if node[key] is not None:
-                        self.db.addjournal(link_class, node[key], 'unlink',
-                            (self.classname, nodeid, key))
-
-                    # register the link with the newly linked node
-                    if value is not None:
-                        self.db.addjournal(link_class, value, 'link',
-                            (self.classname, nodeid, key))
-
-            elif isinstance(prop, Multilink):
-                if type(value) != type([]):
-                    raise TypeError, 'new property "%s" not a list of ids'%key
-                link_class = self.properties[key].classname
-                l = []
-                for entry in value:
-                    # if it isn't a number, it's a key
-                    if type(entry) != type(''):
-                        raise ValueError, 'new property "%s" link value ' \
-                            'must be a string'%key
-                    if not num_re.match(entry):
-                        try:
-                            entry = self.db.classes[link_class].lookup(entry)
-                        except (TypeError, KeyError):
-                            raise IndexError, 'new property "%s": %s not a %s'%(
-                                key, entry, self.properties[key].classname)
-                    l.append(entry)
-                value = l
-                propvalues[key] = value
-
-                # handle removals
-                if node.has_key(key):
-                    l = node[key]
-                else:
-                    l = []
-                for id in l[:]:
-                    if id in value:
-                        continue
-                    # register the unlink with the old linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'unlink',
-                            (self.classname, nodeid, key))
-                    l.remove(id)
-
-                # handle additions
-                for id in value:
-                    if not self.db.hasnode(link_class, id):
-                        raise IndexError, '%s has no node %s'%(
-                            link_class, id)
-                    if id in l:
-                        continue
-                    # register the link with the newly linked node
-                    if self.properties[key].do_journal:
-                        self.db.addjournal(link_class, id, 'link',
-                            (self.classname, nodeid, key))
-                    l.append(id)
-
-            elif isinstance(prop, String):
-                if value is not None and type(value) != type(''):
-                    raise TypeError, 'new property "%s" not a string'%key
-
-            elif isinstance(prop, Password):
-                if not isinstance(value, password.Password):
-                    raise TypeError, 'new property "%s" not a Password'% key
-                propvalues[key] = value
-
-            elif value is not None and isinstance(prop, Date):
-                if not isinstance(value, date.Date):
-                    raise TypeError, 'new property "%s" not a Date'% key
-                propvalues[key] = value
-
-            elif value is not None and isinstance(prop, Interval):
-                if not isinstance(value, date.Interval):
-                    raise TypeError, 'new property "%s" not an Interval'% key
-                propvalues[key] = value
-
-            node[key] = value
-
-        # nothing to do?
-        if not propvalues:
-            return
-
-        # do the set, and journal it
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'set', propvalues)
+        raise NotImplementedError
 
     def retire(self, nodeid):
         """Retire a node.
@@ -699,12 +405,7 @@ class Class:
         Retired nodes are not returned by the find(), list(), or lookup()
         methods, and other nodes may reuse the values of their key properties.
         """
-        if self.db.journaltag is None:
-            raise DatabaseError, 'Database open read-only'
-        node = self.db.getnode(self.classname, nodeid)
-        node[self.db.RETIRED_FLAG] = 1
-        self.db.setnode(self.classname, nodeid, node)
-        self.db.addjournal(self.classname, nodeid, 'retired', None)
+        raise NotImplementedError
 
     def history(self, nodeid):
         """Retrieve the journal of edits on a particular node.
@@ -719,13 +420,13 @@ class Class:
         'date' is a Timestamp object specifying the time of the change and
         'tag' is the journaltag specified when the database was opened.
         """
-        return self.db.getjournal(self.classname, nodeid)
+        raise NotImplementedError
 
     # Locating nodes:
     def hasnode(self, nodeid):
         '''Determine if the given nodeid actually exists
         '''
-        return self.db.hasnode(self.classname, nodeid)
+        raise NotImplementedError
 
     def setkey(self, propname):
         """Select a String property of this class to be the key property.
@@ -734,12 +435,11 @@ class Class:
         None, or a TypeError is raised.  The values of the key property on
         all existing nodes must be unique or a ValueError is raised.
         """
-        # TODO: validate that the property is a String!
-        self.key = propname
+        raise NotImplementedError
 
     def getkey(self):
         """Return the name of the key property for this class or None."""
-        return self.key
+        raise NotImplementedError
 
     def labelprop(self, default_to_id=0):
         ''' Return the property name for a label for the given node.
@@ -751,21 +451,8 @@ class Class:
             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?
+        raise NotImplementedError
+
     def lookup(self, keyvalue):
         """Locate a particular node by its key property and return its id.
 
@@ -774,18 +461,7 @@ class Class:
         the nodes in this class, the matching node's id is returned;
         otherwise a KeyError is raised.
         """
-        cldb = self.db.getclassdb(self.classname)
-        try:
-            for nodeid in self.db.getnodeids(self.classname, cldb):
-                node = self.db.getnode(self.classname, nodeid, cldb)
-                if node.has_key(self.db.RETIRED_FLAG):
-                    continue
-                if node[self.key] == keyvalue:
-                    cldb.close()
-                    return nodeid
-        finally:
-            cldb.close()
-        raise KeyError, keyvalue
+        raise NotImplementedError
 
     # XXX: change from spec - allows multiple props to match
     def find(self, **propspec):
@@ -798,96 +474,12 @@ class Class:
 
         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, nodeids in propspec:
-            # check the prop is OK
-            prop = self.properties[propname]
-            if not isinstance(prop, Link) and not isinstance(prop, Multilink):
-                raise TypeError, "'%s' not a Link/Multilink property"%propname
-            #XXX edit is expensive and of questionable use
-            #for nodeid in nodeids:
-            #    if not self.db.hasnode(prop.classname, nodeid):
-            #        raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
-
-        # ok, now do the find
-        cldb = self.db.getclassdb(self.classname)
-        l = []
-        try:
-            for id in self.db.getnodeids(self.classname, db=cldb):
-                node = self.db.getnode(self.classname, id, db=cldb)
-                if node.has_key(self.db.RETIRED_FLAG):
-                    continue
-                for propname, nodeids in propspec:
-                    # can't test if the node doesn't have this property
-                    if not node.has_key(propname):
-                        continue
-                    if type(nodeids) is type(''):
-                        nodeids = {nodeids:1}
-                    prop = self.properties[propname]
-                    value = node[propname]
-                    if isinstance(prop, Link) and nodeids.has_key(value):
-                        l.append(id)
-                        break
-                    elif isinstance(prop, Multilink):
-                        hit = 0
-                        for v in value:
-                            if nodeids.has_key(v):
-                                l.append(id)
-                                hit = 1
-                                break
-                        if hit:
-                            break
-        finally:
-            cldb.close()
-        return l
+        that "foo" occurs in msg1, msg3 and file7, so we have hits on these
+        issues:
 
-    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.
+            db.issue.find(messages={'1':1,'3':1}, files={'7':1})
         """
-        for propname in requirements.keys():
-            prop = self.properties[propname]
-            if isinstance(not prop, String):
-                raise TypeError, "'%s' not a String property"%propname
-            requirements[propname] = requirements[propname].lower()
-        l = []
-        cldb = self.db.getclassdb(self.classname)
-        try:
-            for nodeid in self.db.getnodeids(self.classname, cldb):
-                node = self.db.getnode(self.classname, nodeid, cldb)
-                if node.has_key(self.db.RETIRED_FLAG):
-                    continue
-                for key, value in requirements.items():
-                    if node[key] and node[key].lower() != value:
-                        break
-                else:
-                    l.append(nodeid)
-        finally:
-            cldb.close()
-        return l
-
-    def list(self):
-        """Return a list of the ids of the active nodes in this class."""
-        l = []
-        cn = self.classname
-        cldb = self.db.getclassdb(cn)
-        try:
-            for nodeid in self.db.getnodeids(cn, cldb):
-                node = self.db.getnode(cn, nodeid, cldb)
-                if node.has_key(self.db.RETIRED_FLAG):
-                    continue
-                l.append(nodeid)
-        finally:
-            cldb.close()
-        l.sort()
-        return l
+        raise NotImplementedError
 
     # XXX not in spec
     def filter(self, search_matches, filterspec, sort, group, 
@@ -896,223 +488,7 @@ class Class:
             match the 'filter' spec, sorted by the group spec and then the
             sort spec
         '''
-        cn = self.classname
-
-        # optimise filterspec
-        l = []
-        props = self.getprops()
-        for k, v in filterspec.items():
-            propclass = props[k]
-            if isinstance(propclass, Link):
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if entry == '-1': entry = None
-                    elif not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except (TypeError,KeyError):
-                            raise ValueError, 'property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-
-                l.append((0, k, u))
-            elif isinstance(propclass, Multilink):
-                if type(v) is not type([]):
-                    v = [v]
-                # replace key values with node ids
-                u = []
-                link_class =  self.db.classes[propclass.classname]
-                for entry in v:
-                    if not num_re.match(entry):
-                        try:
-                            entry = link_class.lookup(entry)
-                        except (TypeError,KeyError):
-                            raise ValueError, 'new property "%s": %s not a %s'%(
-                                k, entry, self.properties[k].classname)
-                    u.append(entry)
-                l.append((1, k, u))
-            elif isinstance(propclass, String):
-                # simple glob searching
-                v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
-                v = v.replace('?', '.')
-                v = v.replace('*', '.*?')
-                l.append((2, k, re.compile(v, re.I)))
-            else:
-                l.append((6, k, v))
-        filterspec = l
-
-        # now, find all the nodes that are active and pass filtering
-        l = []
-        cldb = self.db.getclassdb(cn)
-        try:
-            for nodeid in self.db.getnodeids(cn, cldb):
-                node = self.db.getnode(cn, nodeid, cldb)
-                if node.has_key(self.db.RETIRED_FLAG):
-                    continue
-                # apply filter
-                for t, k, v in filterspec:
-                    # this node doesn't have this property, so reject it
-                    if not node.has_key(k): break
-
-                    if t == 0 and node[k] not in v:
-                        # link - if this node'd property doesn't appear in the
-                        # filterspec's nodeid list, skip it
-                        break
-                    elif t == 1:
-                        # multilink - if any of the nodeids required by the
-                        # filterspec aren't in this node's property, then skip
-                        # it
-                        for value in v:
-                            if value not in node[k]:
-                                break
-                        else:
-                            continue
-                        break
-                    elif t == 2 and (node[k] is None or not v.search(node[k])):
-                        # RE search
-                        break
-                    elif t == 6 and node[k] != v:
-                        # straight value comparison for the other types
-                        break
-                else:
-                    l.append((nodeid, node))
-        finally:
-            cldb.close()
-        l.sort()
-
-        # filter based on full text search
-        if search_matches is not None:
-            k = []
-            l_debug = []
-            for v in l:
-                l_debug.append(v[0])
-                if search_matches.has_key(v[0]):
-                    k.append(v)
-            l = k
-
-        # optimise sort
-        m = []
-        for entry in sort:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        sort = m
-
-        # optimise group
-        m = []
-        for entry in group:
-            if entry[0] != '-':
-                m.append(('+', entry))
-            else:
-                m.append((entry[0], entry[1:]))
-        group = m
-        # 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 list in group, sort:
-                for dir, prop in list:
-                    # 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 = an[prop] = av.lower()
-                        if bv and bv[0] in string.uppercase:
-                            bv = bn[prop] = 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
-
-                    # Multilink properties are sorted according to how many
-                    # links are present.
-                    elif isinstance(propclass, Multilink):
-                        if dir == '+':
-                            r = cmp(len(av), len(bv))
-                            if r != 0: return r
-                        elif dir == '-':
-                            r = cmp(len(bv), len(av))
-                            if r != 0: return r
-                # end for dir, prop in list:
-            # end for list 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]
+        raise NotImplementedError
 
     def count(self):
         """Get the number of nodes in this class.
@@ -1121,18 +497,15 @@ class Class:
         in this class run from 1 to numnodes, and numnodes+1 will be the
         id of the next node to be created in this class.
         """
-        return self.db.countnodes(self.classname)
+        raise NotImplementedError
 
     # Manipulating properties:
-
     def getprops(self, protected=1):
         """Return a dictionary mapping property names to property objects.
            If the "protected" flag is true, we include protected properties -
-           those which may not be modified."""
-        d = self.properties.copy()
-        if protected:
-            d['id'] = String()
-        return d
+           those which may not be modified.
+        """
+        raise NotImplementedError
 
     def addprop(self, **properties):
         """Add properties to this class.
@@ -1142,20 +515,12 @@ class Class:
         may collide with the names of existing properties, or a ValueError
         is raised before any properties have been added.
         """
-        for key in properties.keys():
-            if self.properties.has_key(key):
-                raise ValueError, key
-        self.properties.update(properties)
+        raise NotImplementedError
 
     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, String) and propclass.indexme:
-                # and index them under (classname, nodeid, property)
-                self.db.indexer.add_text((self.classname, nodeid, prop),
-                    str(self.get(nodeid, prop)))
+        raise NotImplementedError
 
 # XXX not in spec
 class Node:
@@ -1215,6 +580,9 @@ def Choice(name, db, *options):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.74  2002/07/10 00:24:10  richard
+# braino
+#
 # Revision 1.73  2002/07/10 00:19:48  richard
 # Added explicit closing of backend database handles.
 #
index fe2cde8b678939abe5a5e7c69a96aa120c40394d..9214fb614d9984c62bff26049993b85ea881e7e3 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: init.py,v 1.19 2002-05-23 01:14:20 richard Exp $
+# $Id: init.py,v 1.20 2002-07-14 02:05:53 richard Exp $
 
 __doc__ = """
 Init (create) a roundup instance.
@@ -98,7 +98,8 @@ def install(instance_home, template, backend):
 
     # now select database
     db = '''# WARNING: DO NOT EDIT THIS FILE!!!
-from roundup.backends.back_%s import Database'''%backend
+from roundup.backends.back_%s import Database, Class, FileClass, IssueClass
+'''%backend
     open(os.path.join(instance_home, 'select_db.py'), 'w').write(db)
 
 
@@ -113,6 +114,10 @@ def initialise(instance_home, adminpw):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.19  2002/05/23 01:14:20  richard
+#  . split instance initialisation into two steps, allowing config changes
+#    before the database is initialised.
+#
 # Revision 1.18  2001/11/22 15:46:42  jhermann
 # Added module docstrings to all modules.
 #
index dc181a4f3a78e60c3fc67e02c68f528129ddbdda..57e678da6ed9cbc74b4a2d7b412913e93d0c21e8 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: roundupdb.py,v 1.61 2002-07-09 04:19:09 richard Exp $
+# $Id: roundupdb.py,v 1.62 2002-07-14 02:05:53 richard Exp $
 
 __doc__ = """
 Extending hyperdb with types specific to issue-tracking.
 """
 
-import re, os, smtplib, socket, copy, time, random
+import re, os, smtplib, socket, time, random
 import MimeWriter, cStringIO
 import base64, quopri, mimetypes
 # if available, use the 'email' module, otherwise fallback to 'rfc822'
@@ -30,22 +30,12 @@ try :
 except ImportError :
     from rfc822 import dump_address_pair as straddr
 
-import hyperdb, date
+import hyperdb
 
 # set to indicate to roundup not to actually _send_ email
 # this var must contain a file to write the mail to
 SENDMAILDEBUG = os.environ.get('SENDMAILDEBUG', '')
 
-class DesignatorError(ValueError):
-    pass
-def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
-    ''' Take a foo123 and return ('foo', 123)
-    '''
-    m = dre.match(designator)
-    if m is None:
-        raise DesignatorError, '"%s" not a node designator'%designator
-    return m.group(1), m.group(2)
-
 
 def extractUserFromList(userClass, users):
     '''Given a list of users, try to extract the first non-anonymous user
@@ -102,200 +92,6 @@ class Database:
         else:
             return 0
 
-_marker = []
-# XXX: added the 'creator' faked attribute
-class Class(hyperdb.Class):
-    # Overridden methods:
-    def __init__(self, db, classname, **properties):
-        if (properties.has_key('creation') or properties.has_key('activity')
-                or properties.has_key('creator')):
-            raise ValueError, '"creation", "activity" and "creator" are reserved'
-        hyperdb.Class.__init__(self, db, classname, **properties)
-        self.auditors = {'create': [], 'set': [], 'retire': []}
-        self.reactors = {'create': [], 'set': [], 'retire': []}
-
-    def create(self, **propvalues):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
-        self.fireAuditors('create', None, propvalues)
-        nodeid = hyperdb.Class.create(self, **propvalues)
-        self.fireReactors('create', nodeid, None)
-        return nodeid
-
-    def set(self, nodeid, **propvalues):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        if propvalues.has_key('creation') or propvalues.has_key('activity'):
-            raise KeyError, '"creation" and "activity" are reserved'
-        self.fireAuditors('set', nodeid, propvalues)
-        # Take a copy of the node dict so that the subsequent set
-        # operation doesn't modify the oldvalues structure.
-        try:
-            # try not using the cache initially
-            oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
-                cache=0))
-        except IndexError:
-            # this will be needed if somone does a create() and set()
-            # with no intervening commit()
-            oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
-        hyperdb.Class.set(self, nodeid, **propvalues)
-        self.fireReactors('set', nodeid, oldvalues)
-
-    def retire(self, nodeid):
-        """These operations trigger detectors and can be vetoed.  Attempts
-        to modify the "creation" or "activity" properties cause a KeyError.
-        """
-        self.fireAuditors('retire', nodeid, None)
-        hyperdb.Class.retire(self, nodeid)
-        self.fireReactors('retire', nodeid, None)
-
-    def get(self, nodeid, propname, default=_marker, cache=1):
-        """Attempts to get the "creation" or "activity" properties should
-        do the right thing.
-        """
-        if propname == 'creation':
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                return self.db.getjournal(self.classname, nodeid)[0][1]
-            else:
-                # on the strange chance that there's no journal
-                return date.Date()
-        if propname == 'activity':
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                return self.db.getjournal(self.classname, nodeid)[-1][1]
-            else:
-                # on the strange chance that there's no journal
-                return date.Date()
-        if propname == 'creator':
-            journal = self.db.getjournal(self.classname, nodeid)
-            if journal:
-                name = self.db.getjournal(self.classname, nodeid)[0][2]
-            else:
-                return None
-            return self.db.user.lookup(name)
-        if default is not _marker:
-            return hyperdb.Class.get(self, nodeid, propname, default,
-                cache=cache)
-        else:
-            return hyperdb.Class.get(self, nodeid, propname, cache=cache)
-
-    def getprops(self, protected=1):
-        """In addition to the actual properties on the node, these
-        methods provide the "creation" and "activity" properties. If the
-        "protected" flag is true, we include protected properties - those
-        which may not be modified.
-        """
-        d = hyperdb.Class.getprops(self, protected=protected).copy()
-        if protected:
-            d['creation'] = hyperdb.Date()
-            d['activity'] = hyperdb.Date()
-            d['creator'] = hyperdb.Link("user")
-        return d
-
-    #
-    # 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)
-
-class 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.
-
-       The default MIME type of this data is defined by the
-       "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 create(self, **propvalues):
-        ''' snaffle the file propvalue and store in a file
-        '''
-        content = propvalues['content']
-        del propvalues['content']
-        newid = Class.create(self, **propvalues)
-        self.db.storefile(self.classname, newid, None, content)
-        return newid
-
-    def get(self, nodeid, propname, default=_marker, cache=1):
-        ''' trap the content propname and get it from the file
-        '''
-
-        poss_msg = 'Possibly a access right configuration problem.'
-        if propname == 'content':
-            try:
-                return self.db.getfile(self.classname, nodeid, None)
-            except IOError, (strerror):
-                # BUG: by catching this we donot see an error in the log.
-                return 'ERROR reading file: %s%s\n%s\n%s'%(
-                        self.classname, nodeid, poss_msg, strerror)
-        if default is not _marker:
-            return Class.get(self, nodeid, propname, default, cache=cache)
-        else:
-            return Class.get(self, nodeid, propname, cache=cache)
-
-    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()
-        if protected:
-            d['content'] = hyperdb.String()
-        return d
-
-    def index(self, nodeid):
-        ''' Index the node in the search index.
-
-            We want to index the content in addition to the normal String
-            property indexing.
-        '''
-        # perform normal indexing
-        Class.index(self, nodeid)
-
-        # get the content to index
-        content = self.get(nodeid, 'content')
-
-        # figure the mime type
-        if self.properties.has_key('type'):
-            mime_type = self.get(nodeid, 'type')
-        else:
-            mime_type = self.default_mime_type
-
-        # and index!
-        self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
-            mime_type)
-
 class MessageSendError(RuntimeError):
     pass
 
@@ -303,29 +99,19 @@ class DetectorError(RuntimeError):
     pass
 
 # XXX deviation from spec - was called ItemClass
-class IssueClass(Class):
-
-    # Overridden methods:
-
-    def __init__(self, db, classname, **properties):
-        """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."""
-        if not properties.has_key('title'):
+class IssueClass:
+    """ This class is intended to be mixed-in with a hyperdb backend
+        implementation. The backend should provide a mechanism that
+        enforces the title, messages, files, nosy and superseder
+        properties:
             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'):
             properties['nosy'] = hyperdb.Multilink("user")
-        if not properties.has_key('superseder'):
             properties['superseder'] = hyperdb.Multilink(classname)
-        Class.__init__(self, db, classname, **properties)
+    """
 
     # New methods:
-
     def addmessage(self, nodeid, summary, text):
         """Add a message to an issue's mail spool.
 
@@ -553,15 +339,16 @@ class IssueClass(Class):
         # simplistic check to see if the url is valid,
         # then append a trailing slash if it is missing
         base = self.db.config.ISSUE_TRACKER_WEB 
-        if not isinstance( base , type('') ) or not base.startswith( "http://" ) :
-            base = "Configuration Error: ISSUE_TRACKER_WEB isn't a fully-qualified URL"
+        if not isinstance(base , type('')) or not base.startswith('http://'):
+            base = "Configuration Error: ISSUE_TRACKER_WEB isn't a " \
+                "fully-qualified URL"
         elif base[-1] != '/' :
             base += '/'
         web = base + 'issue'+ nodeid
 
         # ensure the email address is properly quoted
-        email = straddr( (self.db.config.INSTANCE_NAME ,
-                          self.db.config.ISSUE_TRACKER_EMAIL) )
+        email = straddr((self.db.config.INSTANCE_NAME,
+            self.db.config.ISSUE_TRACKER_EMAIL))
 
         line = '_' * max(len(web), len(email))
         return '%s\n%s\n%s\n%s'%(line, email, web, line)
@@ -608,12 +395,10 @@ class IssueClass(Class):
     def generateChangeNote(self, nodeid, oldvalues):
         """Generate a change note that lists property changes
         """
-
         if __debug__ :
-            if not isinstance( oldvalues , type({}) ) :
-                raise TypeError(
-                        "'oldvalues' must be dict-like, not %s."
-                        % str(type(oldvalues)) )
+            if not isinstance(oldvalues, type({})) :
+                raise TypeError("'oldvalues' must be dict-like, not %s."%
+                    type(oldvalues))
 
         cn = self.classname
         cl = self.db.classes[cn]
@@ -691,6 +476,11 @@ class IssueClass(Class):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.61  2002/07/09 04:19:09  richard
+# Added reindex command to roundup-admin.
+# Fixed reindex on first access.
+# Also fixed reindexing of entries that change.
+#
 # Revision 1.60  2002/07/09 03:02:52  richard
 # More indexer work:
 # - all String properties may now be indexed too. Currently there's a bit of
index f6c1829d367986f77c84396d6d48b04fb3452b6d..03fbeac902c44529982c65f6c85b51a1d38fb9dc 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.18 2002-07-09 03:02:53 richard Exp $
+# $Id: dbinit.py,v 1.19 2002-07-14 02:05:54 richard Exp $
 
 import os
 
 import instance_config
-from roundup import roundupdb
-import select_db
+from select_db import Database, Class, FileClass, IssueClass
 
-from roundup.roundupdb import Class, FileClass
-
-class Database(roundupdb.Database, select_db.Database):
-    ''' Creates a hybrid database from: 
-         . the selected database back-end from select_db
-         . the roundup extensions from roundupdb 
-    ''' 
-    pass 
-
-class IssueClass(roundupdb.IssueClass):
-    ''' issues need the email information
-    '''
-    pass
-
 def open(name=None):
     ''' as from the roundupdb method openDB 
     ''' 
     from roundup.hyperdb import String, Password, Date, Link, Multilink
 
@@ -143,6 +126,22 @@ def init(adminpw):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.18  2002/07/09 03:02:53  richard
+# More indexer work:
+# - all String properties may now be indexed too. Currently there's a bit of
+#   "issue" specific code in the actual searching which needs to be
+#   addressed. In a nutshell:
+#   + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+#         file = FileClass(db, "file", name=String(), type=String(),
+#             comment=String(indexme="yes"))
+#   + the comment will then be indexed and be searchable, with the results
+#     related back to the issue that the file is linked to
+# - as a result of this work, the FileClass has a default MIME type that may
+#   be overridden in a subclass, or by the use of a "type" property as is
+#   done in the default templates.
+# - the regeneration of the indexes (if necessary) is done once the schema is
+#   set up in the dbinit.
+#
 # Revision 1.17  2002/05/24 04:03:23  richard
 # Added commentage to the dbinit files to help people with their
 # customisation.
index fb8f875c236e996878b78a72b62680ecf8dad4b7..0ada216bde71f11f1f8e46f6ec901c0630149145 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: dbinit.py,v 1.22 2002-07-09 03:02:53 richard Exp $
+# $Id: dbinit.py,v 1.23 2002-07-14 02:05:54 richard Exp $
 
 import os
 
 import instance_config
-from roundup import roundupdb
-import select_db
+from select_db import Database, Class, FileClass, IssueClass
 
-from roundup.roundupdb import Class, FileClass
-
-class Database(roundupdb.Database, select_db.Database):
-    ''' Creates a hybrid database from: 
-         . the selected database back-end from select_db
-         . the roundup extensions from roundupdb 
-    ''' 
-    pass 
-
-class IssueClass(roundupdb.IssueClass):
-    ''' issues need the email information
-    '''
-    pass
-
 def open(name=None):
     ''' as from the roundupdb method openDB 
  
@@ -195,6 +179,22 @@ def init(adminpw):
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.22  2002/07/09 03:02:53  richard
+# More indexer work:
+# - all String properties may now be indexed too. Currently there's a bit of
+#   "issue" specific code in the actual searching which needs to be
+#   addressed. In a nutshell:
+#   + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+#         file = FileClass(db, "file", name=String(), type=String(),
+#             comment=String(indexme="yes"))
+#   + the comment will then be indexed and be searchable, with the results
+#     related back to the issue that the file is linked to
+# - as a result of this work, the FileClass has a default MIME type that may
+#   be overridden in a subclass, or by the use of a "type" property as is
+#   done in the default templates.
+# - the regeneration of the indexes (if necessary) is done once the schema is
+#   set up in the dbinit.
+#
 # Revision 1.21  2002/05/24 04:03:23  richard
 # Added commentage to the dbinit files to help people with their
 # customisation.
index 3b9c9baa68a169fd021fc2cd2382bda1e7932333..e8a5b6b198e6426de01e7bb1c9e8754a312529b7 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_db.py,v 1.26 2002-07-11 01:11:03 richard Exp $ 
+# $Id: test_db.py,v 1.27 2002-07-14 02:05:54 richard Exp $ 
 
 import unittest, os, shutil
 
 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
-    Interval, DatabaseError, Class
-from roundup.roundupdb import FileClass
+    Interval, DatabaseError
 from roundup import date, password
 from roundup.indexer import Indexer
 
-def setupSchema(db, create, Class, FileClass):
-    status = Class(db, "status", name=String())
+def setupSchema(db, create, module):
+    status = module.Class(db, "status", name=String())
     status.setkey("name")
-    user = Class(db, "user", username=String(), password=Password())
-    file = FileClass(db, "file", name=String(), type=String(),
+    user = module.Class(db, "user", username=String(), password=Password())
+    file = module.FileClass(db, "file", name=String(), type=String(),
         comment=String(indexme="yes"))
-    issue = Class(db, "issue", title=String(indexme="yes"),
+    issue = module.IssueClass(db, "issue", title=String(indexme="yes"),
         status=Link("status"), nosy=Multilink("user"), deadline=Date(),
         foo=Interval(), files=Multilink("file"))
     db.post_init()
@@ -69,9 +68,9 @@ class anydbmDBTestCase(MyTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         self.db = anydbm.Database(config, 'test')
-        setupSchema(self.db, 1, Class, FileClass)
+        setupSchema(self.db, 1, anydbm)
         self.db2 = anydbm.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, anydbm)
 
     def testStringChange(self):
         self.db.issue.create(title="spam", status='1')
@@ -117,8 +116,9 @@ class anydbmDBTestCase(MyTestCase):
         props = self.db.issue.getprops()
         keys = props.keys()
         keys.sort()
-        self.assertEqual(keys, ['deadline', 'files', 'fixer', 'foo', 'id',
-            'nosy', 'status', 'title'])
+        self.assertEqual(keys, ['activity', 'creation', 'creator', 'deadline',
+            'files', 'fixer', 'foo', 'id', 'messages', 'nosy', 'status',
+            'superseder', 'title'])
         self.assertEqual(self.db.issue.get('1', "fixer"), None)
 
     def testRetire(self):
@@ -251,8 +251,8 @@ class anydbmDBTestCase(MyTestCase):
         self.assertEqual(action, 'create')
         keys = params.keys()
         keys.sort()
-        self.assertEqual(keys, ['deadline', 'files', 'fixer', 'foo', 'nosy', 
-            'status', 'title'])
+        self.assertEqual(keys, ['deadline', 'files', 'fixer', 'foo',
+            'messages', 'nosy', 'status', 'superseder', 'title'])
         self.assertEqual(None,params['deadline'])
         self.assertEqual(None,params['fixer'])
         self.assertEqual(None,params['foo'])
@@ -347,11 +347,11 @@ class anydbmReadOnlyDBTestCase(MyTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         db = anydbm.Database(config, 'test')
-        setupSchema(db, 1, Class, FileClass)
+        setupSchema(db, 1, anydbm)
         self.db = anydbm.Database(config)
-        setupSchema(self.db, 0, Class, FileClass)
+        setupSchema(self.db, 0, anydbm)
         self.db2 = anydbm.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, anydbm)
 
     def testExceptions(self):
         ' make sure exceptions are raised on writes to a read-only db '
@@ -372,9 +372,9 @@ class bsddbDBTestCase(anydbmDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         self.db = bsddb.Database(config, 'test')
-        setupSchema(self.db, 1, Class, FileClass)
+        setupSchema(self.db, 1, bsddb)
         self.db2 = bsddb.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, bsddb)
 
 class bsddbReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
     def setUp(self):
@@ -384,11 +384,11 @@ class bsddbReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         db = bsddb.Database(config, 'test')
-        setupSchema(db, 1, Class, FileClass)
+        setupSchema(db, 1, bsddb)
         self.db = bsddb.Database(config)
-        setupSchema(self.db, 0, Class, FileClass)
+        setupSchema(self.db, 0, bsddb)
         self.db2 = bsddb.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, bsddb)
 
 
 class bsddb3DBTestCase(anydbmDBTestCase):
@@ -399,9 +399,9 @@ class bsddb3DBTestCase(anydbmDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         self.db = bsddb3.Database(config, 'test')
-        setupSchema(self.db, 1, Class, FileClass)
+        setupSchema(self.db, 1, bsddb3)
         self.db2 = bsddb3.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, bsddb3)
 
 class bsddb3ReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
     def setUp(self):
@@ -411,11 +411,11 @@ class bsddb3ReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         db = bsddb3.Database(config, 'test')
-        setupSchema(db, 1, Class, FileClass)
+        setupSchema(db, 1, bsddb3)
         self.db = bsddb3.Database(config)
-        setupSchema(self.db, 0, Class, FileClass)
+        setupSchema(self.db, 0, bsddb3)
         self.db2 = bsddb3.Database(config, 'test')
-        setupSchema(self.db2, 0, Class, FileClass)
+        setupSchema(self.db2, 0, bsddb3)
 
 
 class metakitDBTestCase(anydbmDBTestCase):
@@ -428,9 +428,9 @@ class metakitDBTestCase(anydbmDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         self.db = metakit.Database(config, 'test')
-        setupSchema(self.db, 1, metakit.Class, metakit.FileClass)
+        setupSchema(self.db, 1, metakit)
         self.db2 = metakit.Database(config, 'test')
-        setupSchema(self.db2, 0, metakit.Class, metakit.FileClass)
+        setupSchema(self.db2, 0, metakit)
 
     def testTransactions(self):
         # remember the number of items we started
@@ -480,11 +480,11 @@ class metakitReadOnlyDBTestCase(anydbmReadOnlyDBTestCase):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
         db = metakit.Database(config, 'test')
-        setupSchema(db, 1, metakit.Class, metakit.FileClass)
+        setupSchema(db, 1, metakit)
         self.db = metakit.Database(config)
-        setupSchema(self.db, 0, metakit.Class, metakit.FileClass)
+        setupSchema(self.db, 0, metakit)
         self.db2 = metakit.Database(config, 'test')
-        setupSchema(self.db2, 0, metakit.Class, metakit.FileClass)
+        setupSchema(self.db2, 0, metakit)
 
 def suite():
     l = [
@@ -517,6 +517,10 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.26  2002/07/11 01:11:03  richard
+# Added metakit backend to the db tests and fixed the more easily fixable test
+# failures.
+#
 # Revision 1.25  2002/07/09 04:19:09  richard
 # Added reindex command to roundup-admin.
 # Fixed reindex on first access.
index 09b3029777319fa7364d8a867960422e1426d652..1a9520ddf9b28a4e9ab64c06e24ebe417026d509 100644 (file)
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_init.py,v 1.12 2002-07-11 01:13:13 richard Exp $
+# $Id: test_init.py,v 1.13 2002-07-14 02:05:54 richard Exp $
 
 import unittest, os, shutil, errno, imp, sys
 
@@ -125,8 +125,11 @@ class metakitExtendedTestCase(ExtendedTestCase):
     backend = 'metakit'
 
 def suite():
-    l = [unittest.makeSuite(ClassicTestCase, 'test'),
-         unittest.makeSuite(ExtendedTestCase, 'test')]
+    l = [
+        unittest.makeSuite(ClassicTestCase, 'test'),
+        unittest.makeSuite(ExtendedTestCase, 'test')
+    ]
+
     try:
         import bsddb
         l.append(unittest.makeSuite(bsddbClassicTestCase, 'test'))
@@ -152,6 +155,9 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.12  2002/07/11 01:13:13  richard
+# *** empty log message ***
+#
 # Revision 1.11  2002/07/11 01:12:34  richard
 # Forgot to add to init tests
 #
index 88ac024344a63b0172f7c39b9c67d1c14dd69064..89a78ce7bb56915eab8b944f9d41f10c2465343b 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_schema.py,v 1.7 2002-01-14 02:20:15 richard Exp $ 
+# $Id: test_schema.py,v 1.8 2002-07-14 02:05:54 richard Exp $ 
 
 import unittest, os, shutil
 
-from roundup.backends import anydbm
+from roundup.backends import back_anydbm
 from roundup.hyperdb import String, Password, Link, Multilink, Date, \
-    Interval, Class
+    Interval
 
 class config:
     DATABASE='_test_dir'
@@ -39,20 +39,18 @@ class config:
 
 class SchemaTestCase(unittest.TestCase):
     def setUp(self):
-        class Database(anydbm.Database):
-            pass
         # remove previous test, ignore errors
         if os.path.exists(config.DATABASE):
             shutil.rmtree(config.DATABASE)
         os.makedirs(config.DATABASE + '/files')
-        self.db = Database(config, 'test')
+        self.db = back_anydbm.Database(config, 'test')
         self.db.clear()
 
     def tearDown(self):
         shutil.rmtree('_test_dir')
 
     def testA_Status(self):
-        status = Class(self.db, "status", name=String())
+        status = back_anydbm.Class(self.db, "status", name=String())
         self.assert_(status, 'no class object generated')
         status.setkey("name")
         val = status.create(name="unread")
@@ -74,11 +72,13 @@ class SchemaTestCase(unittest.TestCase):
         self.assertEqual(val, ['1', '2', '4'], 'blah')
 
     def testB_Issue(self):
-        issue = Class(self.db, "issue", title=String(), status=Link("status"))
+        issue = back_anydbm.Class(self.db, "issue", title=String(),
+            status=Link("status"))
         self.assert_(issue, 'no class object returned')
 
     def testC_User(self):
-        user = Class(self.db, "user", username=String(), password=Password())
+        user = back_anydbm.Class(self.db, "user", username=String(),
+            password=Password())
         self.assert_(user, 'no class object returned')
         user.setkey("username")
 
@@ -89,6 +89,15 @@ def suite():
 
 #
 # $Log: not supported by cvs2svn $
+# Revision 1.7  2002/01/14 02:20:15  richard
+#  . changed all config accesses so they access either the instance or the
+#    config attriubute on the db. This means that all config is obtained from
+#    instance_config instead of the mish-mash of classes. This will make
+#    switching to a ConfigParser setup easier too, I hope.
+#
+# At a minimum, this makes migration a _little_ easier (a lot easier in the
+# 0.5.0 switch, I hope!)
+#
 # Revision 1.6  2001/12/03 21:33:39  richard
 # Fixes so the tests use commit and not close
 #