summary | shortlog | log | commit | commitdiff | tree
raw | patch | inline | side by side (parent: 7bf392a)
raw | patch | inline | side by side (parent: 7bf392a)
author | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Thu, 22 Aug 2002 07:56:51 +0000 (07:56 +0000) | ||
committer | richard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2> | |
Thu, 22 Aug 2002 07:56:51 +0000 (07:56 +0000) |
it a little bit :)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@985 57a73879-2fb5-44c3-a270-3262357dd7e2
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@985 57a73879-2fb5-44c3-a270-3262357dd7e2
roundup/backends/__init__.py | patch | blob | history | |
roundup/backends/back_gadfly.py | [new file with mode: 0644] | patch | blob |
index 1e0348b819096543a0b553b849948b41fbed00f3..8ec7fba00368c0af94f04b666284c4112cbc4611 100644 (file)
# BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#
-# $Id: __init__.py,v 1.13 2002-07-11 01:11:03 richard Exp $
+# $Id: __init__.py,v 1.14 2002-08-22 07:56:51 richard Exp $
__all__ = []
anydbm = back_anydbm
__all__.append('anydbm')
+try:
+ import gadfly
+except ImportError, message:
+ if str(message) != 'No module named gadfly': raise
+else:
+ import back_gadfly
+ bsddb = back_gadfly
+ __all__.append('gadfly')
+
try:
import bsddb
except ImportError, message:
#
# $Log: not supported by cvs2svn $
+# Revision 1.13 2002/07/11 01:11:03 richard
+# Added metakit backend to the db tests and fixed the more easily fixable test
+# failures.
+#
# Revision 1.12 2002/05/22 00:32:33 richard
# . changed the default message list in issues to display the message body
# . made backends.__init__ be more specific about which ImportErrors it really
diff --git a/roundup/backends/back_gadfly.py b/roundup/backends/back_gadfly.py
--- /dev/null
@@ -0,0 +1,1623 @@
+# $Id: back_gadfly.py,v 1.1 2002-08-22 07:56:51 richard Exp $
+__doc__ = '''
+About Gadfly
+============
+
+Gadfly is a collection of python modules that provides relational
+database functionality entirely implemented in Python. It supports a
+subset of the intergalactic standard RDBMS Structured Query Language
+SQL.
+
+
+Basic Structure
+===============
+
+We map roundup classes to relational tables. Automatically detect schema
+changes and modify the gadfly table schemas appropriately. Multilinks
+(which represent a many-to-many relationship) are handled through
+intermediate tables.
+
+Journals are stored adjunct to the per-class tables.
+
+Table columns for properties have "_" prepended so the names can't
+clash with restricted names (like "order"). Retirement is determined by the
+__retired__ column being true.
+
+All columns are defined as VARCHAR, since it really doesn't matter what
+type they're defined as. We stuff all kinds of data in there ;) [as long as
+it's marshallable, gadfly doesn't care]
+
+
+Additional Instance Requirements
+================================
+
+The instance configuration must specify where the database is. It does this
+with GADFLY_DATABASE, which is used as the arguments to the gadfly.gadfly()
+method:
+
+Using an on-disk database directly (not a good idea):
+ GADFLY_DATABASE = (database name, directory)
+
+Using a network database (much better idea):
+ GADFLY_DATABASE = (policy, password, address, port)
+
+Because multiple accesses directly to a gadfly database aren't handled, but
+multiple network accesses are, it's strongly advised that the latter setup be
+used.
+
+'''
+
+# standard python modules
+import sys, os, time, re, errno, weakref
+
+# roundup modules
+from roundup import hyperdb, date, password, roundupdb, security
+from roundup.hyperdb import String, Password, Date, Interval, Link, \
+ Multilink, DatabaseError, Boolean, Number
+
+# the all-important gadfly :)
+import gadfly
+from gadfly import client
+
+# support
+from blobfiles import FileStorage
+from roundup.indexer import Indexer
+from sessions import Sessions
+
+class Database(FileStorage, hyperdb.Database, roundupdb.Database):
+ # flag to set on retired entries
+ RETIRED_FLAG = '__hyperdb_retired'
+
+ def __init__(self, config, journaltag=None):
+ ''' Open the database and load the schema from it.
+ '''
+ self.config, self.journaltag = config, journaltag
+ self.dir = config.DATABASE
+ self.classes = {}
+ self.indexer = Indexer(self.dir)
+ self.sessions = Sessions(self.config)
+ self.security = security.Security(self)
+
+ db = config.GADFLY_DATABASE
+ if len(db) == 2:
+ # ensure files are group readable and writable
+ os.umask(0002)
+ try:
+ self.conn = gadfly.gadfly(*db)
+ except IOError, error:
+ if error.errno != errno.ENOENT:
+ raise
+ self.database_schema = {}
+ self.conn = gadfly.gadfly()
+ self.conn.startup(*db)
+ cursor = self.conn.cursor()
+ cursor.execute('create table schema (schema varchar)')
+ cursor.execute('create table ids (name varchar, num integer)')
+ else:
+ cursor = self.conn.cursor()
+ cursor.execute('select schema from schema')
+ self.database_schema = cursor.fetchone()[0]
+ else:
+ self.conn = client.gfclient(*db)
+ cursor = self.conn.cursor()
+ cursor.execute('select schema from schema')
+ self.database_schema = cursor.fetchone()[0]
+
+ def post_init(self):
+ ''' Called once the schema initialisation has finished.
+
+ We should now confirm that the schema defined by our "classes"
+ attribute actually matches the schema in the database.
+ '''
+ for classname, spec in self.classes.items():
+ if self.database_schema.has_key(classname):
+ dbspec = self.database_schema[classname]
+ self.update_class(spec.schema(), dbspec)
+ self.database_schema[classname] = dbspec
+ else:
+ self.create_class(spec)
+ self.database_schema[classname] = spec.schema()
+
+ for classname in self.database_schema.keys():
+ if not self.classes.has_key(classname):
+ self.drop_class(classname)
+
+ # commit any changes
+ cursor = self.conn.cursor()
+ cursor.execute('delete from schema')
+ cursor.execute('insert into schema values (?)', (self.database_schema,))
+ self.conn.commit()
+
+ def determine_columns(self, spec):
+ ''' Figure the column names and multilink properties from the spec
+ '''
+ cols = []
+ mls = []
+ # add the multilinks separately
+ for col, prop in spec.properties.items():
+ if isinstance(prop, Multilink):
+ mls.append(col)
+ else:
+ cols.append('_'+col)
+ cols.sort()
+ return cols, mls
+
+ def update_class(self, spec, dbspec):
+ ''' Determine the differences between the current spec and the
+ database version of the spec, and update where necessary
+ '''
+ if spec == dbspec:
+ return
+ raise NotImplementedError
+
+ def create_class(self, spec):
+ ''' Create a database table according to the given spec.
+ '''
+ cols, mls = self.determine_columns(spec)
+
+ # add on our special columns
+ cols.append('id')
+ cols.append('__retired__')
+
+ cursor = self.conn.cursor()
+
+ # create the base table
+ cols = ','.join(['%s varchar'%x for x in cols])
+ sql = 'create table %s (%s)'%(spec.classname, cols)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'create_class', (self, sql)
+ cursor.execute(sql)
+
+ # journal table
+ cols = ','.join(['%s varchar'%x
+ for x in 'nodeid date tag action params'.split()])
+ sql = 'create table %s__journal (%s)'%(spec.classname, cols)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'create_class', (self, sql)
+ cursor.execute(sql)
+
+ # now create the multilink tables
+ for ml in mls:
+ sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
+ spec.classname, ml)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'create_class', (self, sql)
+ cursor.execute(sql)
+
+ # ID counter
+ sql = 'insert into ids (name, num) values (?,?)'
+ vals = (spec.classname, 1)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ def drop_class(self, spec):
+ ''' Drop the given table from the database.
+
+ Drop the journal and multilink tables too.
+ '''
+ # figure the multilinks
+ mls = []
+ for col, prop in spec.properties.items():
+ if isinstance(prop, Multilink):
+ mls.append(col)
+ cursor = self.conn.cursor()
+
+ sql = 'drop table %s'%spec.classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'drop_class', (self, sql)
+ cursor.execute(sql)
+
+ sql = 'drop table %s__journal'%spec.classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'drop_class', (self, sql)
+ cursor.execute(sql)
+
+ for ml in mls:
+ sql = 'drop table %s_%s'%(spec.classname, ml)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'drop_class', (self, sql)
+ cursor.execute(sql)
+
+ #
+ # Classes
+ #
+ def __getattr__(self, classname):
+ ''' A convenient way of calling self.getclass(classname).
+ '''
+ if self.classes.has_key(classname):
+ if __debug__:
+ print >>hyperdb.DEBUG, '__getattr__', (self, classname)
+ return self.classes[classname]
+ raise AttributeError, classname
+
+ def addclass(self, cl):
+ ''' Add a Class to the hyperdatabase.
+ '''
+ if __debug__:
+ print >>hyperdb.DEBUG, 'addclass', (self, cl)
+ cn = cl.classname
+ if self.classes.has_key(cn):
+ raise ValueError, cn
+ self.classes[cn] = cl
+
+ def getclasses(self):
+ ''' Return a list of the names of all existing classes.
+ '''
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getclasses', (self,)
+ l = self.classes.keys()
+ l.sort()
+ return l
+
+ def getclass(self, classname):
+ '''Get the Class object representing a particular class.
+
+ If 'classname' is not a valid class name, a KeyError is raised.
+ '''
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getclass', (self, classname)
+ return self.classes[classname]
+
+ def clear(self):
+ ''' Delete all database contents.
+
+ Note: I don't commit here, which is different behaviour to the
+ "nuke from orbit" behaviour in the *dbms.
+ '''
+ if __debug__:
+ print >>hyperdb.DEBUG, 'clear', (self,)
+ cursor = self.conn.cursor()
+ for cn in self.classes.keys():
+ sql = 'delete from %s'%cn
+ if __debug__:
+ print >>hyperdb.DEBUG, 'clear', (self, sql)
+ cursor.execute(sql)
+
+ #
+ # Node IDs
+ #
+ def newid(self, classname):
+ ''' Generate a new id for the given class
+ '''
+ # get the next ID
+ cursor = self.conn.cursor()
+ sql = 'select num from ids where name=?'
+ if __debug__:
+ print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
+ cursor.execute(sql, (classname, ))
+ newid = cursor.fetchone()[0]
+
+ # update the counter
+ sql = 'update ids set num=? where name=?'
+ vals = (newid+1, classname)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ # return as string
+ return str(newid)
+
+ def setid(self, classname, setid):
+ ''' Set the id counter: used during import of database
+ '''
+ cursor = self.conn.cursor()
+ sql = 'update ids set num=? where name=?'
+ vals = (setid, spec.classname)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ #
+ # Nodes
+ #
+
+ def addnode(self, classname, nodeid, node):
+ ''' Add the specified node to its class's db.
+ '''
+ # gadfly requires values for all non-multilink columns
+ cl = self.classes[classname]
+ cols, mls = self.determine_columns(cl)
+
+ # default the non-multilink columns
+ for col, prop in cl.properties.items():
+ if not isinstance(col, Multilink):
+ if not node.has_key(col):
+ node[col] = None
+
+ node = self.serialise(classname, node)
+
+ # make sure the ordering is correct for column name -> column value
+ vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
+ s = ','.join(['?' for x in cols]) + ',?,?'
+ cols = ','.join(cols) + ',id,__retired__'
+
+ # perform the inserts
+ cursor = self.conn.cursor()
+ sql = 'insert into %s (%s) values (%s)'%(classname, cols, s)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ # insert the multilink rows
+ for col in mls:
+ t = '%s_%s'%(classname, col)
+ for entry in node[col]:
+ sql = 'insert into %s (linkid, nodeid) values (?,?)'%t
+ vals = (entry, nodeid)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ def setnode(self, classname, nodeid, node):
+ ''' Change the specified node.
+ '''
+ node = self.serialise(classname, node)
+
+ cl = self.classes[classname]
+ cols = []
+ mls = []
+ # add the multilinks separately
+ for col in node.keys():
+ prop = cl.properties[col]
+ if isinstance(prop, Multilink):
+ mls.append(col)
+ else:
+ cols.append('_'+col)
+ cols.sort()
+
+ # make sure the ordering is correct for column name -> column value
+ vals = tuple([node[col[1:]] for col in cols])
+ s = ','.join(['?' for x in cols])
+ cols = ','.join(cols)
+
+ # perform the update
+ cursor = self.conn.cursor()
+ sql = 'update %s (%s) values (%s)'%(classname, cols, s)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'setnode', (self, sql, vals)
+ cursor.execute(sql, vals)
+
+ # now the fun bit, updating the multilinks ;)
+ # XXX TODO XXX
+
+ def getnode(self, classname, nodeid):
+ ''' Get a node from the database.
+ '''
+ # figure the columns we're fetching
+ cl = self.classes[classname]
+ cols, mls = self.determine_columns(cl)
+ cols = ','.join(cols)
+
+ # perform the basic property fetch
+ cursor = self.conn.cursor()
+ sql = 'select %s from %s where id=?'%(cols, classname)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getnode', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+ values = cursor.fetchone()
+
+ # make up the node
+ node = {}
+ for col in range(len(cols)):
+ node[col] = values[col]
+
+ # now the multilinks
+ for col in mls:
+ # get the link ids
+ sql = 'select linkid from %s_%s where nodeid=?'%(classname, col)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getnode', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+ # extract the first column from the result
+ node[col] = [x[0] for x in cursor.fetchall()]
+
+ return self.unserialise(classname, node)
+
+ 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.serialise()
+ elif isinstance(prop, Interval) and v is not None:
+ d[k] = v.serialise()
+ 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):
+ ''' Determine if the database has a given node.
+ '''
+ cursor = self.conn.cursor()
+ sql = 'select count(*) from %s where nodeid=?'%classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+ return cursor.fetchone()[0]
+
+ def countnodes(self, classname):
+ ''' Count the number of nodes that exist for a particular Class.
+ '''
+ cursor = self.conn.cursor()
+ sql = 'select count(*) from %s'%classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'countnodes', (self, sql)
+ cursor.execute(sql)
+ return cursor.fetchone()[0]
+
+ def getnodeids(self, classname):
+ ''' Retrieve all the ids of the nodes for a particular Class.
+ '''
+ cursor = self.conn.cursor()
+ sql = 'select id from %s'%classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getnodeids', (self, sql)
+ cursor.execute(sql)
+ return [x[0] for x in cursor.fetchall()]
+
+ def addjournal(self, classname, nodeid, action, params):
+ ''' Journal the Action
+ 'action' may be:
+
+ 'create' or 'set' -- 'params' is a dictionary of property values
+ 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
+ 'retire' -- 'params' is None
+ '''
+ if isinstance(params, type({})):
+ if params.has_key('creator'):
+ journaltag = self.user.get(params['creator'], 'username')
+ del params['creator']
+ else:
+ journaltag = self.journaltag
+ if params.has_key('created'):
+ journaldate = params['created'].serialise()
+ del params['created']
+ else:
+ journaldate = date.Date().serialise()
+ if params.has_key('activity'):
+ del params['activity']
+
+ # serialise the parameters now
+ if action in ('set', 'create'):
+ params = self.serialise(classname, params)
+ else:
+ journaltag = self.journaltag
+ journaldate = date.Date().serialise()
+
+ # create the journal entry
+ cols = ','.join('nodeid date tag action params'.split())
+ entry = (nodeid, journaldate, journaltag, action, params)
+
+ if __debug__:
+ print >>hyperdb.DEBUG, 'doSaveJournal', entry
+
+ # do the insert
+ cursor = self.conn.cursor()
+ sql = 'insert into %s__journal (%s) values (?,?,?,?,?)'%(classname,
+ cols)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
+ cursor.execute(sql, entry)
+
+ def getjournal(self, classname, nodeid):
+ ''' get the journal for id
+ '''
+ cols = ','.join('nodeid date tag action params'.split())
+ cursor = self.conn.cursor()
+ sql = 'select %s from %s__journal where nodeid=?'%(cols, classname)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'getjournal', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+ res = []
+ for nodeid, date_stamp, user, action, params in cursor.fetchall():
+ res.append((nodeid, date.Date(date_stamp), user, action, params))
+ return res
+
+ def pack(self, pack_before):
+ ''' Pack the database, removing all journal entries before the
+ "pack_before" date.
+ '''
+ # get a 'yyyymmddhhmmss' version of the date
+ date_stamp = pack_before.serialise()
+
+ # do the delete
+ cursor = self.conn.cursor()
+ for classname in self.classes.keys():
+ sql = 'delete from %s__journal where date<?'%classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
+ cursor.execute(sql, (date_stamp,))
+
+ def commit(self):
+ ''' Commit the current transactions.
+
+ Save all data changed since the database was opened or since the
+ last commit() or rollback().
+ '''
+ self.conn.commit()
+
+ def rollback(self):
+ ''' Reverse all actions from the current transaction.
+
+ Undo all the changes made since the database was opened or the last
+ commit() or rollback() was performed.
+ '''
+ self.conn.rollback()
+
+#
+# The base Class class
+#
+class Class(hyperdb.Class):
+ ''' 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.
+
+ '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 = ''
+
+ # should we journal changes (default yes)
+ self.do_journal = 1
+
+ # do the db-related init stuff
+ db.addclass(self)
+
+ self.auditors = {'create': [], 'set': [], 'retire': []}
+ self.reactors = {'create': [], 'set': [], 'retire': []}
+
+ def schema(self):
+ ''' A dumpable version of the schema that we can store in the
+ database
+ '''
+ return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
+
+ def enableJournalling(self):
+ '''Turn journalling on for this class
+ '''
+ self.do_journal = 1
+
+ def disableJournalling(self):
+ '''Turn journalling off for this class
+ '''
+ self.do_journal = 0
+
+ # 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.
+ '''
+ 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 value is not None and 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.getclass(link_class).hasnode(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.do_journal and 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 nodeid in value:
+ if not self.db.getclass(link_class).hasnode(nodeid):
+ raise IndexError, '%s has no node %s'%(link_class,
+ nodeid)
+ # register the link with the newly linked node
+ if self.do_journal and self.properties[key].do_journal:
+ self.db.addjournal(link_class, nodeid, '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
+
+ elif value is not None and isinstance(prop, Number):
+ try:
+ float(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not numeric'%key
+
+ elif value is not None and isinstance(prop, Boolean):
+ try:
+ int(value)
+ except ValueError:
+ raise TypeError, 'new property "%s" not boolean'%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:
+ propvalues[key] = None
+
+ # done
+ self.db.addnode(self.classname, newid, propvalues)
+ if self.do_journal:
+ self.db.addjournal(self.classname, newid, 'create', propvalues)
+
+ self.fireReactors('create', newid, None)
+
+ return newid
+
+ _marker = []
+ 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.
+ '''
+ if propname == 'id':
+ return nodeid
+
+ if propname == 'creation':
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ 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':
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ 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':
+ if not self.do_journal:
+ raise ValueError, 'Journalling is disabled for this class'
+ journal = self.db.getjournal(self.classname, nodeid)
+ if journal:
+ name = self.db.getjournal(self.classname, nodeid)[0][2]
+ 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:
+ return None
+ else:
+ return default
+
+ return d[propname]
+
+ 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.
+ '''
+ raise NotImplementedError
+
+ 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.
+ '''
+ cursor = self.db.conn.cursor()
+ sql = 'update %s set __retired__=1 where id=?'%self.classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+
+ def is_retired(self, nodeid):
+ '''Return true if the node is rerired
+ '''
+ cursor = self.db.conn.cursor()
+ sql = 'select __retired__ from %s where id=?'%self.classname
+ if __debug__:
+ print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
+ cursor.execute(sql, (nodeid,))
+ return cursor.fetchone()[0]
+
+ def destroy(self, nodeid):
+ '''Destroy a node.
+
+ WARNING: this method should never be used except in extremely rare
+ situations where there could never be links to the node being
+ deleted
+ WARNING: use retire() instead
+ WARNING: the properties of this node will not be available ever again
+ WARNING: really, use retire() instead
+
+ Well, I think that's enough warnings. This method exists mostly to
+ support the session storage of the cgi interface.
+
+ The node is completely removed from the hyperdb, including all journal
+ entries. It will no longer be available, and will generally break code
+ if there are any references to the node.
+ '''
+ raise NotImplementedError
+
+ 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.
+ '''
+ raise NotImplementedError
+
+ # 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.
+ '''
+ # XXX create an index on the key prop column
+ prop = self.getprops()[propname]
+ if not isinstance(prop, String):
+ raise TypeError, 'key properties must be 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
+ '''
+ raise NotImplementedError
+
+ 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.
+ '''
+ if not self.key:
+ raise TypeError, 'No key property set'
+
+ cursor = self.db.conn.cursor()
+ sql = 'select id from %s where _%s=?'%(self.classname, self.key)
+ if __debug__:
+ print >>hyperdb.DEBUG, 'lookup', (self, sql, keyvalue)
+ cursor.execute(sql, (keyvalue,))
+
+ # see if there was a result
+ l = cursor.fetchall()
+ if not l:
+ raise KeyError, keyvalue
+
+ # return the id
+ return l[0][0]
+
+ 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})
+ '''
+ raise NotImplementedError
+
+ 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
+ '''
+ raise NotImplementedError
+
+ 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.
+ '''
+ 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:
+ try:
+ value = str(self.get(nodeid, prop))
+ except IndexError:
+ # node no longer exists - entry should be removed
+ self.db.indexer.purge_entry((self.classname, nodeid, prop))
+ else:
+ # and index them under (classname, nodeid, property)
+ self.db.indexer.add_text((self.classname, nodeid, prop),
+ value)
+
+
+ #
+ # Detector interface
+ #
+ def audit(self, event, detector):
+ '''Register a detector
+ '''
+ l = self.auditors[event]
+ if detector not in l:
+ self.auditors[event].append(detector)
+
+ def fireAuditors(self, action, nodeid, newvalues):
+ '''Fire all registered auditors.
+ '''
+ for audit in self.auditors[action]:
+ audit(self.db, self, nodeid, newvalues)
+
+ def react(self, event, detector):
+ '''Register a detector
+ '''
+ l = self.reactors[event]
+ if detector not in l:
+ self.reactors[event].append(detector)
+
+ def fireReactors(self, action, nodeid, oldvalues):
+ '''Fire all registered reactors.
+ '''
+ for react in self.reactors[action]:
+ react(self.db, self, nodeid, oldvalues)
+
+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 import_list(self, propnames, proplist):
+ ''' Trap the "content" property...
+ '''
+ # dupe this list so we don't affect others
+ propnames = propnames[:]
+
+ # extract the "content" property from the proplist
+ i = propnames.index('content')
+ content = proplist[i]
+ del propnames[i]
+ del proplist[i]
+
+ # do the normal import
+ newid = Class.import_list(self, propnames, proplist)
+
+ # save off the "content" file
+ self.db.storefile(self.classname, newid, None, content)
+ return newid
+
+ _marker = []
+ 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.80 2002/08/16 04:28:13 richard
+# added is_retired query to Class
+#
+# Revision 1.79 2002/07/29 23:30:14 richard
+# documentation reorg post-new-security
+#
+# Revision 1.78 2002/07/21 03:26:37 richard
+# Gordon, does this help?
+#
+# Revision 1.77 2002/07/18 11:27:47 richard
+# ws
+#
+# Revision 1.76 2002/07/18 11:17:30 gmcm
+# Add Number and Boolean types to hyperdb.
+# Add conversion cases to web, mail & admin interfaces.
+# Add storage/serialization cases to back_anydbm & back_metakit.
+#
+# Revision 1.75 2002/07/14 02:05:53 richard
+# . all storage-specific code (ie. backend) is now implemented by the backends
+#
+# 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.
+#
+# Revision 1.72 2002/07/09 21:53:38 gmcm
+# Optimize Class.find so that the propspec can contain a set of ids to match.
+# This is used by indexer.search so it can do just one find for all the index matches.
+# This was already confusing code, but for common terms (lots of index matches),
+# it is enormously faster.
+#
+# Revision 1.71 2002/07/09 03:02:52 richard
+# More indexer work:
+# - all String properties may now be indexed too. Currently there's a bit of
+# "issue" specific code in the actual searching which needs to be
+# addressed. In a nutshell:
+# + pass 'indexme="yes"' as a String() property initialisation arg, eg:
+# file = FileClass(db, "file", name=String(), type=String(),
+# comment=String(indexme="yes"))
+# + the comment will then be indexed and be searchable, with the results
+# related back to the issue that the file is linked to
+# - as a result of this work, the FileClass has a default MIME type that may
+# be overridden in a subclass, or by the use of a "type" property as is
+# done in the default templates.
+# - the regeneration of the indexes (if necessary) is done once the schema is
+# set up in the dbinit.
+#
+# Revision 1.70 2002/06/27 12:06:20 gmcm
+# Improve an error message.
+#
+# Revision 1.69 2002/06/17 23:15:29 richard
+# Can debug to stdout now
+#
+# Revision 1.68 2002/06/11 06:52:03 richard
+# . #564271 ] find() and new properties
+#
+# Revision 1.67 2002/06/11 05:02:37 richard
+# . #565979 ] code error in hyperdb.Class.find
+#
+# Revision 1.66 2002/05/25 07:16:24 rochecompaan
+# Merged search_indexing-branch with HEAD
+#
+# Revision 1.65 2002/05/22 04:12:05 richard
+# . applied patch #558876 ] cgi client customization
+# ... with significant additions and modifications ;)
+# - extended handling of ML assignedto to all places it's handled
+# - added more NotFound info
+#
+# Revision 1.64 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.63 2002/04/15 23:25:15 richard
+# . node ids are now generated from a lockable store - no more race conditions
+#
+# We're using the portalocker code by Jonathan Feinberg that was contributed
+# to the ASPN Python cookbook. This gives us locking across Unix and Windows.
+#
+# Revision 1.62 2002/04/03 07:05:50 richard
+# d'oh! killed retirement of nodes :(
+# all better now...
+#
+# Revision 1.61 2002/04/03 06:11:51 richard
+# Fix for old databases that contain properties that don't exist any more.
+#
+# Revision 1.60 2002/04/03 05:54:31 richard
+# Fixed serialisation problem by moving the serialisation step out of the
+# hyperdb.Class (get, set) into the hyperdb.Database.
+#
+# Also fixed htmltemplate after the showid changes I made yesterday.
+#
+# Unit tests for all of the above written.
+#
+# Revision 1.59.2.2 2002/04/20 13:23:33 rochecompaan
+# We now have a separate search page for nodes. Search links for
+# different classes can be customized in instance_config similar to
+# index links.
+#
+# Revision 1.59.2.1 2002/04/19 19:54:42 rochecompaan
+# cgi_client.py
+# removed search link for the time being
+# moved rendering of matches to htmltemplate
+# hyperdb.py
+# filtering of nodes on full text search incorporated in filter method
+# roundupdb.py
+# added paramater to call of filter method
+# roundup_indexer.py
+# added search method to RoundupIndexer class
+#
+# Revision 1.59 2002/03/12 22:52:26 richard
+# more pychecker warnings removed
+#
+# Revision 1.58 2002/02/27 03:23:16 richard
+# Ran it through pychecker, made fixes
+#
+# Revision 1.57 2002/02/20 05:23:24 richard
+# Didn't accomodate new values for new properties
+#
+# Revision 1.56 2002/02/20 05:05:28 richard
+# . Added simple editing for classes that don't define a templated interface.
+# - access using the admin "class list" interface
+# - limited to admin-only
+# - requires the csv module from object-craft (url given if it's missing)
+#
+# Revision 1.55 2002/02/15 07:27:12 richard
+# Oops, precedences around the way w0rng.
+#
+# Revision 1.54 2002/02/15 07:08:44 richard
+# . Alternate email addresses are now available for users. See the MIGRATION
+# file for info on how to activate the feature.
+#
+# Revision 1.53 2002/01/22 07:21:13 richard
+# . fixed back_bsddb so it passed the journal tests
+#
+# ... it didn't seem happy using the back_anydbm _open method, which is odd.
+# Yet another occurrance of whichdb not being able to recognise older bsddb
+# databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
+# process.
+#
+# Revision 1.52 2002/01/21 16:33:19 rochecompaan
+# You can now use the roundup-admin tool to pack the database
+#
+# Revision 1.51 2002/01/21 03:01:29 richard
+# brief docco on the do_journal argument
+#
+# Revision 1.50 2002/01/19 13:16:04 rochecompaan
+# Journal entries for link and multilink properties can now be switched on
+# or off.
+#
+# Revision 1.49 2002/01/16 07:02:57 richard
+# . lots of date/interval related changes:
+# - more relaxed date format for input
+#
+# Revision 1.48 2002/01/14 06:32:34 richard
+# . #502951 ] adding new properties to old database
+#
+# Revision 1.47 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.46 2002/01/07 10:42:23 richard
+# oops
+#
+# Revision 1.45 2002/01/02 04:18:17 richard
+# hyperdb docstrings
+#
+# Revision 1.44 2002/01/02 02:31:38 richard
+# Sorry for the huge checkin message - I was only intending to implement #496356
+# but I found a number of places where things had been broken by transactions:
+# . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
+# for _all_ roundup-generated smtp messages to be sent to.
+# . the transaction cache had broken the roundupdb.Class set() reactors
+# . newly-created author users in the mailgw weren't being committed to the db
+#
+# Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
+# on when I found that stuff :):
+# . #496356 ] Use threading in messages
+# . detectors were being registered multiple times
+# . added tests for mailgw
+# . much better attaching of erroneous messages in the mail gateway
+#
+# Revision 1.43 2001/12/20 06:13:24 rochecompaan
+# Bugs fixed:
+# . Exception handling in hyperdb for strings-that-look-like numbers got
+# lost somewhere
+# . Internet Explorer submits full path for filename - we now strip away
+# the path
+# Features added:
+# . Link and multilink properties are now displayed sorted in the cgi
+# interface
+#
+# Revision 1.42 2001/12/16 10:53:37 richard
+# take a copy of the node dict so that the subsequent set
+# operation doesn't modify the oldvalues structure
+#
+# Revision 1.41 2001/12/15 23:47:47 richard
+# Cleaned up some bare except statements
+#
+# Revision 1.40 2001/12/14 23:42:57 richard
+# yuck, a gdbm instance tests false :(
+# I've left the debugging code in - it should be removed one day if we're ever
+# _really_ anal about performace :)
+#
+# Revision 1.39 2001/12/02 05:06:16 richard
+# . We now use weakrefs in the Classes to keep the database reference, so
+# the close() method on the database is no longer needed.
+# I bumped the minimum python requirement up to 2.1 accordingly.
+# . #487480 ] roundup-server
+# . #487476 ] INSTALL.txt
+#
+# I also cleaned up the change message / post-edit stuff in the cgi client.
+# There's now a clearly marked "TODO: append the change note" where I believe
+# the change note should be added there. The "changes" list will obviously
+# have to be modified to be a dict of the changes, or somesuch.
+#
+# More testing needed.
+#
+# Revision 1.38 2001/12/01 07:17:50 richard
+# . We now have basic transaction support! Information is only written to
+# the database when the commit() method is called. Only the anydbm
+# backend is modified in this way - neither of the bsddb backends have been.
+# The mail, admin and cgi interfaces all use commit (except the admin tool
+# doesn't have a commit command, so interactive users can't commit...)
+# . Fixed login/registration forwarding the user to the right page (or not,
+# on a failure)
+#
+# Revision 1.37 2001/11/28 21:55:35 richard
+# . login_action and newuser_action return values were being ignored
+# . Woohoo! Found that bloody re-login bug that was killing the mail
+# gateway.
+# (also a minor cleanup in hyperdb)
+#
+# Revision 1.36 2001/11/27 03:16:09 richard
+# Another place that wasn't handling missing properties.
+#
+# Revision 1.35 2001/11/22 15:46:42 jhermann
+# Added module docstrings to all modules.
+#
+# Revision 1.34 2001/11/21 04:04:43 richard
+# *sigh* more missing value handling
+#
+# Revision 1.33 2001/11/21 03:40:54 richard
+# more new property handling
+#
+# Revision 1.32 2001/11/21 03:11:28 richard
+# Better handling of new properties.
+#
+# Revision 1.31 2001/11/12 22:01:06 richard
+# Fixed issues with nosy reaction and author copies.
+#
+# Revision 1.30 2001/11/09 10:11:08 richard
+# . roundup-admin now handles all hyperdb exceptions
+#
+# Revision 1.29 2001/10/27 00:17:41 richard
+# Made Class.stringFind() do caseless matching.
+#
+# Revision 1.28 2001/10/21 04:44:50 richard
+# bug #473124: UI inconsistency with Link fields.
+# This also prompted me to fix a fairly long-standing usability issue -
+# that of being able to turn off certain filters.
+#
+# Revision 1.27 2001/10/20 23:44:27 richard
+# Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
+#
+# Revision 1.26 2001/10/16 03:48:01 richard
+# admin tool now complains if a "find" is attempted with a non-link property.
+#
+# Revision 1.25 2001/10/11 00:17:51 richard
+# Reverted a change in hyperdb so the default value for missing property
+# values in a create() is None and not '' (the empty string.) This obviously
+# breaks CSV import/export - the string 'None' will be created in an
+# export/import operation.
+#
+# Revision 1.24 2001/10/10 03:54:57 richard
+# Added database importing and exporting through CSV files.
+# Uses the csv module from object-craft for exporting if it's available.
+# Requires the csv module for importing.
+#
+# Revision 1.23 2001/10/09 23:58:10 richard
+# Moved the data stringification up into the hyperdb.Class class' get, set
+# and create methods. This means that the data is also stringified for the
+# journal call, and removes duplication of code from the backends. The
+# backend code now only sees strings.
+#
+# Revision 1.22 2001/10/09 07:25:59 richard
+# Added the Password property type. See "pydoc roundup.password" for
+# implementation details. Have updated some of the documentation too.
+#
+# Revision 1.21 2001/10/05 02:23:24 richard
+# . roundup-admin create now prompts for property info if none is supplied
+# on the command-line.
+# . hyperdb Class getprops() method may now return only the mutable
+# properties.
+# . Login now uses cookies, which makes it a whole lot more flexible. We can
+# now support anonymous user access (read-only, unless there's an
+# "anonymous" user, in which case write access is permitted). Login
+# handling has been moved into cgi_client.Client.main()
+# . The "extended" schema is now the default in roundup init.
+# . The schemas have had their page headings modified to cope with the new
+# login handling. Existing installations should copy the interfaces.py
+# file from the roundup lib directory to their instance home.
+# . Incorrectly had a Bizar Software copyright on the cgitb.py module from
+# Ping - has been removed.
+# . Fixed a whole bunch of places in the CGI interface where we should have
+# been returning Not Found instead of throwing an exception.
+# . Fixed a deviation from the spec: trying to modify the 'id' property of
+# an item now throws an exception.
+#
+# Revision 1.20 2001/10/04 02:12:42 richard
+# Added nicer command-line item adding: passing no arguments will enter an
+# interactive more which asks for each property in turn. While I was at it, I
+# fixed an implementation problem WRT the spec - I wasn't raising a
+# ValueError if the key property was missing from a create(). Also added a
+# protected=boolean argument to getprops() so we can list only the mutable
+# properties (defaults to yes, which lists the immutables).
+#
+# Revision 1.19 2001/08/29 04:47:18 richard
+# Fixed CGI client change messages so they actually include the properties
+# changed (again).
+#
+# Revision 1.18 2001/08/16 07:34:59 richard
+# better CGI text searching - but hidden filter fields are disappearing...
+#
+# Revision 1.17 2001/08/16 06:59:58 richard
+# all searches use re now - and they're all case insensitive
+#
+# Revision 1.16 2001/08/15 23:43:18 richard
+# Fixed some isFooTypes that I missed.
+# Refactored some code in the CGI code.
+#
+# Revision 1.15 2001/08/12 06:32:36 richard
+# using isinstance(blah, Foo) now instead of isFooType
+#
+# Revision 1.14 2001/08/07 00:24:42 richard
+# stupid typo
+#
+# Revision 1.13 2001/08/07 00:15:51 richard
+# Added the copyright/license notice to (nearly) all files at request of
+# Bizar Software.
+#
+# Revision 1.12 2001/08/02 06:38:17 richard
+# Roundupdb now appends "mailing list" information to its messages which
+# include the e-mail address and web interface address. Templates may
+# override this in their db classes to include specific information (support
+# instructions, etc).
+#
+# Revision 1.11 2001/08/01 04:24:21 richard
+# mailgw was assuming certain properties existed on the issues being created.
+#
+# Revision 1.10 2001/07/30 02:38:31 richard
+# get() now has a default arg - for migration only.
+#
+# Revision 1.9 2001/07/29 09:28:23 richard
+# Fixed sorting by clicking on column headings.
+#
+# Revision 1.8 2001/07/29 08:27:40 richard
+# Fixed handling of passed-in values in form elements (ie. during a
+# drill-down)
+#
+# Revision 1.7 2001/07/29 07:01:39 richard
+# Added vim command to all source so that we don't get no steenkin' tabs :)
+#
+# Revision 1.6 2001/07/29 05:36:14 richard
+# Cleanup of the link label generation.
+#
+# Revision 1.5 2001/07/29 04:05:37 richard
+# Added the fabricated property "id".
+#
+# Revision 1.4 2001/07/27 06:25:35 richard
+# Fixed some of the exceptions so they're the right type.
+# Removed the str()-ification of node ids so we don't mask oopsy errors any
+# more.
+#
+# Revision 1.3 2001/07/27 05:17:14 richard
+# just some comments
+#
+# Revision 1.2 2001/07/22 12:09:32 richard
+# Final commit of Grande Splite
+#
+# Revision 1.1 2001/07/22 11:58:35 richard
+# More Grande Splite
+#
+#
+# vim: set filetype=python ts=4 sw=4 et si