681dfbd0b33cbae6225c13fd188884e070cca304
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.42 2002-07-10 06:21:38 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 self.indexer.should_reindex():
73 self.reindex()
75 def reindex(self):
76 for klass in self.classes.values():
77 for nodeid in klass.list():
78 klass.index(nodeid)
79 self.indexer.save_index()
81 def __repr__(self):
82 return '<back_anydbm instance at %x>'%id(self)
84 #
85 # Classes
86 #
87 def __getattr__(self, classname):
88 """A convenient way of calling self.getclass(classname)."""
89 if self.classes.has_key(classname):
90 if __debug__:
91 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
92 return self.classes[classname]
93 raise AttributeError, classname
95 def addclass(self, cl):
96 if __debug__:
97 print >>hyperdb.DEBUG, 'addclass', (self, cl)
98 cn = cl.classname
99 if self.classes.has_key(cn):
100 raise ValueError, cn
101 self.classes[cn] = cl
103 def getclasses(self):
104 """Return a list of the names of all existing classes."""
105 if __debug__:
106 print >>hyperdb.DEBUG, 'getclasses', (self,)
107 l = self.classes.keys()
108 l.sort()
109 return l
111 def getclass(self, classname):
112 """Get the Class object representing a particular class.
114 If 'classname' is not a valid class name, a KeyError is raised.
115 """
116 if __debug__:
117 print >>hyperdb.DEBUG, 'getclass', (self, classname)
118 return self.classes[classname]
120 #
121 # Class DBs
122 #
123 def clear(self):
124 '''Delete all database contents
125 '''
126 if __debug__:
127 print >>hyperdb.DEBUG, 'clear', (self,)
128 for cn in self.classes.keys():
129 for dummy in 'nodes', 'journals':
130 path = os.path.join(self.dir, 'journals.%s'%cn)
131 if os.path.exists(path):
132 os.remove(path)
133 elif os.path.exists(path+'.db'): # dbm appends .db
134 os.remove(path+'.db')
136 def getclassdb(self, classname, mode='r'):
137 ''' grab a connection to the class db that will be used for
138 multiple actions
139 '''
140 if __debug__:
141 print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
142 return self._opendb('nodes.%s'%classname, mode)
144 def _opendb(self, name, mode):
145 '''Low-level database opener that gets around anydbm/dbm
146 eccentricities.
147 '''
148 if __debug__:
149 print >>hyperdb.DEBUG, '_opendb', (self, name, mode)
151 # determine which DB wrote the class file
152 db_type = ''
153 path = os.path.join(os.getcwd(), self.dir, name)
154 if os.path.exists(path):
155 db_type = whichdb.whichdb(path)
156 if not db_type:
157 raise hyperdb.DatabaseError, "Couldn't identify database type"
158 elif os.path.exists(path+'.db'):
159 # if the path ends in '.db', it's a dbm database, whether
160 # anydbm says it's dbhash or not!
161 db_type = 'dbm'
163 # new database? let anydbm pick the best dbm
164 if not db_type:
165 if __debug__:
166 print >>hyperdb.DEBUG, "_opendb anydbm.open(%r, 'n')"%path
167 return anydbm.open(path, 'n')
169 # open the database with the correct module
170 try:
171 dbm = __import__(db_type)
172 except ImportError:
173 raise hyperdb.DatabaseError, \
174 "Couldn't open database - the required module '%s'"\
175 " is not available"%db_type
176 if __debug__:
177 print >>hyperdb.DEBUG, "_opendb %r.open(%r, %r)"%(db_type, path,
178 mode)
179 return dbm.open(path, mode)
181 def _lockdb(self, name):
182 ''' Lock a database file
183 '''
184 path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
185 return acquire_lock(path)
187 #
188 # Node IDs
189 #
190 def newid(self, classname):
191 ''' Generate a new id for the given class
192 '''
193 # open the ids DB - create if if doesn't exist
194 lock = self._lockdb('_ids')
195 db = self._opendb('_ids', 'c')
196 if db.has_key(classname):
197 newid = db[classname] = str(int(db[classname]) + 1)
198 else:
199 # the count() bit is transitional - older dbs won't start at 1
200 newid = str(self.getclass(classname).count()+1)
201 db[classname] = newid
202 db.close()
203 release_lock(lock)
204 return newid
206 #
207 # Nodes
208 #
209 def addnode(self, classname, nodeid, node):
210 ''' add the specified node to its class's db
211 '''
212 if __debug__:
213 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
214 self.newnodes.setdefault(classname, {})[nodeid] = 1
215 self.cache.setdefault(classname, {})[nodeid] = node
216 self.savenode(classname, nodeid, node)
218 def setnode(self, classname, nodeid, node):
219 ''' change the specified node
220 '''
221 if __debug__:
222 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
223 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
225 # can't set without having already loaded the node
226 self.cache[classname][nodeid] = node
227 self.savenode(classname, nodeid, node)
229 def savenode(self, classname, nodeid, node):
230 ''' perform the saving of data specified by the set/addnode
231 '''
232 if __debug__:
233 print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
234 self.transactions.append((self._doSaveNode, (classname, nodeid, node)))
236 def getnode(self, classname, nodeid, db=None, cache=1):
237 ''' get a node from the database
238 '''
239 if __debug__:
240 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
241 if cache:
242 # try the cache
243 cache_dict = self.cache.setdefault(classname, {})
244 if cache_dict.has_key(nodeid):
245 if __debug__:
246 print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
247 nodeid)
248 return cache_dict[nodeid]
250 if __debug__:
251 print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
253 # get from the database and save in the cache
254 if db is None:
255 db = self.getclassdb(classname)
256 if not db.has_key(nodeid):
257 raise IndexError, "no such %s %s"%(classname, nodeid)
259 # decode
260 res = marshal.loads(db[nodeid])
262 # reverse the serialisation
263 res = self.unserialise(classname, res)
265 # store off in the cache dict
266 if cache:
267 cache_dict[nodeid] = res
269 return res
271 def hasnode(self, classname, nodeid, db=None):
272 ''' determine if the database has a given node
273 '''
274 if __debug__:
275 print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
277 # try the cache
278 cache = self.cache.setdefault(classname, {})
279 if cache.has_key(nodeid):
280 if __debug__:
281 print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
282 return 1
283 if __debug__:
284 print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
286 # not in the cache - check the database
287 if db is None:
288 db = self.getclassdb(classname)
289 res = db.has_key(nodeid)
290 return res
292 def countnodes(self, classname, db=None):
293 if __debug__:
294 print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
295 # include the new nodes not saved to the DB yet
296 count = len(self.newnodes.get(classname, {}))
298 # and count those in the DB
299 if db is None:
300 db = self.getclassdb(classname)
301 count = count + len(db.keys())
302 return count
304 def getnodeids(self, classname, db=None):
305 if __debug__:
306 print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
307 # start off with the new nodes
308 res = self.newnodes.get(classname, {}).keys()
310 if db is None:
311 db = self.getclassdb(classname)
312 res = res + db.keys()
313 return res
316 #
317 # Files - special node properties
318 # inherited from FileStorage
320 #
321 # Journal
322 #
323 def addjournal(self, classname, nodeid, action, params):
324 ''' Journal the Action
325 'action' may be:
327 'create' or 'set' -- 'params' is a dictionary of property values
328 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
329 'retire' -- 'params' is None
330 '''
331 if __debug__:
332 print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
333 action, params)
334 self.transactions.append((self._doSaveJournal, (classname, nodeid,
335 action, params)))
337 def getjournal(self, classname, nodeid):
338 ''' get the journal for id
339 '''
340 if __debug__:
341 print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
342 # attempt to open the journal - in some rare cases, the journal may
343 # not exist
344 try:
345 db = self._opendb('journals.%s'%classname, 'r')
346 except anydbm.error, error:
347 if str(error) == "need 'c' or 'n' flag to open new db": return []
348 elif error.args[0] != 2: raise
349 return []
350 try:
351 journal = marshal.loads(db[nodeid])
352 except KeyError:
353 raise KeyError, 'no such %s %s'%(classname, nodeid)
354 finally:
355 db.close()
356 res = []
357 for entry in journal:
358 (nodeid, date_stamp, user, action, params) = entry
359 date_obj = date.Date(date_stamp)
360 res.append((nodeid, date_obj, user, action, params))
361 return res
363 def pack(self, pack_before):
364 ''' delete all journal entries before 'pack_before' '''
365 if __debug__:
366 print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
368 pack_before = pack_before.get_tuple()
370 classes = self.getclasses()
372 # TODO: factor this out to method - we're already doing it in
373 # _opendb.
374 db_type = ''
375 path = os.path.join(os.getcwd(), self.dir, classes[0])
376 if os.path.exists(path):
377 db_type = whichdb.whichdb(path)
378 if not db_type:
379 raise hyperdb.DatabaseError, "Couldn't identify database type"
380 elif os.path.exists(path+'.db'):
381 db_type = 'dbm'
383 for classname in classes:
384 db_name = 'journals.%s'%classname
385 db = self._opendb(db_name, 'w')
387 for key in db.keys():
388 journal = marshal.loads(db[key])
389 l = []
390 last_set_entry = None
391 for entry in journal:
392 (nodeid, date_stamp, self.journaltag, action,
393 params) = entry
394 if date_stamp > pack_before or action == 'create':
395 l.append(entry)
396 elif action == 'set':
397 # grab the last set entry to keep information on
398 # activity
399 last_set_entry = entry
400 if last_set_entry:
401 date_stamp = last_set_entry[1]
402 # if the last set entry was made after the pack date
403 # then it is already in the list
404 if date_stamp < pack_before:
405 l.append(last_set_entry)
406 db[key] = marshal.dumps(l)
407 if db_type == 'gdbm':
408 db.reorganize()
409 db.close()
412 #
413 # Basic transaction support
414 #
415 def commit(self):
416 ''' Commit the current transactions.
417 '''
418 if __debug__:
419 print >>hyperdb.DEBUG, 'commit', (self,)
420 # TODO: lock the DB
422 # keep a handle to all the database files opened
423 self.databases = {}
425 # now, do all the transactions
426 reindex = {}
427 for method, args in self.transactions:
428 reindex[method(*args)] = 1
430 # now close all the database files
431 for db in self.databases.values():
432 db.close()
433 del self.databases
434 # TODO: unlock the DB
436 # reindex the nodes that request it
437 for classname, nodeid in filter(None, reindex.keys()):
438 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
439 self.getclass(classname).index(nodeid)
441 # save the indexer state
442 self.indexer.save_index()
444 # all transactions committed, back to normal
445 self.cache = {}
446 self.dirtynodes = {}
447 self.newnodes = {}
448 self.transactions = []
450 def _doSaveNode(self, classname, nodeid, node):
451 if __debug__:
452 print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
453 node)
455 # get the database handle
456 db_name = 'nodes.%s'%classname
457 if self.databases.has_key(db_name):
458 db = self.databases[db_name]
459 else:
460 db = self.databases[db_name] = self.getclassdb(classname, 'c')
462 # now save the marshalled data
463 db[nodeid] = marshal.dumps(self.serialise(classname, node))
465 # return the classname, nodeid so we reindex this content
466 return (classname, nodeid)
468 def _doSaveJournal(self, classname, nodeid, action, params):
469 # serialise first
470 if action in ('set', 'create'):
471 params = self.serialise(classname, params)
473 # create the journal entry
474 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
475 params)
477 if __debug__:
478 print >>hyperdb.DEBUG, '_doSaveJournal', entry
480 # get the database handle
481 db_name = 'journals.%s'%classname
482 if self.databases.has_key(db_name):
483 db = self.databases[db_name]
484 else:
485 db = self.databases[db_name] = self._opendb(db_name, 'c')
487 # now insert the journal entry
488 if db.has_key(nodeid):
489 # append to existing
490 s = db[nodeid]
491 l = marshal.loads(s)
492 l.append(entry)
493 else:
494 l = [entry]
496 db[nodeid] = marshal.dumps(l)
498 def rollback(self):
499 ''' Reverse all actions from the current transaction.
500 '''
501 if __debug__:
502 print >>hyperdb.DEBUG, 'rollback', (self, )
503 for method, args in self.transactions:
504 # delete temporary files
505 if method == self._doStoreFile:
506 self._rollbackStoreFile(*args)
507 self.cache = {}
508 self.dirtynodes = {}
509 self.newnodes = {}
510 self.transactions = []
512 #
513 #$Log: not supported by cvs2svn $
514 #Revision 1.41 2002/07/10 00:21:45 richard
515 #explicit database closing
516 #
517 #Revision 1.40 2002/07/09 04:19:09 richard
518 #Added reindex command to roundup-admin.
519 #Fixed reindex on first access.
520 #Also fixed reindexing of entries that change.
521 #
522 #Revision 1.39 2002/07/09 03:02:52 richard
523 #More indexer work:
524 #- all String properties may now be indexed too. Currently there's a bit of
525 # "issue" specific code in the actual searching which needs to be
526 # addressed. In a nutshell:
527 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
528 # file = FileClass(db, "file", name=String(), type=String(),
529 # comment=String(indexme="yes"))
530 # + the comment will then be indexed and be searchable, with the results
531 # related back to the issue that the file is linked to
532 #- as a result of this work, the FileClass has a default MIME type that may
533 # be overridden in a subclass, or by the use of a "type" property as is
534 # done in the default templates.
535 #- the regeneration of the indexes (if necessary) is done once the schema is
536 # set up in the dbinit.
537 #
538 #Revision 1.38 2002/07/08 06:58:15 richard
539 #cleaned up the indexer code:
540 # - it splits more words out (much simpler, faster splitter)
541 # - removed code we'll never use (roundup.roundup_indexer has the full
542 # implementation, and replaces roundup.indexer)
543 # - only index text/plain and rfc822/message (ideas for other text formats to
544 # index are welcome)
545 # - added simple unit test for indexer. Needs more tests for regression.
546 #
547 #Revision 1.37 2002/06/20 23:52:35 richard
548 #More informative error message
549 #
550 #Revision 1.36 2002/06/19 03:07:19 richard
551 #Moved the file storage commit into blobfiles where it belongs.
552 #
553 #Revision 1.35 2002/05/25 07:16:24 rochecompaan
554 #Merged search_indexing-branch with HEAD
555 #
556 #Revision 1.34 2002/05/15 06:21:21 richard
557 # . node caching now works, and gives a small boost in performance
558 #
559 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
560 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
561 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
562 #(using if __debug__ which is compiled out with -O)
563 #
564 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
565 #All database files are now created group readable and writable.
566 #
567 #Revision 1.32 2002/04/15 23:25:15 richard
568 #. node ids are now generated from a lockable store - no more race conditions
569 #
570 #We're using the portalocker code by Jonathan Feinberg that was contributed
571 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
572 #
573 #Revision 1.31 2002/04/03 05:54:31 richard
574 #Fixed serialisation problem by moving the serialisation step out of the
575 #hyperdb.Class (get, set) into the hyperdb.Database.
576 #
577 #Also fixed htmltemplate after the showid changes I made yesterday.
578 #
579 #Unit tests for all of the above written.
580 #
581 #Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
582 # . Added feature #526730 - search for messages capability
583 #
584 #Revision 1.30 2002/02/27 03:40:59 richard
585 #Ran it through pychecker, made fixes
586 #
587 #Revision 1.29 2002/02/25 14:34:31 grubert
588 # . use blobfiles in back_anydbm which is used in back_bsddb.
589 # change test_db as dirlist does not work for subdirectories.
590 # ATTENTION: blobfiles now creates subdirectories for files.
591 #
592 #Revision 1.28 2002/02/16 09:14:17 richard
593 # . #514854 ] History: "User" is always ticket creator
594 #
595 #Revision 1.27 2002/01/22 07:21:13 richard
596 #. fixed back_bsddb so it passed the journal tests
597 #
598 #... it didn't seem happy using the back_anydbm _open method, which is odd.
599 #Yet another occurrance of whichdb not being able to recognise older bsddb
600 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
601 #process.
602 #
603 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
604 #last_set_entry was referenced before assignment
605 #
606 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
607 #We need to keep the last 'set' entry in the journal to preserve
608 #information on 'activity' for nodes.
609 #
610 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
611 #You can now use the roundup-admin tool to pack the database
612 #
613 #Revision 1.23 2002/01/18 04:32:04 richard
614 #Rollback was breaking because a message hadn't actually been written to the file. Needs
615 #more investigation.
616 #
617 #Revision 1.22 2002/01/14 02:20:15 richard
618 # . changed all config accesses so they access either the instance or the
619 # config attriubute on the db. This means that all config is obtained from
620 # instance_config instead of the mish-mash of classes. This will make
621 # switching to a ConfigParser setup easier too, I hope.
622 #
623 #At a minimum, this makes migration a _little_ easier (a lot easier in the
624 #0.5.0 switch, I hope!)
625 #
626 #Revision 1.21 2002/01/02 02:31:38 richard
627 #Sorry for the huge checkin message - I was only intending to implement #496356
628 #but I found a number of places where things had been broken by transactions:
629 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
630 # for _all_ roundup-generated smtp messages to be sent to.
631 # . the transaction cache had broken the roundupdb.Class set() reactors
632 # . newly-created author users in the mailgw weren't being committed to the db
633 #
634 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
635 #on when I found that stuff :):
636 # . #496356 ] Use threading in messages
637 # . detectors were being registered multiple times
638 # . added tests for mailgw
639 # . much better attaching of erroneous messages in the mail gateway
640 #
641 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
642 #Fixed bugs:
643 # . Fixed file creation and retrieval in same transaction in anydbm
644 # backend
645 # . Cgi interface now renders new issue after issue creation
646 # . Could not set issue status to resolved through cgi interface
647 # . Mail gateway was changing status back to 'chatting' if status was
648 # omitted as an argument
649 #
650 #Revision 1.19 2001/12/17 03:52:48 richard
651 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
652 #storing more than one file per node - if a property name is supplied,
653 #the file is called designator.property.
654 #I decided not to migrate the existing files stored over to the new naming
655 #scheme - the FileClass just doesn't specify the property name.
656 #
657 #Revision 1.18 2001/12/16 10:53:38 richard
658 #take a copy of the node dict so that the subsequent set
659 #operation doesn't modify the oldvalues structure
660 #
661 #Revision 1.17 2001/12/14 23:42:57 richard
662 #yuck, a gdbm instance tests false :(
663 #I've left the debugging code in - it should be removed one day if we're ever
664 #_really_ anal about performace :)
665 #
666 #Revision 1.16 2001/12/12 03:23:14 richard
667 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
668 #incorrectly identifies a dbm file as a dbhash file on my system. This has
669 #been submitted to the python bug tracker as issue #491888:
670 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
671 #
672 #Revision 1.15 2001/12/12 02:30:51 richard
673 #I fixed the problems with people whose anydbm was using the dbm module at the
674 #backend. It turns out the dbm module modifies the file name to append ".db"
675 #and my check to determine if we're opening an existing or new db just
676 #tested os.path.exists() on the filename. Well, no longer! We now perform a
677 #much better check _and_ cope with the anydbm implementation module changing
678 #too!
679 #I also fixed the backends __init__ so only ImportError is squashed.
680 #
681 #Revision 1.14 2001/12/10 22:20:01 richard
682 #Enabled transaction support in the bsddb backend. It uses the anydbm code
683 #where possible, only replacing methods where the db is opened (it uses the
684 #btree opener specifically.)
685 #Also cleaned up some change note generation.
686 #Made the backends package work with pydoc too.
687 #
688 #Revision 1.13 2001/12/02 05:06:16 richard
689 #. We now use weakrefs in the Classes to keep the database reference, so
690 # the close() method on the database is no longer needed.
691 # I bumped the minimum python requirement up to 2.1 accordingly.
692 #. #487480 ] roundup-server
693 #. #487476 ] INSTALL.txt
694 #
695 #I also cleaned up the change message / post-edit stuff in the cgi client.
696 #There's now a clearly marked "TODO: append the change note" where I believe
697 #the change note should be added there. The "changes" list will obviously
698 #have to be modified to be a dict of the changes, or somesuch.
699 #
700 #More testing needed.
701 #
702 #Revision 1.12 2001/12/01 07:17:50 richard
703 #. We now have basic transaction support! Information is only written to
704 # the database when the commit() method is called. Only the anydbm
705 # backend is modified in this way - neither of the bsddb backends have been.
706 # The mail, admin and cgi interfaces all use commit (except the admin tool
707 # doesn't have a commit command, so interactive users can't commit...)
708 #. Fixed login/registration forwarding the user to the right page (or not,
709 # on a failure)
710 #
711 #Revision 1.11 2001/11/21 02:34:18 richard
712 #Added a target version field to the extended issue schema
713 #
714 #Revision 1.10 2001/10/09 23:58:10 richard
715 #Moved the data stringification up into the hyperdb.Class class' get, set
716 #and create methods. This means that the data is also stringified for the
717 #journal call, and removes duplication of code from the backends. The
718 #backend code now only sees strings.
719 #
720 #Revision 1.9 2001/10/09 07:25:59 richard
721 #Added the Password property type. See "pydoc roundup.password" for
722 #implementation details. Have updated some of the documentation too.
723 #
724 #Revision 1.8 2001/09/29 13:27:00 richard
725 #CGI interfaces now spit up a top-level index of all the instances they can
726 #serve.
727 #
728 #Revision 1.7 2001/08/12 06:32:36 richard
729 #using isinstance(blah, Foo) now instead of isFooType
730 #
731 #Revision 1.6 2001/08/07 00:24:42 richard
732 #stupid typo
733 #
734 #Revision 1.5 2001/08/07 00:15:51 richard
735 #Added the copyright/license notice to (nearly) all files at request of
736 #Bizar Software.
737 #
738 #Revision 1.4 2001/07/30 01:41:36 richard
739 #Makes schema changes mucho easier.
740 #
741 #Revision 1.3 2001/07/25 01:23:07 richard
742 #Added the Roundup spec to the new documentation directory.
743 #
744 #Revision 1.2 2001/07/23 08:20:44 richard
745 #Moved over to using marshal in the bsddb and anydbm backends.
746 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
747 # retired - mod hyperdb.Class.list() so it lists retired nodes)
748 #
749 #