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()
593 #
594 # The base Class class
595 #
596 class Class(hyperdb.Class):
597 ''' The handle to a particular class of nodes in a hyperdatabase.
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.
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.
657 Any other properties on this class that are missing from the
658 'propvalues' dictionary are set to None.
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.
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.
894 The properties on the node remain available from the get() method,
895 and the node's id is never reused.
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.
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)
1228 #
1229 # $Log: not supported by cvs2svn $
1230 # Revision 1.80 2002/08/16 04:28:13 richard
1231 # added is_retired query to Class
1232 #
1233 # Revision 1.79 2002/07/29 23:30:14 richard
1234 # documentation reorg post-new-security
1235 #
1236 # Revision 1.78 2002/07/21 03:26:37 richard
1237 # Gordon, does this help?
1238 #
1239 # Revision 1.77 2002/07/18 11:27:47 richard
1240 # ws
1241 #
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.
1246 #
1247 # Revision 1.75 2002/07/14 02:05:53 richard
1248 # . all storage-specific code (ie. backend) is now implemented by the backends
1249 #
1250 # Revision 1.74 2002/07/10 00:24:10 richard
1251 # braino
1252 #
1253 # Revision 1.73 2002/07/10 00:19:48 richard
1254 # Added explicit closing of backend database handles.
1255 #
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.
1261 #
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.
1277 #
1278 # Revision 1.70 2002/06/27 12:06:20 gmcm
1279 # Improve an error message.
1280 #
1281 # Revision 1.69 2002/06/17 23:15:29 richard
1282 # Can debug to stdout now
1283 #
1284 # Revision 1.68 2002/06/11 06:52:03 richard
1285 # . #564271 ] find() and new properties
1286 #
1287 # Revision 1.67 2002/06/11 05:02:37 richard
1288 # . #565979 ] code error in hyperdb.Class.find
1289 #
1290 # Revision 1.66 2002/05/25 07:16:24 rochecompaan
1291 # Merged search_indexing-branch with HEAD
1292 #
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
1298 #
1299 # Revision 1.64 2002/05/15 06:21:21 richard
1300 # . node caching now works, and gives a small boost in performance
1301 #
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)
1306 #
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
1309 #
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.
1312 #
1313 # Revision 1.62 2002/04/03 07:05:50 richard
1314 # d'oh! killed retirement of nodes :(
1315 # all better now...
1316 #
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.
1319 #
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.
1323 #
1324 # Also fixed htmltemplate after the showid changes I made yesterday.
1325 #
1326 # Unit tests for all of the above written.
1327 #
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.
1332 #
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
1343 #
1344 # Revision 1.59 2002/03/12 22:52:26 richard
1345 # more pychecker warnings removed
1346 #
1347 # Revision 1.58 2002/02/27 03:23:16 richard
1348 # Ran it through pychecker, made fixes
1349 #
1350 # Revision 1.57 2002/02/20 05:23:24 richard
1351 # Didn't accomodate new values for new properties
1352 #
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)
1358 #
1359 # Revision 1.55 2002/02/15 07:27:12 richard
1360 # Oops, precedences around the way w0rng.
1361 #
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.
1365 #
1366 # Revision 1.53 2002/01/22 07:21:13 richard
1367 # . fixed back_bsddb so it passed the journal tests
1368 #
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.
1373 #
1374 # Revision 1.52 2002/01/21 16:33:19 rochecompaan
1375 # You can now use the roundup-admin tool to pack the database
1376 #
1377 # Revision 1.51 2002/01/21 03:01:29 richard
1378 # brief docco on the do_journal argument
1379 #
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.
1383 #
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
1387 #
1388 # Revision 1.48 2002/01/14 06:32:34 richard
1389 # . #502951 ] adding new properties to old database
1390 #
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.
1396 #
1397 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1398 # 0.5.0 switch, I hope!)
1399 #
1400 # Revision 1.46 2002/01/07 10:42:23 richard
1401 # oops
1402 #
1403 # Revision 1.45 2002/01/02 04:18:17 richard
1404 # hyperdb docstrings
1405 #
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
1413 #
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
1420 #
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
1430 #
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
1434 #
1435 # Revision 1.41 2001/12/15 23:47:47 richard
1436 # Cleaned up some bare except statements
1437 #
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 :)
1442 #
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
1449 #
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.
1454 #
1455 # More testing needed.
1456 #
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)
1465 #
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)
1471 #
1472 # Revision 1.36 2001/11/27 03:16:09 richard
1473 # Another place that wasn't handling missing properties.
1474 #
1475 # Revision 1.35 2001/11/22 15:46:42 jhermann
1476 # Added module docstrings to all modules.
1477 #
1478 # Revision 1.34 2001/11/21 04:04:43 richard
1479 # *sigh* more missing value handling
1480 #
1481 # Revision 1.33 2001/11/21 03:40:54 richard
1482 # more new property handling
1483 #
1484 # Revision 1.32 2001/11/21 03:11:28 richard
1485 # Better handling of new properties.
1486 #
1487 # Revision 1.31 2001/11/12 22:01:06 richard
1488 # Fixed issues with nosy reaction and author copies.
1489 #
1490 # Revision 1.30 2001/11/09 10:11:08 richard
1491 # . roundup-admin now handles all hyperdb exceptions
1492 #
1493 # Revision 1.29 2001/10/27 00:17:41 richard
1494 # Made Class.stringFind() do caseless matching.
1495 #
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.
1500 #
1501 # Revision 1.27 2001/10/20 23:44:27 richard
1502 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
1503 #
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.
1506 #
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.
1512 #
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.
1517 #
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.
1523 #
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.
1527 #
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.
1547 #
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).
1555 #
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).
1559 #
1560 # Revision 1.18 2001/08/16 07:34:59 richard
1561 # better CGI text searching - but hidden filter fields are disappearing...
1562 #
1563 # Revision 1.17 2001/08/16 06:59:58 richard
1564 # all searches use re now - and they're all case insensitive
1565 #
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.
1569 #
1570 # Revision 1.15 2001/08/12 06:32:36 richard
1571 # using isinstance(blah, Foo) now instead of isFooType
1572 #
1573 # Revision 1.14 2001/08/07 00:24:42 richard
1574 # stupid typo
1575 #
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.
1579 #
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).
1585 #
1586 # Revision 1.11 2001/08/01 04:24:21 richard
1587 # mailgw was assuming certain properties existed on the issues being created.
1588 #
1589 # Revision 1.10 2001/07/30 02:38:31 richard
1590 # get() now has a default arg - for migration only.
1591 #
1592 # Revision 1.9 2001/07/29 09:28:23 richard
1593 # Fixed sorting by clicking on column headings.
1594 #
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)
1598 #
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 :)
1601 #
1602 # Revision 1.6 2001/07/29 05:36:14 richard
1603 # Cleanup of the link label generation.
1604 #
1605 # Revision 1.5 2001/07/29 04:05:37 richard
1606 # Added the fabricated property "id".
1607 #
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.
1612 #
1613 # Revision 1.3 2001/07/27 05:17:14 richard
1614 # just some comments
1615 #
1616 # Revision 1.2 2001/07/22 12:09:32 richard
1617 # Final commit of Grande Splite
1618 #
1619 # Revision 1.1 2001/07/22 11:58:35 richard
1620 # More Grande Splite
1621 #
1622 #
1623 # vim: set filetype=python ts=4 sw=4 et si