1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 #$Id: back_anydbm.py,v 1.39 2002-07-09 03:02:52 richard Exp $
19 '''
20 This module defines a backend that saves the hyperdatabase in a database
21 chosen by anydbm. It is guaranteed to always be available in python
22 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
23 serious bugs, and is not available)
24 '''
26 import whichdb, anydbm, os, marshal
27 from roundup import hyperdb, date
28 from blobfiles import FileStorage
29 from roundup.indexer import Indexer
30 from locking import acquire_lock, release_lock
32 #
33 # Now the database
34 #
35 class Database(FileStorage, hyperdb.Database):
36 """A database for storing records containing flexible data types.
38 Transaction stuff TODO:
39 . check the timestamp of the class file and nuke the cache if it's
40 modified. Do some sort of conflict checking on the dirty stuff.
41 . perhaps detect write collisions (related to above)?
43 """
44 def __init__(self, config, journaltag=None):
45 """Open a hyperdatabase given a specifier to some storage.
47 The 'storagelocator' is obtained from config.DATABASE.
48 The meaning of 'storagelocator' depends on the particular
49 implementation of the hyperdatabase. It could be a file name,
50 a directory path, a socket descriptor for a connection to a
51 database over the network, etc.
53 The 'journaltag' is a token that will be attached to the journal
54 entries for any edits done on the database. If 'journaltag' is
55 None, the database is opened in read-only mode: the Class.create(),
56 Class.set(), and Class.retire() methods are disabled.
57 """
58 self.config, self.journaltag = config, journaltag
59 self.dir = config.DATABASE
60 self.classes = {}
61 self.cache = {} # cache of nodes loaded or created
62 self.dirtynodes = {} # keep track of the dirty nodes by class
63 self.newnodes = {} # keep track of the new nodes by class
64 self.transactions = []
65 self.indexer = Indexer(self.dir)
66 # ensure files are group readable and writable
67 os.umask(0002)
69 def post_init(self):
70 """Called once the schema initialisation has finished."""
71 # reindex the db if necessary
72 if not self.indexer.should_reindex():
73 return
74 for klass in self.classes.values():
75 for nodeid in klass.list():
76 klass.index(nodeid)
77 self.indexer.save_index()
79 def __repr__(self):
80 return '<back_anydbm instance at %x>'%id(self)
82 #
83 # Classes
84 #
85 def __getattr__(self, classname):
86 """A convenient way of calling self.getclass(classname)."""
87 if self.classes.has_key(classname):
88 if __debug__:
89 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
90 return self.classes[classname]
91 raise AttributeError, classname
93 def addclass(self, cl):
94 if __debug__:
95 print >>hyperdb.DEBUG, 'addclass', (self, cl)
96 cn = cl.classname
97 if self.classes.has_key(cn):
98 raise ValueError, cn
99 self.classes[cn] = cl
101 def getclasses(self):
102 """Return a list of the names of all existing classes."""
103 if __debug__:
104 print >>hyperdb.DEBUG, 'getclasses', (self,)
105 l = self.classes.keys()
106 l.sort()
107 return l
109 def getclass(self, classname):
110 """Get the Class object representing a particular class.
112 If 'classname' is not a valid class name, a KeyError is raised.
113 """
114 if __debug__:
115 print >>hyperdb.DEBUG, 'getclass', (self, classname)
116 return self.classes[classname]
118 #
119 # Class DBs
120 #
121 def clear(self):
122 '''Delete all database contents
123 '''
124 if __debug__:
125 print >>hyperdb.DEBUG, 'clear', (self,)
126 for cn in self.classes.keys():
127 for dummy in 'nodes', 'journals':
128 path = os.path.join(self.dir, 'journals.%s'%cn)
129 if os.path.exists(path):
130 os.remove(path)
131 elif os.path.exists(path+'.db'): # dbm appends .db
132 os.remove(path+'.db')
134 def getclassdb(self, classname, mode='r'):
135 ''' grab a connection to the class db that will be used for
136 multiple actions
137 '''
138 if __debug__:
139 print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
140 return self._opendb('nodes.%s'%classname, mode)
142 def _opendb(self, name, mode):
143 '''Low-level database opener that gets around anydbm/dbm
144 eccentricities.
145 '''
146 if __debug__:
147 print >>hyperdb.DEBUG, '_opendb', (self, name, mode)
149 # determine which DB wrote the class file
150 db_type = ''
151 path = os.path.join(os.getcwd(), self.dir, name)
152 if os.path.exists(path):
153 db_type = whichdb.whichdb(path)
154 if not db_type:
155 raise hyperdb.DatabaseError, "Couldn't identify database type"
156 elif os.path.exists(path+'.db'):
157 # if the path ends in '.db', it's a dbm database, whether
158 # anydbm says it's dbhash or not!
159 db_type = 'dbm'
161 # new database? let anydbm pick the best dbm
162 if not db_type:
163 if __debug__:
164 print >>hyperdb.DEBUG, "_opendb anydbm.open(%r, 'n')"%path
165 return anydbm.open(path, 'n')
167 # open the database with the correct module
168 try:
169 dbm = __import__(db_type)
170 except ImportError:
171 raise hyperdb.DatabaseError, \
172 "Couldn't open database - the required module '%s'"\
173 " is not available"%db_type
174 if __debug__:
175 print >>hyperdb.DEBUG, "_opendb %r.open(%r, %r)"%(db_type, path,
176 mode)
177 return dbm.open(path, mode)
179 def _lockdb(self, name):
180 ''' Lock a database file
181 '''
182 path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
183 return acquire_lock(path)
185 #
186 # Node IDs
187 #
188 def newid(self, classname):
189 ''' Generate a new id for the given class
190 '''
191 # open the ids DB - create if if doesn't exist
192 lock = self._lockdb('_ids')
193 db = self._opendb('_ids', 'c')
194 if db.has_key(classname):
195 newid = db[classname] = str(int(db[classname]) + 1)
196 else:
197 # the count() bit is transitional - older dbs won't start at 1
198 newid = str(self.getclass(classname).count()+1)
199 db[classname] = newid
200 db.close()
201 release_lock(lock)
202 return newid
204 #
205 # Nodes
206 #
207 def addnode(self, classname, nodeid, node):
208 ''' add the specified node to its class's db
209 '''
210 if __debug__:
211 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
212 self.newnodes.setdefault(classname, {})[nodeid] = 1
213 self.cache.setdefault(classname, {})[nodeid] = node
214 self.savenode(classname, nodeid, node)
216 def setnode(self, classname, nodeid, node):
217 ''' change the specified node
218 '''
219 if __debug__:
220 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
221 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
223 # can't set without having already loaded the node
224 self.cache[classname][nodeid] = node
225 self.savenode(classname, nodeid, node)
227 def savenode(self, classname, nodeid, node):
228 ''' perform the saving of data specified by the set/addnode
229 '''
230 if __debug__:
231 print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
232 self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
234 def getnode(self, classname, nodeid, db=None, cache=1):
235 ''' get a node from the database
236 '''
237 if __debug__:
238 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
239 if cache:
240 # try the cache
241 cache_dict = self.cache.setdefault(classname, {})
242 if cache_dict.has_key(nodeid):
243 if __debug__:
244 print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
245 nodeid)
246 return cache_dict[nodeid]
248 if __debug__:
249 print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
251 # get from the database and save in the cache
252 if db is None:
253 db = self.getclassdb(classname)
254 if not db.has_key(nodeid):
255 raise IndexError, "no such %s %s"%(classname, nodeid)
257 # decode
258 res = marshal.loads(db[nodeid])
260 # reverse the serialisation
261 res = self.unserialise(classname, res)
263 # store off in the cache dict
264 if cache:
265 cache_dict[nodeid] = res
267 return res
269 def hasnode(self, classname, nodeid, db=None):
270 ''' determine if the database has a given node
271 '''
272 if __debug__:
273 print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
275 # try the cache
276 cache = self.cache.setdefault(classname, {})
277 if cache.has_key(nodeid):
278 if __debug__:
279 print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
280 return 1
281 if __debug__:
282 print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
284 # not in the cache - check the database
285 if db is None:
286 db = self.getclassdb(classname)
287 res = db.has_key(nodeid)
288 return res
290 def countnodes(self, classname, db=None):
291 if __debug__:
292 print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
293 # include the new nodes not saved to the DB yet
294 count = len(self.newnodes.get(classname, {}))
296 # and count those in the DB
297 if db is None:
298 db = self.getclassdb(classname)
299 count = count + len(db.keys())
300 return count
302 def getnodeids(self, classname, db=None):
303 if __debug__:
304 print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
305 # start off with the new nodes
306 res = self.newnodes.get(classname, {}).keys()
308 if db is None:
309 db = self.getclassdb(classname)
310 res = res + db.keys()
311 return res
314 #
315 # Files - special node properties
316 # inherited from FileStorage
318 #
319 # Journal
320 #
321 def addjournal(self, classname, nodeid, action, params):
322 ''' Journal the Action
323 'action' may be:
325 'create' or 'set' -- 'params' is a dictionary of property values
326 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
327 'retire' -- 'params' is None
328 '''
329 if __debug__:
330 print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
331 action, params)
332 self.transactions.append((self._doSaveJournal, (classname, nodeid,
333 action, params)))
335 def getjournal(self, classname, nodeid):
336 ''' get the journal for id
337 '''
338 if __debug__:
339 print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
340 # attempt to open the journal - in some rare cases, the journal may
341 # not exist
342 try:
343 db = self._opendb('journals.%s'%classname, 'r')
344 except anydbm.error, error:
345 if str(error) == "need 'c' or 'n' flag to open new db": return []
346 elif error.args[0] != 2: raise
347 return []
348 try:
349 journal = marshal.loads(db[nodeid])
350 except KeyError:
351 raise KeyError, 'no such %s %s'%(classname, nodeid)
352 res = []
353 for entry in journal:
354 (nodeid, date_stamp, user, action, params) = entry
355 date_obj = date.Date(date_stamp)
356 res.append((nodeid, date_obj, user, action, params))
357 return res
359 def pack(self, pack_before):
360 ''' delete all journal entries before 'pack_before' '''
361 if __debug__:
362 print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
364 pack_before = pack_before.get_tuple()
366 classes = self.getclasses()
368 # TODO: factor this out to method - we're already doing it in
369 # _opendb.
370 db_type = ''
371 path = os.path.join(os.getcwd(), self.dir, classes[0])
372 if os.path.exists(path):
373 db_type = whichdb.whichdb(path)
374 if not db_type:
375 raise hyperdb.DatabaseError, "Couldn't identify database type"
376 elif os.path.exists(path+'.db'):
377 db_type = 'dbm'
379 for classname in classes:
380 db_name = 'journals.%s'%classname
381 db = self._opendb(db_name, 'w')
383 for key in db.keys():
384 journal = marshal.loads(db[key])
385 l = []
386 last_set_entry = None
387 for entry in journal:
388 (nodeid, date_stamp, self.journaltag, action,
389 params) = entry
390 if date_stamp > pack_before or action == 'create':
391 l.append(entry)
392 elif action == 'set':
393 # grab the last set entry to keep information on
394 # activity
395 last_set_entry = entry
396 if last_set_entry:
397 date_stamp = last_set_entry[1]
398 # if the last set entry was made after the pack date
399 # then it is already in the list
400 if date_stamp < pack_before:
401 l.append(last_set_entry)
402 db[key] = marshal.dumps(l)
403 if db_type == 'gdbm':
404 db.reorganize()
405 db.close()
408 #
409 # Basic transaction support
410 #
411 def commit(self):
412 ''' Commit the current transactions.
413 '''
414 if __debug__:
415 print >>hyperdb.DEBUG, 'commit', (self,)
416 # TODO: lock the DB
418 # keep a handle to all the database files opened
419 self.databases = {}
421 # now, do all the transactions
422 reindex = {}
423 for method, args in self.transactions:
424 reindex[method(*args)] = 1
426 # now close all the database files
427 for db in self.databases.values():
428 db.close()
429 del self.databases
430 # TODO: unlock the DB
432 # reindex the nodes that request it
433 for classname, nodeid in filter(None, reindex.keys()):
434 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
435 self.getclass(classname).index(nodeid)
437 # save the indexer state
438 self.indexer.save_index()
440 # all transactions committed, back to normal
441 self.cache = {}
442 self.dirtynodes = {}
443 self.newnodes = {}
444 self.transactions = []
446 def _doSaveNode(self, classname, nodeid, node):
447 if __debug__:
448 print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
449 node)
451 # get the database handle
452 db_name = 'nodes.%s'%classname
453 if self.databases.has_key(db_name):
454 db = self.databases[db_name]
455 else:
456 db = self.databases[db_name] = self.getclassdb(classname, 'c')
458 # now save the marshalled data
459 db[nodeid] = marshal.dumps(self.serialise(classname, node))
461 # return the classname, nodeid so we reindex this content
462 return (classname, nodeid)
464 def _doSaveJournal(self, classname, nodeid, action, params):
465 # serialise first
466 if action in ('set', 'create'):
467 params = self.serialise(classname, params)
469 # create the journal entry
470 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
471 params)
473 if __debug__:
474 print >>hyperdb.DEBUG, '_doSaveJournal', entry
476 # get the database handle
477 db_name = 'journals.%s'%classname
478 if self.databases.has_key(db_name):
479 db = self.databases[db_name]
480 else:
481 db = self.databases[db_name] = self._opendb(db_name, 'c')
483 # now insert the journal entry
484 if db.has_key(nodeid):
485 # append to existing
486 s = db[nodeid]
487 l = marshal.loads(s)
488 l.append(entry)
489 else:
490 l = [entry]
492 db[nodeid] = marshal.dumps(l)
494 def rollback(self):
495 ''' Reverse all actions from the current transaction.
496 '''
497 if __debug__:
498 print >>hyperdb.DEBUG, 'rollback', (self, )
499 for method, args in self.transactions:
500 # delete temporary files
501 if method == self._doStoreFile:
502 self._rollbackStoreFile(*args)
503 self.cache = {}
504 self.dirtynodes = {}
505 self.newnodes = {}
506 self.transactions = []
508 #
509 #$Log: not supported by cvs2svn $
510 #Revision 1.38 2002/07/08 06:58:15 richard
511 #cleaned up the indexer code:
512 # - it splits more words out (much simpler, faster splitter)
513 # - removed code we'll never use (roundup.roundup_indexer has the full
514 # implementation, and replaces roundup.indexer)
515 # - only index text/plain and rfc822/message (ideas for other text formats to
516 # index are welcome)
517 # - added simple unit test for indexer. Needs more tests for regression.
518 #
519 #Revision 1.37 2002/06/20 23:52:35 richard
520 #More informative error message
521 #
522 #Revision 1.36 2002/06/19 03:07:19 richard
523 #Moved the file storage commit into blobfiles where it belongs.
524 #
525 #Revision 1.35 2002/05/25 07:16:24 rochecompaan
526 #Merged search_indexing-branch with HEAD
527 #
528 #Revision 1.34 2002/05/15 06:21:21 richard
529 # . node caching now works, and gives a small boost in performance
530 #
531 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
532 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
533 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
534 #(using if __debug__ which is compiled out with -O)
535 #
536 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
537 #All database files are now created group readable and writable.
538 #
539 #Revision 1.32 2002/04/15 23:25:15 richard
540 #. node ids are now generated from a lockable store - no more race conditions
541 #
542 #We're using the portalocker code by Jonathan Feinberg that was contributed
543 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
544 #
545 #Revision 1.31 2002/04/03 05:54:31 richard
546 #Fixed serialisation problem by moving the serialisation step out of the
547 #hyperdb.Class (get, set) into the hyperdb.Database.
548 #
549 #Also fixed htmltemplate after the showid changes I made yesterday.
550 #
551 #Unit tests for all of the above written.
552 #
553 #Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
554 # . Added feature #526730 - search for messages capability
555 #
556 #Revision 1.30 2002/02/27 03:40:59 richard
557 #Ran it through pychecker, made fixes
558 #
559 #Revision 1.29 2002/02/25 14:34:31 grubert
560 # . use blobfiles in back_anydbm which is used in back_bsddb.
561 # change test_db as dirlist does not work for subdirectories.
562 # ATTENTION: blobfiles now creates subdirectories for files.
563 #
564 #Revision 1.28 2002/02/16 09:14:17 richard
565 # . #514854 ] History: "User" is always ticket creator
566 #
567 #Revision 1.27 2002/01/22 07:21:13 richard
568 #. fixed back_bsddb so it passed the journal tests
569 #
570 #... it didn't seem happy using the back_anydbm _open method, which is odd.
571 #Yet another occurrance of whichdb not being able to recognise older bsddb
572 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
573 #process.
574 #
575 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
576 #last_set_entry was referenced before assignment
577 #
578 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
579 #We need to keep the last 'set' entry in the journal to preserve
580 #information on 'activity' for nodes.
581 #
582 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
583 #You can now use the roundup-admin tool to pack the database
584 #
585 #Revision 1.23 2002/01/18 04:32:04 richard
586 #Rollback was breaking because a message hadn't actually been written to the file. Needs
587 #more investigation.
588 #
589 #Revision 1.22 2002/01/14 02:20:15 richard
590 # . changed all config accesses so they access either the instance or the
591 # config attriubute on the db. This means that all config is obtained from
592 # instance_config instead of the mish-mash of classes. This will make
593 # switching to a ConfigParser setup easier too, I hope.
594 #
595 #At a minimum, this makes migration a _little_ easier (a lot easier in the
596 #0.5.0 switch, I hope!)
597 #
598 #Revision 1.21 2002/01/02 02:31:38 richard
599 #Sorry for the huge checkin message - I was only intending to implement #496356
600 #but I found a number of places where things had been broken by transactions:
601 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
602 # for _all_ roundup-generated smtp messages to be sent to.
603 # . the transaction cache had broken the roundupdb.Class set() reactors
604 # . newly-created author users in the mailgw weren't being committed to the db
605 #
606 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
607 #on when I found that stuff :):
608 # . #496356 ] Use threading in messages
609 # . detectors were being registered multiple times
610 # . added tests for mailgw
611 # . much better attaching of erroneous messages in the mail gateway
612 #
613 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
614 #Fixed bugs:
615 # . Fixed file creation and retrieval in same transaction in anydbm
616 # backend
617 # . Cgi interface now renders new issue after issue creation
618 # . Could not set issue status to resolved through cgi interface
619 # . Mail gateway was changing status back to 'chatting' if status was
620 # omitted as an argument
621 #
622 #Revision 1.19 2001/12/17 03:52:48 richard
623 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
624 #storing more than one file per node - if a property name is supplied,
625 #the file is called designator.property.
626 #I decided not to migrate the existing files stored over to the new naming
627 #scheme - the FileClass just doesn't specify the property name.
628 #
629 #Revision 1.18 2001/12/16 10:53:38 richard
630 #take a copy of the node dict so that the subsequent set
631 #operation doesn't modify the oldvalues structure
632 #
633 #Revision 1.17 2001/12/14 23:42:57 richard
634 #yuck, a gdbm instance tests false :(
635 #I've left the debugging code in - it should be removed one day if we're ever
636 #_really_ anal about performace :)
637 #
638 #Revision 1.16 2001/12/12 03:23:14 richard
639 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
640 #incorrectly identifies a dbm file as a dbhash file on my system. This has
641 #been submitted to the python bug tracker as issue #491888:
642 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
643 #
644 #Revision 1.15 2001/12/12 02:30:51 richard
645 #I fixed the problems with people whose anydbm was using the dbm module at the
646 #backend. It turns out the dbm module modifies the file name to append ".db"
647 #and my check to determine if we're opening an existing or new db just
648 #tested os.path.exists() on the filename. Well, no longer! We now perform a
649 #much better check _and_ cope with the anydbm implementation module changing
650 #too!
651 #I also fixed the backends __init__ so only ImportError is squashed.
652 #
653 #Revision 1.14 2001/12/10 22:20:01 richard
654 #Enabled transaction support in the bsddb backend. It uses the anydbm code
655 #where possible, only replacing methods where the db is opened (it uses the
656 #btree opener specifically.)
657 #Also cleaned up some change note generation.
658 #Made the backends package work with pydoc too.
659 #
660 #Revision 1.13 2001/12/02 05:06:16 richard
661 #. We now use weakrefs in the Classes to keep the database reference, so
662 # the close() method on the database is no longer needed.
663 # I bumped the minimum python requirement up to 2.1 accordingly.
664 #. #487480 ] roundup-server
665 #. #487476 ] INSTALL.txt
666 #
667 #I also cleaned up the change message / post-edit stuff in the cgi client.
668 #There's now a clearly marked "TODO: append the change note" where I believe
669 #the change note should be added there. The "changes" list will obviously
670 #have to be modified to be a dict of the changes, or somesuch.
671 #
672 #More testing needed.
673 #
674 #Revision 1.12 2001/12/01 07:17:50 richard
675 #. We now have basic transaction support! Information is only written to
676 # the database when the commit() method is called. Only the anydbm
677 # backend is modified in this way - neither of the bsddb backends have been.
678 # The mail, admin and cgi interfaces all use commit (except the admin tool
679 # doesn't have a commit command, so interactive users can't commit...)
680 #. Fixed login/registration forwarding the user to the right page (or not,
681 # on a failure)
682 #
683 #Revision 1.11 2001/11/21 02:34:18 richard
684 #Added a target version field to the extended issue schema
685 #
686 #Revision 1.10 2001/10/09 23:58:10 richard
687 #Moved the data stringification up into the hyperdb.Class class' get, set
688 #and create methods. This means that the data is also stringified for the
689 #journal call, and removes duplication of code from the backends. The
690 #backend code now only sees strings.
691 #
692 #Revision 1.9 2001/10/09 07:25:59 richard
693 #Added the Password property type. See "pydoc roundup.password" for
694 #implementation details. Have updated some of the documentation too.
695 #
696 #Revision 1.8 2001/09/29 13:27:00 richard
697 #CGI interfaces now spit up a top-level index of all the instances they can
698 #serve.
699 #
700 #Revision 1.7 2001/08/12 06:32:36 richard
701 #using isinstance(blah, Foo) now instead of isFooType
702 #
703 #Revision 1.6 2001/08/07 00:24:42 richard
704 #stupid typo
705 #
706 #Revision 1.5 2001/08/07 00:15:51 richard
707 #Added the copyright/license notice to (nearly) all files at request of
708 #Bizar Software.
709 #
710 #Revision 1.4 2001/07/30 01:41:36 richard
711 #Makes schema changes mucho easier.
712 #
713 #Revision 1.3 2001/07/25 01:23:07 richard
714 #Added the Roundup spec to the new documentation directory.
715 #
716 #Revision 1.2 2001/07/23 08:20:44 richard
717 #Moved over to using marshal in the bsddb and anydbm backends.
718 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
719 # retired - mod hyperdb.Class.list() so it lists retired nodes)
720 #
721 #