Code

ec62ba03a63f02e243d5dbcf1d7f49b88f9a1737
[roundup.git] / roundup / backends / back_gadfly.py
1 # $Id: back_gadfly.py,v 1.1 2002-08-22 07:56:51 richard Exp $
2 __doc__ = '''
3 About Gadfly
4 ============
6 Gadfly  is  a  collection  of  python modules that provides relational
7 database  functionality  entirely implemented in Python. It supports a
8 subset  of  the intergalactic standard RDBMS Structured Query Language
9 SQL.
12 Basic Structure
13 ===============
15 We map roundup classes to relational tables. Automatically detect schema
16 changes and modify the gadfly table schemas appropriately. Multilinks
17 (which represent a many-to-many relationship) are handled through
18 intermediate tables.
20 Journals are stored adjunct to the per-class tables.
22 Table columns for properties have "_" prepended so the names can't
23 clash with restricted names (like "order"). Retirement is determined by the
24 __retired__ column being true.
26 All columns are defined as VARCHAR, since it really doesn't matter what
27 type they're defined as. We stuff all kinds of data in there ;) [as long as
28 it's marshallable, gadfly doesn't care]
31 Additional Instance Requirements
32 ================================
34 The instance configuration must specify where the database is. It does this
35 with GADFLY_DATABASE, which is used as the arguments to the gadfly.gadfly()
36 method:
38 Using an on-disk database directly (not a good idea):
39   GADFLY_DATABASE = (database name, directory)
41 Using a network database (much better idea):
42   GADFLY_DATABASE = (policy, password, address, port)
44 Because multiple accesses directly to a gadfly database aren't handled, but
45 multiple network accesses are, it's strongly advised that the latter setup be
46 used.
48 '''
50 # standard python modules
51 import sys, os, time, re, errno, weakref
53 # roundup modules
54 from roundup import hyperdb, date, password, roundupdb, security
55 from roundup.hyperdb import String, Password, Date, Interval, Link, \
56     Multilink, DatabaseError, Boolean, Number
58 # the all-important gadfly :)
59 import gadfly
60 from gadfly import client
62 # support
63 from blobfiles import FileStorage
64 from roundup.indexer import Indexer
65 from sessions import Sessions
67 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
68     # flag to set on retired entries
69     RETIRED_FLAG = '__hyperdb_retired'
71     def __init__(self, config, journaltag=None):
72         ''' Open the database and load the schema from it.
73         '''
74         self.config, self.journaltag = config, journaltag
75         self.dir = config.DATABASE
76         self.classes = {}
77         self.indexer = Indexer(self.dir)
78         self.sessions = Sessions(self.config)
79         self.security = security.Security(self)
81         db = config.GADFLY_DATABASE
82         if len(db) == 2:
83             # ensure files are group readable and writable
84             os.umask(0002)
85             try:
86                 self.conn = gadfly.gadfly(*db)
87             except IOError, error:
88                 if error.errno != errno.ENOENT:
89                     raise
90                 self.database_schema = {}
91                 self.conn = gadfly.gadfly()
92                 self.conn.startup(*db)
93                 cursor = self.conn.cursor()
94                 cursor.execute('create table schema (schema varchar)')
95                 cursor.execute('create table ids (name varchar, num integer)')
96             else:
97                 cursor = self.conn.cursor()
98                 cursor.execute('select schema from schema')
99                 self.database_schema = cursor.fetchone()[0]
100         else:
101             self.conn = client.gfclient(*db)
102             cursor = self.conn.cursor()
103             cursor.execute('select schema from schema')
104             self.database_schema = cursor.fetchone()[0]
106     def post_init(self):
107         ''' Called once the schema initialisation has finished.
109             We should now confirm that the schema defined by our "classes"
110             attribute actually matches the schema in the database.
111         '''
112         for classname, spec in self.classes.items():
113             if self.database_schema.has_key(classname):
114                 dbspec = self.database_schema[classname]
115                 self.update_class(spec.schema(), dbspec)
116                 self.database_schema[classname] = dbspec
117             else:
118                 self.create_class(spec)
119                 self.database_schema[classname] = spec.schema()
121         for classname in self.database_schema.keys():
122             if not self.classes.has_key(classname):
123                 self.drop_class(classname)
125         # commit any changes
126         cursor = self.conn.cursor()
127         cursor.execute('delete from schema')
128         cursor.execute('insert into schema values (?)', (self.database_schema,))
129         self.conn.commit()
131     def determine_columns(self, spec):
132         ''' Figure the column names and multilink properties from the spec
133         '''
134         cols = []
135         mls = []
136         # add the multilinks separately
137         for col, prop in spec.properties.items():
138             if isinstance(prop, Multilink):
139                 mls.append(col)
140             else:
141                 cols.append('_'+col)
142         cols.sort()
143         return cols, mls
145     def update_class(self, spec, dbspec):
146         ''' Determine the differences between the current spec and the
147             database version of the spec, and update where necessary
148         '''
149         if spec == dbspec:
150             return
151         raise NotImplementedError
153     def create_class(self, spec):
154         ''' Create a database table according to the given spec.
155         '''
156         cols, mls = self.determine_columns(spec)
158         # add on our special columns
159         cols.append('id')
160         cols.append('__retired__')
162         cursor = self.conn.cursor()
164         # create the base table
165         cols = ','.join(['%s varchar'%x for x in cols])
166         sql = 'create table %s (%s)'%(spec.classname, cols)
167         if __debug__:
168             print >>hyperdb.DEBUG, 'create_class', (self, sql)
169         cursor.execute(sql)
171         # journal table
172         cols = ','.join(['%s varchar'%x
173             for x in 'nodeid date tag action params'.split()])
174         sql = 'create table %s__journal (%s)'%(spec.classname, cols)
175         if __debug__:
176             print >>hyperdb.DEBUG, 'create_class', (self, sql)
177         cursor.execute(sql)
179         # now create the multilink tables
180         for ml in mls:
181             sql = 'create table %s_%s (linkid varchar, nodeid varchar)'%(
182                 spec.classname, ml)
183             if __debug__:
184                 print >>hyperdb.DEBUG, 'create_class', (self, sql)
185             cursor.execute(sql)
187         # ID counter
188         sql = 'insert into ids (name, num) values (?,?)'
189         vals = (spec.classname, 1)
190         if __debug__:
191             print >>hyperdb.DEBUG, 'create_class', (self, sql, vals)
192         cursor.execute(sql, vals)
194     def drop_class(self, spec):
195         ''' Drop the given table from the database.
197             Drop the journal and multilink tables too.
198         '''
199         # figure the multilinks
200         mls = []
201         for col, prop in spec.properties.items():
202             if isinstance(prop, Multilink):
203                 mls.append(col)
204         cursor = self.conn.cursor()
206         sql = 'drop table %s'%spec.classname
207         if __debug__:
208             print >>hyperdb.DEBUG, 'drop_class', (self, sql)
209         cursor.execute(sql)
211         sql = 'drop table %s__journal'%spec.classname
212         if __debug__:
213             print >>hyperdb.DEBUG, 'drop_class', (self, sql)
214         cursor.execute(sql)
216         for ml in mls:
217             sql = 'drop table %s_%s'%(spec.classname, ml)
218             if __debug__:
219                 print >>hyperdb.DEBUG, 'drop_class', (self, sql)
220             cursor.execute(sql)
222     #
223     # Classes
224     #
225     def __getattr__(self, classname):
226         ''' A convenient way of calling self.getclass(classname).
227         '''
228         if self.classes.has_key(classname):
229             if __debug__:
230                 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
231             return self.classes[classname]
232         raise AttributeError, classname
234     def addclass(self, cl):
235         ''' Add a Class to the hyperdatabase.
236         '''
237         if __debug__:
238             print >>hyperdb.DEBUG, 'addclass', (self, cl)
239         cn = cl.classname
240         if self.classes.has_key(cn):
241             raise ValueError, cn
242         self.classes[cn] = cl
244     def getclasses(self):
245         ''' Return a list of the names of all existing classes.
246         '''
247         if __debug__:
248             print >>hyperdb.DEBUG, 'getclasses', (self,)
249         l = self.classes.keys()
250         l.sort()
251         return l
253     def getclass(self, classname):
254         '''Get the Class object representing a particular class.
256         If 'classname' is not a valid class name, a KeyError is raised.
257         '''
258         if __debug__:
259             print >>hyperdb.DEBUG, 'getclass', (self, classname)
260         return self.classes[classname]
262     def clear(self):
263         ''' Delete all database contents.
265             Note: I don't commit here, which is different behaviour to the
266             "nuke from orbit" behaviour in the *dbms.
267         '''
268         if __debug__:
269             print >>hyperdb.DEBUG, 'clear', (self,)
270         cursor = self.conn.cursor()
271         for cn in self.classes.keys():
272             sql = 'delete from %s'%cn
273             if __debug__:
274                 print >>hyperdb.DEBUG, 'clear', (self, sql)
275             cursor.execute(sql)
277     #
278     # Node IDs
279     #
280     def newid(self, classname):
281         ''' Generate a new id for the given class
282         '''
283         # get the next ID
284         cursor = self.conn.cursor()
285         sql = 'select num from ids where name=?'
286         if __debug__:
287             print >>hyperdb.DEBUG, 'newid', (self, sql, classname)
288         cursor.execute(sql, (classname, ))
289         newid = cursor.fetchone()[0]
291         # update the counter
292         sql = 'update ids set num=? where name=?'
293         vals = (newid+1, classname)
294         if __debug__:
295             print >>hyperdb.DEBUG, 'newid', (self, sql, vals)
296         cursor.execute(sql, vals)
298         # return as string
299         return str(newid)
301     def setid(self, classname, setid):
302         ''' Set the id counter: used during import of database
303         '''
304         cursor = self.conn.cursor()
305         sql = 'update ids set num=? where name=?'
306         vals = (setid, spec.classname)
307         if __debug__:
308             print >>hyperdb.DEBUG, 'setid', (self, sql, vals)
309         cursor.execute(sql, vals)
311     #
312     # Nodes
313     #
315     def addnode(self, classname, nodeid, node):
316         ''' Add the specified node to its class's db.
317         '''
318         # gadfly requires values for all non-multilink columns
319         cl = self.classes[classname]
320         cols, mls = self.determine_columns(cl)
322         # default the non-multilink columns
323         for col, prop in cl.properties.items():
324             if not isinstance(col, Multilink):
325                 if not node.has_key(col):
326                     node[col] = None
328         node = self.serialise(classname, node)
330         # make sure the ordering is correct for column name -> column value
331         vals = tuple([node[col[1:]] for col in cols]) + (nodeid, 0)
332         s = ','.join(['?' for x in cols]) + ',?,?'
333         cols = ','.join(cols) + ',id,__retired__'
335         # perform the inserts
336         cursor = self.conn.cursor()
337         sql = 'insert into %s (%s) values (%s)'%(classname, cols, s)
338         if __debug__:
339             print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
340         cursor.execute(sql, vals)
342         # insert the multilink rows
343         for col in mls:
344             t = '%s_%s'%(classname, col)
345             for entry in node[col]:
346                 sql = 'insert into %s (linkid, nodeid) values (?,?)'%t
347                 vals = (entry, nodeid)
348                 if __debug__:
349                     print >>hyperdb.DEBUG, 'addnode', (self, sql, vals)
350                 cursor.execute(sql, vals)
352     def setnode(self, classname, nodeid, node):
353         ''' Change the specified node.
354         '''
355         node = self.serialise(classname, node)
357         cl = self.classes[classname]
358         cols = []
359         mls = []
360         # add the multilinks separately
361         for col in node.keys():
362             prop = cl.properties[col]
363             if isinstance(prop, Multilink):
364                 mls.append(col)
365             else:
366                 cols.append('_'+col)
367         cols.sort()
369         # make sure the ordering is correct for column name -> column value
370         vals = tuple([node[col[1:]] for col in cols])
371         s = ','.join(['?' for x in cols])
372         cols = ','.join(cols)
374         # perform the update
375         cursor = self.conn.cursor()
376         sql = 'update %s (%s) values (%s)'%(classname, cols, s)
377         if __debug__:
378             print >>hyperdb.DEBUG, 'setnode', (self, sql, vals)
379         cursor.execute(sql, vals)
381         # now the fun bit, updating the multilinks ;)
382         # XXX TODO XXX
384     def getnode(self, classname, nodeid):
385         ''' Get a node from the database.
386         '''
387         # figure the columns we're fetching
388         cl = self.classes[classname]
389         cols, mls = self.determine_columns(cl)
390         cols = ','.join(cols)
392         # perform the basic property fetch
393         cursor = self.conn.cursor()
394         sql = 'select %s from %s where id=?'%(cols, classname)
395         if __debug__:
396             print >>hyperdb.DEBUG, 'getnode', (self, sql, nodeid)
397         cursor.execute(sql, (nodeid,))
398         values = cursor.fetchone()
400         # make up the node
401         node = {}
402         for col in range(len(cols)):
403             node[col] = values[col]
405         # now the multilinks
406         for col in mls:
407             # get the link ids
408             sql = 'select linkid from %s_%s where nodeid=?'%(classname, col)
409             if __debug__:
410                 print >>hyperdb.DEBUG, 'getnode', (self, sql, nodeid)
411             cursor.execute(sql, (nodeid,))
412             # extract the first column from the result
413             node[col] = [x[0] for x in cursor.fetchall()]
415         return self.unserialise(classname, node)
417     def serialise(self, classname, node):
418         '''Copy the node contents, converting non-marshallable data into
419            marshallable data.
420         '''
421         if __debug__:
422             print >>hyperdb.DEBUG, 'serialise', classname, node
423         properties = self.getclass(classname).getprops()
424         d = {}
425         for k, v in node.items():
426             # if the property doesn't exist, or is the "retired" flag then
427             # it won't be in the properties dict
428             if not properties.has_key(k):
429                 d[k] = v
430                 continue
432             # get the property spec
433             prop = properties[k]
435             if isinstance(prop, Password):
436                 d[k] = str(v)
437             elif isinstance(prop, Date) and v is not None:
438                 d[k] = v.serialise()
439             elif isinstance(prop, Interval) and v is not None:
440                 d[k] = v.serialise()
441             else:
442                 d[k] = v
443         return d
445     def unserialise(self, classname, node):
446         '''Decode the marshalled node data
447         '''
448         if __debug__:
449             print >>hyperdb.DEBUG, 'unserialise', classname, node
450         properties = self.getclass(classname).getprops()
451         d = {}
452         for k, v in node.items():
453             # if the property doesn't exist, or is the "retired" flag then
454             # it won't be in the properties dict
455             if not properties.has_key(k):
456                 d[k] = v
457                 continue
459             # get the property spec
460             prop = properties[k]
462             if isinstance(prop, Date) and v is not None:
463                 d[k] = date.Date(v)
464             elif isinstance(prop, Interval) and v is not None:
465                 d[k] = date.Interval(v)
466             elif isinstance(prop, Password):
467                 p = password.Password()
468                 p.unpack(v)
469                 d[k] = p
470             else:
471                 d[k] = v
472         return d
474     def hasnode(self, classname, nodeid):
475         ''' Determine if the database has a given node.
476         '''
477         cursor = self.conn.cursor()
478         sql = 'select count(*) from %s where nodeid=?'%classname
479         if __debug__:
480             print >>hyperdb.DEBUG, 'hasnode', (self, sql, nodeid)
481         cursor.execute(sql, (nodeid,))
482         return cursor.fetchone()[0]
484     def countnodes(self, classname):
485         ''' Count the number of nodes that exist for a particular Class.
486         '''
487         cursor = self.conn.cursor()
488         sql = 'select count(*) from %s'%classname
489         if __debug__:
490             print >>hyperdb.DEBUG, 'countnodes', (self, sql)
491         cursor.execute(sql)
492         return cursor.fetchone()[0]
494     def getnodeids(self, classname):
495         ''' Retrieve all the ids of the nodes for a particular Class.
496         '''
497         cursor = self.conn.cursor()
498         sql = 'select id from %s'%classname
499         if __debug__:
500             print >>hyperdb.DEBUG, 'getnodeids', (self, sql)
501         cursor.execute(sql)
502         return [x[0] for x in cursor.fetchall()]
504     def addjournal(self, classname, nodeid, action, params):
505         ''' Journal the Action
506         'action' may be:
508             'create' or 'set' -- 'params' is a dictionary of property values
509             'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
510             'retire' -- 'params' is None
511         '''
512         if isinstance(params, type({})):
513             if params.has_key('creator'):
514                 journaltag = self.user.get(params['creator'], 'username')
515                 del params['creator']
516             else:
517                 journaltag = self.journaltag
518             if params.has_key('created'):
519                 journaldate = params['created'].serialise()
520                 del params['created']
521             else:
522                 journaldate = date.Date().serialise()
523             if params.has_key('activity'):
524                 del params['activity']
526             # serialise the parameters now
527             if action in ('set', 'create'):
528                 params = self.serialise(classname, params)
529         else:
530             journaltag = self.journaltag
531             journaldate = date.Date().serialise()
533         # create the journal entry
534         cols = ','.join('nodeid date tag action params'.split())
535         entry = (nodeid, journaldate, journaltag, action, params)
537         if __debug__:
538             print >>hyperdb.DEBUG, 'doSaveJournal', entry
540         # do the insert
541         cursor = self.conn.cursor()
542         sql = 'insert into %s__journal (%s) values (?,?,?,?,?)'%(classname,
543             cols)
544         if __debug__:
545             print >>hyperdb.DEBUG, 'addjournal', (self, sql, entry)
546         cursor.execute(sql, entry)
548     def getjournal(self, classname, nodeid):
549         ''' get the journal for id
550         '''
551         cols = ','.join('nodeid date tag action params'.split())
552         cursor = self.conn.cursor()
553         sql = 'select %s from %s__journal where nodeid=?'%(cols, classname)
554         if __debug__:
555             print >>hyperdb.DEBUG, 'getjournal', (self, sql, nodeid)
556         cursor.execute(sql, (nodeid,))
557         res = []
558         for nodeid, date_stamp, user, action, params in cursor.fetchall():
559             res.append((nodeid, date.Date(date_stamp), user, action, params))
560         return res
562     def pack(self, pack_before):
563         ''' Pack the database, removing all journal entries before the
564             "pack_before" date.
565         '''
566         # get a 'yyyymmddhhmmss' version of the date
567         date_stamp = pack_before.serialise()
569         # do the delete
570         cursor = self.conn.cursor()
571         for classname in self.classes.keys():
572             sql = 'delete from %s__journal where date<?'%classname
573             if __debug__:
574                 print >>hyperdb.DEBUG, 'pack', (self, sql, date_stamp)
575             cursor.execute(sql, (date_stamp,))
577     def commit(self):
578         ''' Commit the current transactions.
580         Save all data changed since the database was opened or since the
581         last commit() or rollback().
582         '''
583         self.conn.commit()
585     def rollback(self):
586         ''' Reverse all actions from the current transaction.
588         Undo all the changes made since the database was opened or the last
589         commit() or rollback() was performed.
590         '''
591         self.conn.rollback()
594 # The base Class class
596 class Class(hyperdb.Class):
597     ''' The handle to a particular class of nodes in a hyperdatabase.
598         
599         All methods except __repr__ and getnode must be implemented by a
600         concrete backend Class.
601     '''
603     def __init__(self, db, classname, **properties):
604         '''Create a new class with a given name and property specification.
606         'classname' must not collide with the name of an existing class,
607         or a ValueError is raised.  The keyword arguments in 'properties'
608         must map names to property objects, or a TypeError is raised.
609         '''
610         if (properties.has_key('creation') or properties.has_key('activity')
611                 or properties.has_key('creator')):
612             raise ValueError, '"creation", "activity" and "creator" are '\
613                 'reserved'
615         self.classname = classname
616         self.properties = properties
617         self.db = weakref.proxy(db)       # use a weak ref to avoid circularity
618         self.key = ''
620         # should we journal changes (default yes)
621         self.do_journal = 1
623         # do the db-related init stuff
624         db.addclass(self)
626         self.auditors = {'create': [], 'set': [], 'retire': []}
627         self.reactors = {'create': [], 'set': [], 'retire': []}
629     def schema(self):
630         ''' A dumpable version of the schema that we can store in the
631             database
632         '''
633         return (self.key, [(x, repr(y)) for x,y in self.properties.items()])
635     def enableJournalling(self):
636         '''Turn journalling on for this class
637         '''
638         self.do_journal = 1
640     def disableJournalling(self):
641         '''Turn journalling off for this class
642         '''
643         self.do_journal = 0
645     # Editing nodes:
646     def create(self, **propvalues):
647         ''' Create a new node of this class and return its id.
649         The keyword arguments in 'propvalues' map property names to values.
651         The values of arguments must be acceptable for the types of their
652         corresponding properties or a TypeError is raised.
653         
654         If this class has a key property, it must be present and its value
655         must not collide with other key strings or a ValueError is raised.
656         
657         Any other properties on this class that are missing from the
658         'propvalues' dictionary are set to None.
659         
660         If an id in a link or multilink property does not refer to a valid
661         node, an IndexError is raised.
662         '''
663         if propvalues.has_key('id'):
664             raise KeyError, '"id" is reserved'
666         if self.db.journaltag is None:
667             raise DatabaseError, 'Database open read-only'
669         if propvalues.has_key('creation') or propvalues.has_key('activity'):
670             raise KeyError, '"creation" and "activity" are reserved'
672         self.fireAuditors('create', None, propvalues)
674         # new node's id
675         newid = self.db.newid(self.classname)
677         # validate propvalues
678         num_re = re.compile('^\d+$')
679         for key, value in propvalues.items():
680             if key == self.key:
681                 try:
682                     self.lookup(value)
683                 except KeyError:
684                     pass
685                 else:
686                     raise ValueError, 'node with key "%s" exists'%value
688             # try to handle this property
689             try:
690                 prop = self.properties[key]
691             except KeyError:
692                 raise KeyError, '"%s" has no property "%s"'%(self.classname,
693                     key)
695             if value is not None and isinstance(prop, Link):
696                 if type(value) != type(''):
697                     raise ValueError, 'link value must be String'
698                 link_class = self.properties[key].classname
699                 # if it isn't a number, it's a key
700                 if not num_re.match(value):
701                     try:
702                         value = self.db.classes[link_class].lookup(value)
703                     except (TypeError, KeyError):
704                         raise IndexError, 'new property "%s": %s not a %s'%(
705                             key, value, link_class)
706                 elif not self.db.getclass(link_class).hasnode(value):
707                     raise IndexError, '%s has no node %s'%(link_class, value)
709                 # save off the value
710                 propvalues[key] = value
712                 # register the link with the newly linked node
713                 if self.do_journal and self.properties[key].do_journal:
714                     self.db.addjournal(link_class, value, 'link',
715                         (self.classname, newid, key))
717             elif isinstance(prop, Multilink):
718                 if type(value) != type([]):
719                     raise TypeError, 'new property "%s" not a list of ids'%key
721                 # clean up and validate the list of links
722                 link_class = self.properties[key].classname
723                 l = []
724                 for entry in value:
725                     if type(entry) != type(''):
726                         raise ValueError, '"%s" link value (%s) must be '\
727                             'String'%(key, value)
728                     # if it isn't a number, it's a key
729                     if not num_re.match(entry):
730                         try:
731                             entry = self.db.classes[link_class].lookup(entry)
732                         except (TypeError, KeyError):
733                             raise IndexError, 'new property "%s": %s not a %s'%(
734                                 key, entry, self.properties[key].classname)
735                     l.append(entry)
736                 value = l
737                 propvalues[key] = value
739                 # handle additions
740                 for nodeid in value:
741                     if not self.db.getclass(link_class).hasnode(nodeid):
742                         raise IndexError, '%s has no node %s'%(link_class,
743                             nodeid)
744                     # register the link with the newly linked node
745                     if self.do_journal and self.properties[key].do_journal:
746                         self.db.addjournal(link_class, nodeid, 'link',
747                             (self.classname, newid, key))
749             elif isinstance(prop, String):
750                 if type(value) != type(''):
751                     raise TypeError, 'new property "%s" not a string'%key
753             elif isinstance(prop, Password):
754                 if not isinstance(value, password.Password):
755                     raise TypeError, 'new property "%s" not a Password'%key
757             elif isinstance(prop, Date):
758                 if value is not None and not isinstance(value, date.Date):
759                     raise TypeError, 'new property "%s" not a Date'%key
761             elif isinstance(prop, Interval):
762                 if value is not None and not isinstance(value, date.Interval):
763                     raise TypeError, 'new property "%s" not an Interval'%key
765             elif value is not None and isinstance(prop, Number):
766                 try:
767                     float(value)
768                 except ValueError:
769                     raise TypeError, 'new property "%s" not numeric'%key
771             elif value is not None and isinstance(prop, Boolean):
772                 try:
773                     int(value)
774                 except ValueError:
775                     raise TypeError, 'new property "%s" not boolean'%key
777         # make sure there's data where there needs to be
778         for key, prop in self.properties.items():
779             if propvalues.has_key(key):
780                 continue
781             if key == self.key:
782                 raise ValueError, 'key property "%s" is required'%key
783             if isinstance(prop, Multilink):
784                 propvalues[key] = []
785             else:
786                 propvalues[key] = None
788         # done
789         self.db.addnode(self.classname, newid, propvalues)
790         if self.do_journal:
791             self.db.addjournal(self.classname, newid, 'create', propvalues)
793         self.fireReactors('create', newid, None)
795         return newid
797     _marker = []
798     def get(self, nodeid, propname, default=_marker, cache=1):
799         '''Get the value of a property on an existing node of this class.
801         'nodeid' must be the id of an existing node of this class or an
802         IndexError is raised.  'propname' must be the name of a property
803         of this class or a KeyError is raised.
805         'cache' indicates whether the transaction cache should be queried
806         for the node. If the node has been modified and you need to
807         determine what its values prior to modification are, you need to
808         set cache=0.
809         '''
810         if propname == 'id':
811             return nodeid
813         if propname == 'creation':
814             if not self.do_journal:
815                 raise ValueError, 'Journalling is disabled for this class'
816             journal = self.db.getjournal(self.classname, nodeid)
817             if journal:
818                 return self.db.getjournal(self.classname, nodeid)[0][1]
819             else:
820                 # on the strange chance that there's no journal
821                 return date.Date()
822         if propname == 'activity':
823             if not self.do_journal:
824                 raise ValueError, 'Journalling is disabled for this class'
825             journal = self.db.getjournal(self.classname, nodeid)
826             if journal:
827                 return self.db.getjournal(self.classname, nodeid)[-1][1]
828             else:
829                 # on the strange chance that there's no journal
830                 return date.Date()
831         if propname == 'creator':
832             if not self.do_journal:
833                 raise ValueError, 'Journalling is disabled for this class'
834             journal = self.db.getjournal(self.classname, nodeid)
835             if journal:
836                 name = self.db.getjournal(self.classname, nodeid)[0][2]
837             else:
838                 return None
839             return self.db.user.lookup(name)
841         # get the property (raises KeyErorr if invalid)
842         prop = self.properties[propname]
844         # get the node's dict
845         d = self.db.getnode(self.classname, nodeid) #, cache=cache)
847         if not d.has_key(propname):
848             if default is _marker:
849                 if isinstance(prop, Multilink):
850                     return []
851                 else:
852                     return None
853             else:
854                 return default
856         return d[propname]
858     def getnode(self, nodeid, cache=1):
859         ''' Return a convenience wrapper for the node.
861         'nodeid' must be the id of an existing node of this class or an
862         IndexError is raised.
864         'cache' indicates whether the transaction cache should be queried
865         for the node. If the node has been modified and you need to
866         determine what its values prior to modification are, you need to
867         set cache=0.
868         '''
869         return Node(self, nodeid, cache=cache)
871     def set(self, nodeid, **propvalues):
872         '''Modify a property on an existing node of this class.
873         
874         'nodeid' must be the id of an existing node of this class or an
875         IndexError is raised.
877         Each key in 'propvalues' must be the name of a property of this
878         class or a KeyError is raised.
880         All values in 'propvalues' must be acceptable types for their
881         corresponding properties or a TypeError is raised.
883         If the value of the key property is set, it must not collide with
884         other key strings or a ValueError is raised.
886         If the value of a Link or Multilink property contains an invalid
887         node id, a ValueError is raised.
888         '''
889         raise NotImplementedError
891     def retire(self, nodeid):
892         '''Retire a node.
893         
894         The properties on the node remain available from the get() method,
895         and the node's id is never reused.
896         
897         Retired nodes are not returned by the find(), list(), or lookup()
898         methods, and other nodes may reuse the values of their key properties.
899         '''
900         cursor = self.db.conn.cursor()
901         sql = 'update %s set __retired__=1 where id=?'%self.classname
902         if __debug__:
903             print >>hyperdb.DEBUG, 'retire', (self, sql, nodeid)
904         cursor.execute(sql, (nodeid,))
906     def is_retired(self, nodeid):
907         '''Return true if the node is rerired
908         '''
909         cursor = self.db.conn.cursor()
910         sql = 'select __retired__ from %s where id=?'%self.classname
911         if __debug__:
912             print >>hyperdb.DEBUG, 'is_retired', (self, sql, nodeid)
913         cursor.execute(sql, (nodeid,))
914         return cursor.fetchone()[0]
916     def destroy(self, nodeid):
917         '''Destroy a node.
918         
919         WARNING: this method should never be used except in extremely rare
920                  situations where there could never be links to the node being
921                  deleted
922         WARNING: use retire() instead
923         WARNING: the properties of this node will not be available ever again
924         WARNING: really, use retire() instead
926         Well, I think that's enough warnings. This method exists mostly to
927         support the session storage of the cgi interface.
929         The node is completely removed from the hyperdb, including all journal
930         entries. It will no longer be available, and will generally break code
931         if there are any references to the node.
932         '''
933         raise NotImplementedError
935     def history(self, nodeid):
936         '''Retrieve the journal of edits on a particular node.
938         'nodeid' must be the id of an existing node of this class or an
939         IndexError is raised.
941         The returned list contains tuples of the form
943             (date, tag, action, params)
945         'date' is a Timestamp object specifying the time of the change and
946         'tag' is the journaltag specified when the database was opened.
947         '''
948         raise NotImplementedError
950     # Locating nodes:
951     def hasnode(self, nodeid):
952         '''Determine if the given nodeid actually exists
953         '''
954         return self.db.hasnode(self.classname, nodeid)
956     def setkey(self, propname):
957         '''Select a String property of this class to be the key property.
959         'propname' must be the name of a String property of this class or
960         None, or a TypeError is raised.  The values of the key property on
961         all existing nodes must be unique or a ValueError is raised.
962         '''
963         # XXX create an index on the key prop column
964         prop = self.getprops()[propname]
965         if not isinstance(prop, String):
966             raise TypeError, 'key properties must be String'
967         self.key = propname
969     def getkey(self):
970         '''Return the name of the key property for this class or None.'''
971         return self.key
973     def labelprop(self, default_to_id=0):
974         ''' Return the property name for a label for the given node.
976         This method attempts to generate a consistent label for the node.
977         It tries the following in order:
978             1. key property
979             2. "name" property
980             3. "title" property
981             4. first property from the sorted property name list
982         '''
983         raise NotImplementedError
985     def lookup(self, keyvalue):
986         '''Locate a particular node by its key property and return its id.
988         If this class has no key property, a TypeError is raised.  If the
989         'keyvalue' matches one of the values for the key property among
990         the nodes in this class, the matching node's id is returned;
991         otherwise a KeyError is raised.
992         '''
993         if not self.key:
994             raise TypeError, 'No key property set'
996         cursor = self.db.conn.cursor()
997         sql = 'select id from %s where _%s=?'%(self.classname, self.key)
998         if __debug__:
999             print >>hyperdb.DEBUG, 'lookup', (self, sql, keyvalue)
1000         cursor.execute(sql, (keyvalue,))
1002         # see if there was a result
1003         l = cursor.fetchall()
1004         if not l:
1005             raise KeyError, keyvalue
1007         # return the id
1008         return l[0][0]
1010     def find(self, **propspec):
1011         '''Get the ids of nodes in this class which link to the given nodes.
1013         'propspec' consists of keyword args propname={nodeid:1,}   
1014         'propname' must be the name of a property in this class, or a
1015         KeyError is raised.  That property must be a Link or Multilink
1016         property, or a TypeError is raised.
1018         Any node in this class whose 'propname' property links to any of the
1019         nodeids will be returned. Used by the full text indexing, which knows
1020         that "foo" occurs in msg1, msg3 and file7, so we have hits on these
1021         issues:
1023             db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1024         '''
1025         raise NotImplementedError
1027     def filter(self, search_matches, filterspec, sort, group, 
1028             num_re = re.compile('^\d+$')):
1029         ''' Return a list of the ids of the active nodes in this class that
1030             match the 'filter' spec, sorted by the group spec and then the
1031             sort spec
1032         '''
1033         raise NotImplementedError
1035     def count(self):
1036         '''Get the number of nodes in this class.
1038         If the returned integer is 'numnodes', the ids of all the nodes
1039         in this class run from 1 to numnodes, and numnodes+1 will be the
1040         id of the next node to be created in this class.
1041         '''
1042         return self.db.countnodes(self.classname)
1044     # Manipulating properties:
1045     def getprops(self, protected=1):
1046         '''Return a dictionary mapping property names to property objects.
1047            If the "protected" flag is true, we include protected properties -
1048            those which may not be modified.
1049         '''
1050         d = self.properties.copy()
1051         if protected:
1052             d['id'] = String()
1053             d['creation'] = hyperdb.Date()
1054             d['activity'] = hyperdb.Date()
1055             d['creator'] = hyperdb.Link("user")
1056         return d
1058     def addprop(self, **properties):
1059         '''Add properties to this class.
1061         The keyword arguments in 'properties' must map names to property
1062         objects, or a TypeError is raised.  None of the keys in 'properties'
1063         may collide with the names of existing properties, or a ValueError
1064         is raised before any properties have been added.
1065         '''
1066         for key in properties.keys():
1067             if self.properties.has_key(key):
1068                 raise ValueError, key
1069         self.properties.update(properties)
1071     def index(self, nodeid):
1072         '''Add (or refresh) the node to search indexes
1073         '''
1074         # find all the String properties that have indexme
1075         for prop, propclass in self.getprops().items():
1076             if isinstance(propclass, String) and propclass.indexme:
1077                 try:
1078                     value = str(self.get(nodeid, prop))
1079                 except IndexError:
1080                     # node no longer exists - entry should be removed
1081                     self.db.indexer.purge_entry((self.classname, nodeid, prop))
1082                 else:
1083                     # and index them under (classname, nodeid, property)
1084                     self.db.indexer.add_text((self.classname, nodeid, prop),
1085                         value)
1088     #
1089     # Detector interface
1090     #
1091     def audit(self, event, detector):
1092         '''Register a detector
1093         '''
1094         l = self.auditors[event]
1095         if detector not in l:
1096             self.auditors[event].append(detector)
1098     def fireAuditors(self, action, nodeid, newvalues):
1099         '''Fire all registered auditors.
1100         '''
1101         for audit in self.auditors[action]:
1102             audit(self.db, self, nodeid, newvalues)
1104     def react(self, event, detector):
1105         '''Register a detector
1106         '''
1107         l = self.reactors[event]
1108         if detector not in l:
1109             self.reactors[event].append(detector)
1111     def fireReactors(self, action, nodeid, oldvalues):
1112         '''Fire all registered reactors.
1113         '''
1114         for react in self.reactors[action]:
1115             react(self.db, self, nodeid, oldvalues)
1117 class FileClass(Class):
1118     '''This class defines a large chunk of data. To support this, it has a
1119        mandatory String property "content" which is typically saved off
1120        externally to the hyperdb.
1122        The default MIME type of this data is defined by the
1123        "default_mime_type" class attribute, which may be overridden by each
1124        node if the class defines a "type" String property.
1125     '''
1126     default_mime_type = 'text/plain'
1128     def create(self, **propvalues):
1129         ''' snaffle the file propvalue and store in a file
1130         '''
1131         content = propvalues['content']
1132         del propvalues['content']
1133         newid = Class.create(self, **propvalues)
1134         self.db.storefile(self.classname, newid, None, content)
1135         return newid
1137     def import_list(self, propnames, proplist):
1138         ''' Trap the "content" property...
1139         '''
1140         # dupe this list so we don't affect others
1141         propnames = propnames[:]
1143         # extract the "content" property from the proplist
1144         i = propnames.index('content')
1145         content = proplist[i]
1146         del propnames[i]
1147         del proplist[i]
1149         # do the normal import
1150         newid = Class.import_list(self, propnames, proplist)
1152         # save off the "content" file
1153         self.db.storefile(self.classname, newid, None, content)
1154         return newid
1156     _marker = []
1157     def get(self, nodeid, propname, default=_marker, cache=1):
1158         ''' trap the content propname and get it from the file
1159         '''
1161         poss_msg = 'Possibly a access right configuration problem.'
1162         if propname == 'content':
1163             try:
1164                 return self.db.getfile(self.classname, nodeid, None)
1165             except IOError, (strerror):
1166                 # BUG: by catching this we donot see an error in the log.
1167                 return 'ERROR reading file: %s%s\n%s\n%s'%(
1168                         self.classname, nodeid, poss_msg, strerror)
1169         if default is not _marker:
1170             return Class.get(self, nodeid, propname, default, cache=cache)
1171         else:
1172             return Class.get(self, nodeid, propname, cache=cache)
1174     def getprops(self, protected=1):
1175         ''' In addition to the actual properties on the node, these methods
1176             provide the "content" property. If the "protected" flag is true,
1177             we include protected properties - those which may not be
1178             modified.
1179         '''
1180         d = Class.getprops(self, protected=protected).copy()
1181         if protected:
1182             d['content'] = hyperdb.String()
1183         return d
1185     def index(self, nodeid):
1186         ''' Index the node in the search index.
1188             We want to index the content in addition to the normal String
1189             property indexing.
1190         '''
1191         # perform normal indexing
1192         Class.index(self, nodeid)
1194         # get the content to index
1195         content = self.get(nodeid, 'content')
1197         # figure the mime type
1198         if self.properties.has_key('type'):
1199             mime_type = self.get(nodeid, 'type')
1200         else:
1201             mime_type = self.default_mime_type
1203         # and index!
1204         self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
1205             mime_type)
1207 # XXX deviation from spec - was called ItemClass
1208 class IssueClass(Class, roundupdb.IssueClass):
1209     # Overridden methods:
1210     def __init__(self, db, classname, **properties):
1211         '''The newly-created class automatically includes the "messages",
1212         "files", "nosy", and "superseder" properties.  If the 'properties'
1213         dictionary attempts to specify any of these properties or a
1214         "creation" or "activity" property, a ValueError is raised.
1215         '''
1216         if not properties.has_key('title'):
1217             properties['title'] = hyperdb.String(indexme='yes')
1218         if not properties.has_key('messages'):
1219             properties['messages'] = hyperdb.Multilink("msg")
1220         if not properties.has_key('files'):
1221             properties['files'] = hyperdb.Multilink("file")
1222         if not properties.has_key('nosy'):
1223             properties['nosy'] = hyperdb.Multilink("user")
1224         if not properties.has_key('superseder'):
1225             properties['superseder'] = hyperdb.Multilink(classname)
1226         Class.__init__(self, db, classname, **properties)
1229 # $Log: not supported by cvs2svn $
1230 # Revision 1.80  2002/08/16 04:28:13  richard
1231 # added is_retired query to Class
1233 # Revision 1.79  2002/07/29 23:30:14  richard
1234 # documentation reorg post-new-security
1236 # Revision 1.78  2002/07/21 03:26:37  richard
1237 # Gordon, does this help?
1239 # Revision 1.77  2002/07/18 11:27:47  richard
1240 # ws
1242 # Revision 1.76  2002/07/18 11:17:30  gmcm
1243 # Add Number and Boolean types to hyperdb.
1244 # Add conversion cases to web, mail & admin interfaces.
1245 # Add storage/serialization cases to back_anydbm & back_metakit.
1247 # Revision 1.75  2002/07/14 02:05:53  richard
1248 # . all storage-specific code (ie. backend) is now implemented by the backends
1250 # Revision 1.74  2002/07/10 00:24:10  richard
1251 # braino
1253 # Revision 1.73  2002/07/10 00:19:48  richard
1254 # Added explicit closing of backend database handles.
1256 # Revision 1.72  2002/07/09 21:53:38  gmcm
1257 # Optimize Class.find so that the propspec can contain a set of ids to match.
1258 # This is used by indexer.search so it can do just one find for all the index matches.
1259 # This was already confusing code, but for common terms (lots of index matches),
1260 # it is enormously faster.
1262 # Revision 1.71  2002/07/09 03:02:52  richard
1263 # More indexer work:
1264 # - all String properties may now be indexed too. Currently there's a bit of
1265 #   "issue" specific code in the actual searching which needs to be
1266 #   addressed. In a nutshell:
1267 #   + pass 'indexme="yes"' as a String() property initialisation arg, eg:
1268 #         file = FileClass(db, "file", name=String(), type=String(),
1269 #             comment=String(indexme="yes"))
1270 #   + the comment will then be indexed and be searchable, with the results
1271 #     related back to the issue that the file is linked to
1272 # - as a result of this work, the FileClass has a default MIME type that may
1273 #   be overridden in a subclass, or by the use of a "type" property as is
1274 #   done in the default templates.
1275 # - the regeneration of the indexes (if necessary) is done once the schema is
1276 #   set up in the dbinit.
1278 # Revision 1.70  2002/06/27 12:06:20  gmcm
1279 # Improve an error message.
1281 # Revision 1.69  2002/06/17 23:15:29  richard
1282 # Can debug to stdout now
1284 # Revision 1.68  2002/06/11 06:52:03  richard
1285 #  . #564271 ] find() and new properties
1287 # Revision 1.67  2002/06/11 05:02:37  richard
1288 #  . #565979 ] code error in hyperdb.Class.find
1290 # Revision 1.66  2002/05/25 07:16:24  rochecompaan
1291 # Merged search_indexing-branch with HEAD
1293 # Revision 1.65  2002/05/22 04:12:05  richard
1294 #  . applied patch #558876 ] cgi client customization
1295 #    ... with significant additions and modifications ;)
1296 #    - extended handling of ML assignedto to all places it's handled
1297 #    - added more NotFound info
1299 # Revision 1.64  2002/05/15 06:21:21  richard
1300 #  . node caching now works, and gives a small boost in performance
1302 # As a part of this, I cleaned up the DEBUG output and implemented TRACE
1303 # output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
1304 # CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
1305 # (using if __debug__ which is compiled out with -O)
1307 # Revision 1.63  2002/04/15 23:25:15  richard
1308 # . node ids are now generated from a lockable store - no more race conditions
1310 # We're using the portalocker code by Jonathan Feinberg that was contributed
1311 # to the ASPN Python cookbook. This gives us locking across Unix and Windows.
1313 # Revision 1.62  2002/04/03 07:05:50  richard
1314 # d'oh! killed retirement of nodes :(
1315 # all better now...
1317 # Revision 1.61  2002/04/03 06:11:51  richard
1318 # Fix for old databases that contain properties that don't exist any more.
1320 # Revision 1.60  2002/04/03 05:54:31  richard
1321 # Fixed serialisation problem by moving the serialisation step out of the
1322 # hyperdb.Class (get, set) into the hyperdb.Database.
1324 # Also fixed htmltemplate after the showid changes I made yesterday.
1326 # Unit tests for all of the above written.
1328 # Revision 1.59.2.2  2002/04/20 13:23:33  rochecompaan
1329 # We now have a separate search page for nodes.  Search links for
1330 # different classes can be customized in instance_config similar to
1331 # index links.
1333 # Revision 1.59.2.1  2002/04/19 19:54:42  rochecompaan
1334 # cgi_client.py
1335 #     removed search link for the time being
1336 #     moved rendering of matches to htmltemplate
1337 # hyperdb.py
1338 #     filtering of nodes on full text search incorporated in filter method
1339 # roundupdb.py
1340 #     added paramater to call of filter method
1341 # roundup_indexer.py
1342 #     added search method to RoundupIndexer class
1344 # Revision 1.59  2002/03/12 22:52:26  richard
1345 # more pychecker warnings removed
1347 # Revision 1.58  2002/02/27 03:23:16  richard
1348 # Ran it through pychecker, made fixes
1350 # Revision 1.57  2002/02/20 05:23:24  richard
1351 # Didn't accomodate new values for new properties
1353 # Revision 1.56  2002/02/20 05:05:28  richard
1354 #  . Added simple editing for classes that don't define a templated interface.
1355 #    - access using the admin "class list" interface
1356 #    - limited to admin-only
1357 #    - requires the csv module from object-craft (url given if it's missing)
1359 # Revision 1.55  2002/02/15 07:27:12  richard
1360 # Oops, precedences around the way w0rng.
1362 # Revision 1.54  2002/02/15 07:08:44  richard
1363 #  . Alternate email addresses are now available for users. See the MIGRATION
1364 #    file for info on how to activate the feature.
1366 # Revision 1.53  2002/01/22 07:21:13  richard
1367 # . fixed back_bsddb so it passed the journal tests
1369 # ... it didn't seem happy using the back_anydbm _open method, which is odd.
1370 # Yet another occurrance of whichdb not being able to recognise older bsddb
1371 # databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
1372 # process.
1374 # Revision 1.52  2002/01/21 16:33:19  rochecompaan
1375 # You can now use the roundup-admin tool to pack the database
1377 # Revision 1.51  2002/01/21 03:01:29  richard
1378 # brief docco on the do_journal argument
1380 # Revision 1.50  2002/01/19 13:16:04  rochecompaan
1381 # Journal entries for link and multilink properties can now be switched on
1382 # or off.
1384 # Revision 1.49  2002/01/16 07:02:57  richard
1385 #  . lots of date/interval related changes:
1386 #    - more relaxed date format for input
1388 # Revision 1.48  2002/01/14 06:32:34  richard
1389 #  . #502951 ] adding new properties to old database
1391 # Revision 1.47  2002/01/14 02:20:15  richard
1392 #  . changed all config accesses so they access either the instance or the
1393 #    config attriubute on the db. This means that all config is obtained from
1394 #    instance_config instead of the mish-mash of classes. This will make
1395 #    switching to a ConfigParser setup easier too, I hope.
1397 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1398 # 0.5.0 switch, I hope!)
1400 # Revision 1.46  2002/01/07 10:42:23  richard
1401 # oops
1403 # Revision 1.45  2002/01/02 04:18:17  richard
1404 # hyperdb docstrings
1406 # Revision 1.44  2002/01/02 02:31:38  richard
1407 # Sorry for the huge checkin message - I was only intending to implement #496356
1408 # but I found a number of places where things had been broken by transactions:
1409 #  . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
1410 #    for _all_ roundup-generated smtp messages to be sent to.
1411 #  . the transaction cache had broken the roundupdb.Class set() reactors
1412 #  . newly-created author users in the mailgw weren't being committed to the db
1414 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
1415 # on when I found that stuff :):
1416 #  . #496356 ] Use threading in messages
1417 #  . detectors were being registered multiple times
1418 #  . added tests for mailgw
1419 #  . much better attaching of erroneous messages in the mail gateway
1421 # Revision 1.43  2001/12/20 06:13:24  rochecompaan
1422 # Bugs fixed:
1423 #   . Exception handling in hyperdb for strings-that-look-like numbers got
1424 #     lost somewhere
1425 #   . Internet Explorer submits full path for filename - we now strip away
1426 #     the path
1427 # Features added:
1428 #   . Link and multilink properties are now displayed sorted in the cgi
1429 #     interface
1431 # Revision 1.42  2001/12/16 10:53:37  richard
1432 # take a copy of the node dict so that the subsequent set
1433 # operation doesn't modify the oldvalues structure
1435 # Revision 1.41  2001/12/15 23:47:47  richard
1436 # Cleaned up some bare except statements
1438 # Revision 1.40  2001/12/14 23:42:57  richard
1439 # yuck, a gdbm instance tests false :(
1440 # I've left the debugging code in - it should be removed one day if we're ever
1441 # _really_ anal about performace :)
1443 # Revision 1.39  2001/12/02 05:06:16  richard
1444 # . We now use weakrefs in the Classes to keep the database reference, so
1445 #   the close() method on the database is no longer needed.
1446 #   I bumped the minimum python requirement up to 2.1 accordingly.
1447 # . #487480 ] roundup-server
1448 # . #487476 ] INSTALL.txt
1450 # I also cleaned up the change message / post-edit stuff in the cgi client.
1451 # There's now a clearly marked "TODO: append the change note" where I believe
1452 # the change note should be added there. The "changes" list will obviously
1453 # have to be modified to be a dict of the changes, or somesuch.
1455 # More testing needed.
1457 # Revision 1.38  2001/12/01 07:17:50  richard
1458 # . We now have basic transaction support! Information is only written to
1459 #   the database when the commit() method is called. Only the anydbm
1460 #   backend is modified in this way - neither of the bsddb backends have been.
1461 #   The mail, admin and cgi interfaces all use commit (except the admin tool
1462 #   doesn't have a commit command, so interactive users can't commit...)
1463 # . Fixed login/registration forwarding the user to the right page (or not,
1464 #   on a failure)
1466 # Revision 1.37  2001/11/28 21:55:35  richard
1467 #  . login_action and newuser_action return values were being ignored
1468 #  . Woohoo! Found that bloody re-login bug that was killing the mail
1469 #    gateway.
1470 #  (also a minor cleanup in hyperdb)
1472 # Revision 1.36  2001/11/27 03:16:09  richard
1473 # Another place that wasn't handling missing properties.
1475 # Revision 1.35  2001/11/22 15:46:42  jhermann
1476 # Added module docstrings to all modules.
1478 # Revision 1.34  2001/11/21 04:04:43  richard
1479 # *sigh* more missing value handling
1481 # Revision 1.33  2001/11/21 03:40:54  richard
1482 # more new property handling
1484 # Revision 1.32  2001/11/21 03:11:28  richard
1485 # Better handling of new properties.
1487 # Revision 1.31  2001/11/12 22:01:06  richard
1488 # Fixed issues with nosy reaction and author copies.
1490 # Revision 1.30  2001/11/09 10:11:08  richard
1491 #  . roundup-admin now handles all hyperdb exceptions
1493 # Revision 1.29  2001/10/27 00:17:41  richard
1494 # Made Class.stringFind() do caseless matching.
1496 # Revision 1.28  2001/10/21 04:44:50  richard
1497 # bug #473124: UI inconsistency with Link fields.
1498 #    This also prompted me to fix a fairly long-standing usability issue -
1499 #    that of being able to turn off certain filters.
1501 # Revision 1.27  2001/10/20 23:44:27  richard
1502 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
1504 # Revision 1.26  2001/10/16 03:48:01  richard
1505 # admin tool now complains if a "find" is attempted with a non-link property.
1507 # Revision 1.25  2001/10/11 00:17:51  richard
1508 # Reverted a change in hyperdb so the default value for missing property
1509 # values in a create() is None and not '' (the empty string.) This obviously
1510 # breaks CSV import/export - the string 'None' will be created in an
1511 # export/import operation.
1513 # Revision 1.24  2001/10/10 03:54:57  richard
1514 # Added database importing and exporting through CSV files.
1515 # Uses the csv module from object-craft for exporting if it's available.
1516 # Requires the csv module for importing.
1518 # Revision 1.23  2001/10/09 23:58:10  richard
1519 # Moved the data stringification up into the hyperdb.Class class' get, set
1520 # and create methods. This means that the data is also stringified for the
1521 # journal call, and removes duplication of code from the backends. The
1522 # backend code now only sees strings.
1524 # Revision 1.22  2001/10/09 07:25:59  richard
1525 # Added the Password property type. See "pydoc roundup.password" for
1526 # implementation details. Have updated some of the documentation too.
1528 # Revision 1.21  2001/10/05 02:23:24  richard
1529 #  . roundup-admin create now prompts for property info if none is supplied
1530 #    on the command-line.
1531 #  . hyperdb Class getprops() method may now return only the mutable
1532 #    properties.
1533 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
1534 #    now support anonymous user access (read-only, unless there's an
1535 #    "anonymous" user, in which case write access is permitted). Login
1536 #    handling has been moved into cgi_client.Client.main()
1537 #  . The "extended" schema is now the default in roundup init.
1538 #  . The schemas have had their page headings modified to cope with the new
1539 #    login handling. Existing installations should copy the interfaces.py
1540 #    file from the roundup lib directory to their instance home.
1541 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1542 #    Ping - has been removed.
1543 #  . Fixed a whole bunch of places in the CGI interface where we should have
1544 #    been returning Not Found instead of throwing an exception.
1545 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
1546 #    an item now throws an exception.
1548 # Revision 1.20  2001/10/04 02:12:42  richard
1549 # Added nicer command-line item adding: passing no arguments will enter an
1550 # interactive more which asks for each property in turn. While I was at it, I
1551 # fixed an implementation problem WRT the spec - I wasn't raising a
1552 # ValueError if the key property was missing from a create(). Also added a
1553 # protected=boolean argument to getprops() so we can list only the mutable
1554 # properties (defaults to yes, which lists the immutables).
1556 # Revision 1.19  2001/08/29 04:47:18  richard
1557 # Fixed CGI client change messages so they actually include the properties
1558 # changed (again).
1560 # Revision 1.18  2001/08/16 07:34:59  richard
1561 # better CGI text searching - but hidden filter fields are disappearing...
1563 # Revision 1.17  2001/08/16 06:59:58  richard
1564 # all searches use re now - and they're all case insensitive
1566 # Revision 1.16  2001/08/15 23:43:18  richard
1567 # Fixed some isFooTypes that I missed.
1568 # Refactored some code in the CGI code.
1570 # Revision 1.15  2001/08/12 06:32:36  richard
1571 # using isinstance(blah, Foo) now instead of isFooType
1573 # Revision 1.14  2001/08/07 00:24:42  richard
1574 # stupid typo
1576 # Revision 1.13  2001/08/07 00:15:51  richard
1577 # Added the copyright/license notice to (nearly) all files at request of
1578 # Bizar Software.
1580 # Revision 1.12  2001/08/02 06:38:17  richard
1581 # Roundupdb now appends "mailing list" information to its messages which
1582 # include the e-mail address and web interface address. Templates may
1583 # override this in their db classes to include specific information (support
1584 # instructions, etc).
1586 # Revision 1.11  2001/08/01 04:24:21  richard
1587 # mailgw was assuming certain properties existed on the issues being created.
1589 # Revision 1.10  2001/07/30 02:38:31  richard
1590 # get() now has a default arg - for migration only.
1592 # Revision 1.9  2001/07/29 09:28:23  richard
1593 # Fixed sorting by clicking on column headings.
1595 # Revision 1.8  2001/07/29 08:27:40  richard
1596 # Fixed handling of passed-in values in form elements (ie. during a
1597 # drill-down)
1599 # Revision 1.7  2001/07/29 07:01:39  richard
1600 # Added vim command to all source so that we don't get no steenkin' tabs :)
1602 # Revision 1.6  2001/07/29 05:36:14  richard
1603 # Cleanup of the link label generation.
1605 # Revision 1.5  2001/07/29 04:05:37  richard
1606 # Added the fabricated property "id".
1608 # Revision 1.4  2001/07/27 06:25:35  richard
1609 # Fixed some of the exceptions so they're the right type.
1610 # Removed the str()-ification of node ids so we don't mask oopsy errors any
1611 # more.
1613 # Revision 1.3  2001/07/27 05:17:14  richard
1614 # just some comments
1616 # Revision 1.2  2001/07/22 12:09:32  richard
1617 # Final commit of Grande Splite
1619 # Revision 1.1  2001/07/22 11:58:35  richard
1620 # More Grande Splite
1623 # vim: set filetype=python ts=4 sw=4 et si