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.40 2002-07-09 04:19:09 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 res = []
355 for entry in journal:
356 (nodeid, date_stamp, user, action, params) = entry
357 date_obj = date.Date(date_stamp)
358 res.append((nodeid, date_obj, user, action, params))
359 return res
361 def pack(self, pack_before):
362 ''' delete all journal entries before 'pack_before' '''
363 if __debug__:
364 print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
366 pack_before = pack_before.get_tuple()
368 classes = self.getclasses()
370 # TODO: factor this out to method - we're already doing it in
371 # _opendb.
372 db_type = ''
373 path = os.path.join(os.getcwd(), self.dir, classes[0])
374 if os.path.exists(path):
375 db_type = whichdb.whichdb(path)
376 if not db_type:
377 raise hyperdb.DatabaseError, "Couldn't identify database type"
378 elif os.path.exists(path+'.db'):
379 db_type = 'dbm'
381 for classname in classes:
382 db_name = 'journals.%s'%classname
383 db = self._opendb(db_name, 'w')
385 for key in db.keys():
386 journal = marshal.loads(db[key])
387 l = []
388 last_set_entry = None
389 for entry in journal:
390 (nodeid, date_stamp, self.journaltag, action,
391 params) = entry
392 if date_stamp > pack_before or action == 'create':
393 l.append(entry)
394 elif action == 'set':
395 # grab the last set entry to keep information on
396 # activity
397 last_set_entry = entry
398 if last_set_entry:
399 date_stamp = last_set_entry[1]
400 # if the last set entry was made after the pack date
401 # then it is already in the list
402 if date_stamp < pack_before:
403 l.append(last_set_entry)
404 db[key] = marshal.dumps(l)
405 if db_type == 'gdbm':
406 db.reorganize()
407 db.close()
410 #
411 # Basic transaction support
412 #
413 def commit(self):
414 ''' Commit the current transactions.
415 '''
416 if __debug__:
417 print >>hyperdb.DEBUG, 'commit', (self,)
418 # TODO: lock the DB
420 # keep a handle to all the database files opened
421 self.databases = {}
423 # now, do all the transactions
424 reindex = {}
425 for method, args in self.transactions:
426 reindex[method(*args)] = 1
428 # now close all the database files
429 for db in self.databases.values():
430 db.close()
431 del self.databases
432 # TODO: unlock the DB
434 # reindex the nodes that request it
435 for classname, nodeid in filter(None, reindex.keys()):
436 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
437 self.getclass(classname).index(nodeid)
439 # save the indexer state
440 self.indexer.save_index()
442 # all transactions committed, back to normal
443 self.cache = {}
444 self.dirtynodes = {}
445 self.newnodes = {}
446 self.transactions = []
448 def _doSaveNode(self, classname, nodeid, node):
449 if __debug__:
450 print >>hyperdb.DEBUG, '_doSaveNode', (self, classname, nodeid,
451 node)
453 # get the database handle
454 db_name = 'nodes.%s'%classname
455 if self.databases.has_key(db_name):
456 db = self.databases[db_name]
457 else:
458 db = self.databases[db_name] = self.getclassdb(classname, 'c')
460 # now save the marshalled data
461 db[nodeid] = marshal.dumps(self.serialise(classname, node))
463 # return the classname, nodeid so we reindex this content
464 return (classname, nodeid)
466 def _doSaveJournal(self, classname, nodeid, action, params):
467 # serialise first
468 if action in ('set', 'create'):
469 params = self.serialise(classname, params)
471 # create the journal entry
472 entry = (nodeid, date.Date().get_tuple(), self.journaltag, action,
473 params)
475 if __debug__:
476 print >>hyperdb.DEBUG, '_doSaveJournal', entry
478 # get the database handle
479 db_name = 'journals.%s'%classname
480 if self.databases.has_key(db_name):
481 db = self.databases[db_name]
482 else:
483 db = self.databases[db_name] = self._opendb(db_name, 'c')
485 # now insert the journal entry
486 if db.has_key(nodeid):
487 # append to existing
488 s = db[nodeid]
489 l = marshal.loads(s)
490 l.append(entry)
491 else:
492 l = [entry]
494 db[nodeid] = marshal.dumps(l)
496 def rollback(self):
497 ''' Reverse all actions from the current transaction.
498 '''
499 if __debug__:
500 print >>hyperdb.DEBUG, 'rollback', (self, )
501 for method, args in self.transactions:
502 # delete temporary files
503 if method == self._doStoreFile:
504 self._rollbackStoreFile(*args)
505 self.cache = {}
506 self.dirtynodes = {}
507 self.newnodes = {}
508 self.transactions = []
510 #
511 #$Log: not supported by cvs2svn $
512 #Revision 1.39 2002/07/09 03:02:52 richard
513 #More indexer work:
514 #- all String properties may now be indexed too. Currently there's a bit of
515 # "issue" specific code in the actual searching which needs to be
516 # addressed. In a nutshell:
517 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
518 # file = FileClass(db, "file", name=String(), type=String(),
519 # comment=String(indexme="yes"))
520 # + the comment will then be indexed and be searchable, with the results
521 # related back to the issue that the file is linked to
522 #- as a result of this work, the FileClass has a default MIME type that may
523 # be overridden in a subclass, or by the use of a "type" property as is
524 # done in the default templates.
525 #- the regeneration of the indexes (if necessary) is done once the schema is
526 # set up in the dbinit.
527 #
528 #Revision 1.38 2002/07/08 06:58:15 richard
529 #cleaned up the indexer code:
530 # - it splits more words out (much simpler, faster splitter)
531 # - removed code we'll never use (roundup.roundup_indexer has the full
532 # implementation, and replaces roundup.indexer)
533 # - only index text/plain and rfc822/message (ideas for other text formats to
534 # index are welcome)
535 # - added simple unit test for indexer. Needs more tests for regression.
536 #
537 #Revision 1.37 2002/06/20 23:52:35 richard
538 #More informative error message
539 #
540 #Revision 1.36 2002/06/19 03:07:19 richard
541 #Moved the file storage commit into blobfiles where it belongs.
542 #
543 #Revision 1.35 2002/05/25 07:16:24 rochecompaan
544 #Merged search_indexing-branch with HEAD
545 #
546 #Revision 1.34 2002/05/15 06:21:21 richard
547 # . node caching now works, and gives a small boost in performance
548 #
549 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
550 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
551 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
552 #(using if __debug__ which is compiled out with -O)
553 #
554 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
555 #All database files are now created group readable and writable.
556 #
557 #Revision 1.32 2002/04/15 23:25:15 richard
558 #. node ids are now generated from a lockable store - no more race conditions
559 #
560 #We're using the portalocker code by Jonathan Feinberg that was contributed
561 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
562 #
563 #Revision 1.31 2002/04/03 05:54:31 richard
564 #Fixed serialisation problem by moving the serialisation step out of the
565 #hyperdb.Class (get, set) into the hyperdb.Database.
566 #
567 #Also fixed htmltemplate after the showid changes I made yesterday.
568 #
569 #Unit tests for all of the above written.
570 #
571 #Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
572 # . Added feature #526730 - search for messages capability
573 #
574 #Revision 1.30 2002/02/27 03:40:59 richard
575 #Ran it through pychecker, made fixes
576 #
577 #Revision 1.29 2002/02/25 14:34:31 grubert
578 # . use blobfiles in back_anydbm which is used in back_bsddb.
579 # change test_db as dirlist does not work for subdirectories.
580 # ATTENTION: blobfiles now creates subdirectories for files.
581 #
582 #Revision 1.28 2002/02/16 09:14:17 richard
583 # . #514854 ] History: "User" is always ticket creator
584 #
585 #Revision 1.27 2002/01/22 07:21:13 richard
586 #. fixed back_bsddb so it passed the journal tests
587 #
588 #... it didn't seem happy using the back_anydbm _open method, which is odd.
589 #Yet another occurrance of whichdb not being able to recognise older bsddb
590 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
591 #process.
592 #
593 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
594 #last_set_entry was referenced before assignment
595 #
596 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
597 #We need to keep the last 'set' entry in the journal to preserve
598 #information on 'activity' for nodes.
599 #
600 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
601 #You can now use the roundup-admin tool to pack the database
602 #
603 #Revision 1.23 2002/01/18 04:32:04 richard
604 #Rollback was breaking because a message hadn't actually been written to the file. Needs
605 #more investigation.
606 #
607 #Revision 1.22 2002/01/14 02:20:15 richard
608 # . changed all config accesses so they access either the instance or the
609 # config attriubute on the db. This means that all config is obtained from
610 # instance_config instead of the mish-mash of classes. This will make
611 # switching to a ConfigParser setup easier too, I hope.
612 #
613 #At a minimum, this makes migration a _little_ easier (a lot easier in the
614 #0.5.0 switch, I hope!)
615 #
616 #Revision 1.21 2002/01/02 02:31:38 richard
617 #Sorry for the huge checkin message - I was only intending to implement #496356
618 #but I found a number of places where things had been broken by transactions:
619 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
620 # for _all_ roundup-generated smtp messages to be sent to.
621 # . the transaction cache had broken the roundupdb.Class set() reactors
622 # . newly-created author users in the mailgw weren't being committed to the db
623 #
624 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
625 #on when I found that stuff :):
626 # . #496356 ] Use threading in messages
627 # . detectors were being registered multiple times
628 # . added tests for mailgw
629 # . much better attaching of erroneous messages in the mail gateway
630 #
631 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
632 #Fixed bugs:
633 # . Fixed file creation and retrieval in same transaction in anydbm
634 # backend
635 # . Cgi interface now renders new issue after issue creation
636 # . Could not set issue status to resolved through cgi interface
637 # . Mail gateway was changing status back to 'chatting' if status was
638 # omitted as an argument
639 #
640 #Revision 1.19 2001/12/17 03:52:48 richard
641 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
642 #storing more than one file per node - if a property name is supplied,
643 #the file is called designator.property.
644 #I decided not to migrate the existing files stored over to the new naming
645 #scheme - the FileClass just doesn't specify the property name.
646 #
647 #Revision 1.18 2001/12/16 10:53:38 richard
648 #take a copy of the node dict so that the subsequent set
649 #operation doesn't modify the oldvalues structure
650 #
651 #Revision 1.17 2001/12/14 23:42:57 richard
652 #yuck, a gdbm instance tests false :(
653 #I've left the debugging code in - it should be removed one day if we're ever
654 #_really_ anal about performace :)
655 #
656 #Revision 1.16 2001/12/12 03:23:14 richard
657 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
658 #incorrectly identifies a dbm file as a dbhash file on my system. This has
659 #been submitted to the python bug tracker as issue #491888:
660 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
661 #
662 #Revision 1.15 2001/12/12 02:30:51 richard
663 #I fixed the problems with people whose anydbm was using the dbm module at the
664 #backend. It turns out the dbm module modifies the file name to append ".db"
665 #and my check to determine if we're opening an existing or new db just
666 #tested os.path.exists() on the filename. Well, no longer! We now perform a
667 #much better check _and_ cope with the anydbm implementation module changing
668 #too!
669 #I also fixed the backends __init__ so only ImportError is squashed.
670 #
671 #Revision 1.14 2001/12/10 22:20:01 richard
672 #Enabled transaction support in the bsddb backend. It uses the anydbm code
673 #where possible, only replacing methods where the db is opened (it uses the
674 #btree opener specifically.)
675 #Also cleaned up some change note generation.
676 #Made the backends package work with pydoc too.
677 #
678 #Revision 1.13 2001/12/02 05:06:16 richard
679 #. We now use weakrefs in the Classes to keep the database reference, so
680 # the close() method on the database is no longer needed.
681 # I bumped the minimum python requirement up to 2.1 accordingly.
682 #. #487480 ] roundup-server
683 #. #487476 ] INSTALL.txt
684 #
685 #I also cleaned up the change message / post-edit stuff in the cgi client.
686 #There's now a clearly marked "TODO: append the change note" where I believe
687 #the change note should be added there. The "changes" list will obviously
688 #have to be modified to be a dict of the changes, or somesuch.
689 #
690 #More testing needed.
691 #
692 #Revision 1.12 2001/12/01 07:17:50 richard
693 #. We now have basic transaction support! Information is only written to
694 # the database when the commit() method is called. Only the anydbm
695 # backend is modified in this way - neither of the bsddb backends have been.
696 # The mail, admin and cgi interfaces all use commit (except the admin tool
697 # doesn't have a commit command, so interactive users can't commit...)
698 #. Fixed login/registration forwarding the user to the right page (or not,
699 # on a failure)
700 #
701 #Revision 1.11 2001/11/21 02:34:18 richard
702 #Added a target version field to the extended issue schema
703 #
704 #Revision 1.10 2001/10/09 23:58:10 richard
705 #Moved the data stringification up into the hyperdb.Class class' get, set
706 #and create methods. This means that the data is also stringified for the
707 #journal call, and removes duplication of code from the backends. The
708 #backend code now only sees strings.
709 #
710 #Revision 1.9 2001/10/09 07:25:59 richard
711 #Added the Password property type. See "pydoc roundup.password" for
712 #implementation details. Have updated some of the documentation too.
713 #
714 #Revision 1.8 2001/09/29 13:27:00 richard
715 #CGI interfaces now spit up a top-level index of all the instances they can
716 #serve.
717 #
718 #Revision 1.7 2001/08/12 06:32:36 richard
719 #using isinstance(blah, Foo) now instead of isFooType
720 #
721 #Revision 1.6 2001/08/07 00:24:42 richard
722 #stupid typo
723 #
724 #Revision 1.5 2001/08/07 00:15:51 richard
725 #Added the copyright/license notice to (nearly) all files at request of
726 #Bizar Software.
727 #
728 #Revision 1.4 2001/07/30 01:41:36 richard
729 #Makes schema changes mucho easier.
730 #
731 #Revision 1.3 2001/07/25 01:23:07 richard
732 #Added the Roundup spec to the new documentation directory.
733 #
734 #Revision 1.2 2001/07/23 08:20:44 richard
735 #Moved over to using marshal in the bsddb and anydbm backends.
736 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
737 # retired - mod hyperdb.Class.list() so it lists retired nodes)
738 #
739 #