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.64 2002-08-22 07:57:11 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, re, weakref, string, copy
27 from roundup import hyperdb, date, password, roundupdb, security
28 from blobfiles import FileStorage
29 from sessions import Sessions
30 from roundup.indexer import Indexer
31 from locking import acquire_lock, release_lock
32 from roundup.hyperdb import String, Password, Date, Interval, Link, \
33 Multilink, DatabaseError, Boolean, Number
35 #
36 # Now the database
37 #
38 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
39 '''A database for storing records containing flexible data types.
41 Transaction stuff TODO:
42 . check the timestamp of the class file and nuke the cache if it's
43 modified. Do some sort of conflict checking on the dirty stuff.
44 . perhaps detect write collisions (related to above)?
46 '''
47 def __init__(self, config, journaltag=None):
48 '''Open a hyperdatabase given a specifier to some storage.
50 The 'storagelocator' is obtained from config.DATABASE.
51 The meaning of 'storagelocator' depends on the particular
52 implementation of the hyperdatabase. It could be a file name,
53 a directory path, a socket descriptor for a connection to a
54 database over the network, etc.
56 The 'journaltag' is a token that will be attached to the journal
57 entries for any edits done on the database. If 'journaltag' is
58 None, the database is opened in read-only mode: the Class.create(),
59 Class.set(), and Class.retire() methods are disabled.
60 '''
61 self.config, self.journaltag = config, journaltag
62 self.dir = config.DATABASE
63 self.classes = {}
64 self.cache = {} # cache of nodes loaded or created
65 self.dirtynodes = {} # keep track of the dirty nodes by class
66 self.newnodes = {} # keep track of the new nodes by class
67 self.destroyednodes = {}# keep track of the destroyed nodes by class
68 self.transactions = []
69 self.indexer = Indexer(self.dir)
70 self.sessions = Sessions(self.config)
71 self.security = security.Security(self)
72 # ensure files are group readable and writable
73 os.umask(0002)
75 def post_init(self):
76 '''Called once the schema initialisation has finished.'''
77 # reindex the db if necessary
78 if self.indexer.should_reindex():
79 self.reindex()
81 def reindex(self):
82 for klass in self.classes.values():
83 for nodeid in klass.list():
84 klass.index(nodeid)
85 self.indexer.save_index()
87 def __repr__(self):
88 return '<back_anydbm instance at %x>'%id(self)
90 #
91 # Classes
92 #
93 def __getattr__(self, classname):
94 '''A convenient way of calling self.getclass(classname).'''
95 if self.classes.has_key(classname):
96 if __debug__:
97 print >>hyperdb.DEBUG, '__getattr__', (self, classname)
98 return self.classes[classname]
99 raise AttributeError, classname
101 def addclass(self, cl):
102 if __debug__:
103 print >>hyperdb.DEBUG, 'addclass', (self, cl)
104 cn = cl.classname
105 if self.classes.has_key(cn):
106 raise ValueError, cn
107 self.classes[cn] = cl
109 def getclasses(self):
110 '''Return a list of the names of all existing classes.'''
111 if __debug__:
112 print >>hyperdb.DEBUG, 'getclasses', (self,)
113 l = self.classes.keys()
114 l.sort()
115 return l
117 def getclass(self, classname):
118 '''Get the Class object representing a particular class.
120 If 'classname' is not a valid class name, a KeyError is raised.
121 '''
122 if __debug__:
123 print >>hyperdb.DEBUG, 'getclass', (self, classname)
124 return self.classes[classname]
126 #
127 # Class DBs
128 #
129 def clear(self):
130 '''Delete all database contents
131 '''
132 if __debug__:
133 print >>hyperdb.DEBUG, 'clear', (self,)
134 for cn in self.classes.keys():
135 for dummy in 'nodes', 'journals':
136 path = os.path.join(self.dir, 'journals.%s'%cn)
137 if os.path.exists(path):
138 os.remove(path)
139 elif os.path.exists(path+'.db'): # dbm appends .db
140 os.remove(path+'.db')
142 def getclassdb(self, classname, mode='r'):
143 ''' grab a connection to the class db that will be used for
144 multiple actions
145 '''
146 if __debug__:
147 print >>hyperdb.DEBUG, 'getclassdb', (self, classname, mode)
148 return self.opendb('nodes.%s'%classname, mode)
150 def determine_db_type(self, path):
151 ''' determine which DB wrote the class file
152 '''
153 db_type = ''
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'
162 return db_type
164 def opendb(self, name, mode):
165 '''Low-level database opener that gets around anydbm/dbm
166 eccentricities.
167 '''
168 if __debug__:
169 print >>hyperdb.DEBUG, 'opendb', (self, name, mode)
171 # figure the class db type
172 path = os.path.join(os.getcwd(), self.dir, name)
173 db_type = self.determine_db_type(path)
175 # new database? let anydbm pick the best dbm
176 if not db_type:
177 if __debug__:
178 print >>hyperdb.DEBUG, "opendb anydbm.open(%r, 'n')"%path
179 return anydbm.open(path, 'n')
181 # open the database with the correct module
182 try:
183 dbm = __import__(db_type)
184 except ImportError:
185 raise hyperdb.DatabaseError, \
186 "Couldn't open database - the required module '%s'"\
187 " is not available"%db_type
188 if __debug__:
189 print >>hyperdb.DEBUG, "opendb %r.open(%r, %r)"%(db_type, path,
190 mode)
191 return dbm.open(path, mode)
193 def lockdb(self, name):
194 ''' Lock a database file
195 '''
196 path = os.path.join(os.getcwd(), self.dir, '%s.lock'%name)
197 return acquire_lock(path)
199 #
200 # Node IDs
201 #
202 def newid(self, classname):
203 ''' Generate a new id for the given class
204 '''
205 # open the ids DB - create if if doesn't exist
206 lock = self.lockdb('_ids')
207 db = self.opendb('_ids', 'c')
208 if db.has_key(classname):
209 newid = db[classname] = str(int(db[classname]) + 1)
210 else:
211 # the count() bit is transitional - older dbs won't start at 1
212 newid = str(self.getclass(classname).count()+1)
213 db[classname] = newid
214 db.close()
215 release_lock(lock)
216 return newid
218 def setid(self, classname, setid):
219 ''' Set the id counter: used during import of database
220 '''
221 # open the ids DB - create if if doesn't exist
222 lock = self.lockdb('_ids')
223 db = self.opendb('_ids', 'c')
224 db[classname] = str(setid)
225 db.close()
226 release_lock(lock)
228 #
229 # Nodes
230 #
231 def addnode(self, classname, nodeid, node):
232 ''' add the specified node to its class's db
233 '''
234 if __debug__:
235 print >>hyperdb.DEBUG, 'addnode', (self, classname, nodeid, node)
236 self.newnodes.setdefault(classname, {})[nodeid] = 1
237 self.cache.setdefault(classname, {})[nodeid] = node
238 self.savenode(classname, nodeid, node)
240 def setnode(self, classname, nodeid, node):
241 ''' change the specified node
242 '''
243 if __debug__:
244 print >>hyperdb.DEBUG, 'setnode', (self, classname, nodeid, node)
245 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
247 # can't set without having already loaded the node
248 self.cache[classname][nodeid] = node
249 self.savenode(classname, nodeid, node)
251 def savenode(self, classname, nodeid, node):
252 ''' perform the saving of data specified by the set/addnode
253 '''
254 if __debug__:
255 print >>hyperdb.DEBUG, 'savenode', (self, classname, nodeid, node)
256 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
258 def getnode(self, classname, nodeid, db=None, cache=1):
259 ''' get a node from the database
260 '''
261 if __debug__:
262 print >>hyperdb.DEBUG, 'getnode', (self, classname, nodeid, db)
263 if cache:
264 # try the cache
265 cache_dict = self.cache.setdefault(classname, {})
266 if cache_dict.has_key(nodeid):
267 if __debug__:
268 print >>hyperdb.TRACE, 'get %s %s cached'%(classname,
269 nodeid)
270 return cache_dict[nodeid]
272 if __debug__:
273 print >>hyperdb.TRACE, 'get %s %s'%(classname, nodeid)
275 # get from the database and save in the cache
276 if db is None:
277 db = self.getclassdb(classname)
278 if not db.has_key(nodeid):
279 raise IndexError, "no such %s %s"%(classname, nodeid)
281 # check the uncommitted, destroyed nodes
282 if (self.destroyednodes.has_key(classname) and
283 self.destroyednodes[classname].has_key(nodeid)):
284 raise IndexError, "no such %s %s"%(classname, nodeid)
286 # decode
287 res = marshal.loads(db[nodeid])
289 # reverse the serialisation
290 res = self.unserialise(classname, res)
292 # store off in the cache dict
293 if cache:
294 cache_dict[nodeid] = res
296 return res
298 def destroynode(self, classname, nodeid):
299 '''Remove a node from the database. Called exclusively by the
300 destroy() method on Class.
301 '''
302 if __debug__:
303 print >>hyperdb.DEBUG, 'destroynode', (self, classname, nodeid)
305 # remove from cache and newnodes if it's there
306 if (self.cache.has_key(classname) and
307 self.cache[classname].has_key(nodeid)):
308 del self.cache[classname][nodeid]
309 if (self.newnodes.has_key(classname) and
310 self.newnodes[classname].has_key(nodeid)):
311 del self.newnodes[classname][nodeid]
313 # see if there's any obvious commit actions that we should get rid of
314 for entry in self.transactions[:]:
315 if entry[1][:2] == (classname, nodeid):
316 self.transactions.remove(entry)
318 # add to the destroyednodes map
319 self.destroyednodes.setdefault(classname, {})[nodeid] = 1
321 # add the destroy commit action
322 self.transactions.append((self.doDestroyNode, (classname, nodeid)))
324 def serialise(self, classname, node):
325 '''Copy the node contents, converting non-marshallable data into
326 marshallable data.
327 '''
328 if __debug__:
329 print >>hyperdb.DEBUG, 'serialise', classname, node
330 properties = self.getclass(classname).getprops()
331 d = {}
332 for k, v in node.items():
333 # if the property doesn't exist, or is the "retired" flag then
334 # it won't be in the properties dict
335 if not properties.has_key(k):
336 d[k] = v
337 continue
339 # get the property spec
340 prop = properties[k]
342 if isinstance(prop, Password):
343 d[k] = str(v)
344 elif isinstance(prop, Date) and v is not None:
345 d[k] = v.serialise()
346 elif isinstance(prop, Interval) and v is not None:
347 d[k] = v.serialise()
348 else:
349 d[k] = v
350 return d
352 def unserialise(self, classname, node):
353 '''Decode the marshalled node data
354 '''
355 if __debug__:
356 print >>hyperdb.DEBUG, 'unserialise', classname, node
357 properties = self.getclass(classname).getprops()
358 d = {}
359 for k, v in node.items():
360 # if the property doesn't exist, or is the "retired" flag then
361 # it won't be in the properties dict
362 if not properties.has_key(k):
363 d[k] = v
364 continue
366 # get the property spec
367 prop = properties[k]
369 if isinstance(prop, Date) and v is not None:
370 d[k] = date.Date(v)
371 elif isinstance(prop, Interval) and v is not None:
372 d[k] = date.Interval(v)
373 elif isinstance(prop, Password):
374 p = password.Password()
375 p.unpack(v)
376 d[k] = p
377 else:
378 d[k] = v
379 return d
381 def hasnode(self, classname, nodeid, db=None):
382 ''' determine if the database has a given node
383 '''
384 if __debug__:
385 print >>hyperdb.DEBUG, 'hasnode', (self, classname, nodeid, db)
387 # try the cache
388 cache = self.cache.setdefault(classname, {})
389 if cache.has_key(nodeid):
390 if __debug__:
391 print >>hyperdb.TRACE, 'has %s %s cached'%(classname, nodeid)
392 return 1
393 if __debug__:
394 print >>hyperdb.TRACE, 'has %s %s'%(classname, nodeid)
396 # not in the cache - check the database
397 if db is None:
398 db = self.getclassdb(classname)
399 res = db.has_key(nodeid)
400 return res
402 def countnodes(self, classname, db=None):
403 if __debug__:
404 print >>hyperdb.DEBUG, 'countnodes', (self, classname, db)
406 count = 0
408 # include the uncommitted nodes
409 if self.newnodes.has_key(classname):
410 count += len(self.newnodes[classname])
411 if self.destroyednodes.has_key(classname):
412 count -= len(self.destroyednodes[classname])
414 # and count those in the DB
415 if db is None:
416 db = self.getclassdb(classname)
417 count = count + len(db.keys())
418 return count
420 def getnodeids(self, classname, db=None):
421 if __debug__:
422 print >>hyperdb.DEBUG, 'getnodeids', (self, classname, db)
424 res = []
426 # start off with the new nodes
427 if self.newnodes.has_key(classname):
428 res += self.newnodes[classname].keys()
430 if db is None:
431 db = self.getclassdb(classname)
432 res = res + db.keys()
434 # remove the uncommitted, destroyed nodes
435 if self.destroyednodes.has_key(classname):
436 for nodeid in self.destroyednodes[classname].keys():
437 if db.has_key(nodeid):
438 res.remove(nodeid)
440 return res
443 #
444 # Files - special node properties
445 # inherited from FileStorage
447 #
448 # Journal
449 #
450 def addjournal(self, classname, nodeid, action, params):
451 ''' Journal the Action
452 'action' may be:
454 'create' or 'set' -- 'params' is a dictionary of property values
455 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
456 'retire' -- 'params' is None
457 '''
458 if __debug__:
459 print >>hyperdb.DEBUG, 'addjournal', (self, classname, nodeid,
460 action, params)
461 self.transactions.append((self.doSaveJournal, (classname, nodeid,
462 action, params)))
464 def getjournal(self, classname, nodeid):
465 ''' get the journal for id
467 Raise IndexError if the node doesn't exist (as per history()'s
468 API)
469 '''
470 if __debug__:
471 print >>hyperdb.DEBUG, 'getjournal', (self, classname, nodeid)
472 # attempt to open the journal - in some rare cases, the journal may
473 # not exist
474 try:
475 db = self.opendb('journals.%s'%classname, 'r')
476 except anydbm.error, error:
477 if str(error) == "need 'c' or 'n' flag to open new db":
478 raise IndexError, 'no such %s %s'%(classname, nodeid)
479 elif error.args[0] != 2:
480 raise
481 raise IndexError, 'no such %s %s'%(classname, nodeid)
482 try:
483 journal = marshal.loads(db[nodeid])
484 except KeyError:
485 db.close()
486 raise IndexError, 'no such %s %s'%(classname, nodeid)
487 db.close()
488 res = []
489 for nodeid, date_stamp, user, action, params in journal:
490 res.append((nodeid, date.Date(date_stamp), user, action, params))
491 return res
493 def pack(self, pack_before):
494 ''' delete all journal entries before 'pack_before' '''
495 if __debug__:
496 print >>hyperdb.DEBUG, 'packjournal', (self, pack_before)
498 classes = self.getclasses()
500 # figure the class db type
502 for classname in classes:
503 db_name = 'journals.%s'%classname
504 path = os.path.join(os.getcwd(), self.dir, classname)
505 db_type = self.determine_db_type(path)
506 db = self.opendb(db_name, 'w')
508 for key in db.keys():
509 journal = marshal.loads(db[key])
510 l = []
511 last_set_entry = None
512 for entry in journal:
513 (nodeid, date_stamp, self.journaltag, action,
514 params) = entry
515 date_stamp = date.Date(date_stamp)
516 if date_stamp > pack_before or action == 'create':
517 l.append(entry)
518 elif action == 'set':
519 # grab the last set entry to keep information on
520 # activity
521 last_set_entry = entry
522 if last_set_entry:
523 date_stamp = last_set_entry[1]
524 # if the last set entry was made after the pack date
525 # then it is already in the list
526 if date_stamp < pack_before:
527 l.append(last_set_entry)
528 db[key] = marshal.dumps(l)
529 if db_type == 'gdbm':
530 db.reorganize()
531 db.close()
534 #
535 # Basic transaction support
536 #
537 def commit(self):
538 ''' Commit the current transactions.
539 '''
540 if __debug__:
541 print >>hyperdb.DEBUG, 'commit', (self,)
542 # TODO: lock the DB
544 # keep a handle to all the database files opened
545 self.databases = {}
547 # now, do all the transactions
548 reindex = {}
549 for method, args in self.transactions:
550 reindex[method(*args)] = 1
552 # now close all the database files
553 for db in self.databases.values():
554 db.close()
555 del self.databases
556 # TODO: unlock the DB
558 # reindex the nodes that request it
559 for classname, nodeid in filter(None, reindex.keys()):
560 print >>hyperdb.DEBUG, 'commit.reindex', (classname, nodeid)
561 self.getclass(classname).index(nodeid)
563 # save the indexer state
564 self.indexer.save_index()
566 # all transactions committed, back to normal
567 self.cache = {}
568 self.dirtynodes = {}
569 self.newnodes = {}
570 self.destroyednodes = {}
571 self.transactions = []
573 def getCachedClassDB(self, classname):
574 ''' get the class db, looking in our cache of databases for commit
575 '''
576 # get the database handle
577 db_name = 'nodes.%s'%classname
578 if not self.databases.has_key(db_name):
579 self.databases[db_name] = self.getclassdb(classname, 'c')
580 return self.databases[db_name]
582 def doSaveNode(self, classname, nodeid, node):
583 if __debug__:
584 print >>hyperdb.DEBUG, 'doSaveNode', (self, classname, nodeid,
585 node)
587 db = self.getCachedClassDB(classname)
589 # now save the marshalled data
590 db[nodeid] = marshal.dumps(self.serialise(classname, node))
592 # return the classname, nodeid so we reindex this content
593 return (classname, nodeid)
595 def getCachedJournalDB(self, classname):
596 ''' get the journal db, looking in our cache of databases for commit
597 '''
598 # get the database handle
599 db_name = 'journals.%s'%classname
600 if not self.databases.has_key(db_name):
601 self.databases[db_name] = self.opendb(db_name, 'c')
602 return self.databases[db_name]
604 def doSaveJournal(self, classname, nodeid, action, params):
605 # handle supply of the special journalling parameters (usually
606 # supplied on importing an existing database)
607 if isinstance(params, type({})):
608 if params.has_key('creator'):
609 journaltag = self.user.get(params['creator'], 'username')
610 del params['creator']
611 else:
612 journaltag = self.journaltag
613 if params.has_key('created'):
614 journaldate = params['created'].serialise()
615 del params['created']
616 else:
617 journaldate = date.Date().serialise()
618 if params.has_key('activity'):
619 del params['activity']
621 # serialise the parameters now
622 if action in ('set', 'create'):
623 params = self.serialise(classname, params)
624 else:
625 journaltag = self.journaltag
626 journaldate = date.Date().serialise()
628 # create the journal entry
629 entry = (nodeid, journaldate, journaltag, action, params)
631 if __debug__:
632 print >>hyperdb.DEBUG, 'doSaveJournal', entry
634 db = self.getCachedJournalDB(classname)
636 # now insert the journal entry
637 if db.has_key(nodeid):
638 # append to existing
639 s = db[nodeid]
640 l = marshal.loads(s)
641 l.append(entry)
642 else:
643 l = [entry]
645 db[nodeid] = marshal.dumps(l)
647 def doDestroyNode(self, classname, nodeid):
648 if __debug__:
649 print >>hyperdb.DEBUG, 'doDestroyNode', (self, classname, nodeid)
651 # delete from the class database
652 db = self.getCachedClassDB(classname)
653 if db.has_key(nodeid):
654 del db[nodeid]
656 # delete from the database
657 db = self.getCachedJournalDB(classname)
658 if db.has_key(nodeid):
659 del db[nodeid]
661 # return the classname, nodeid so we reindex this content
662 return (classname, nodeid)
664 def rollback(self):
665 ''' Reverse all actions from the current transaction.
666 '''
667 if __debug__:
668 print >>hyperdb.DEBUG, 'rollback', (self, )
669 for method, args in self.transactions:
670 # delete temporary files
671 if method == self.doStoreFile:
672 self.rollbackStoreFile(*args)
673 self.cache = {}
674 self.dirtynodes = {}
675 self.newnodes = {}
676 self.destroyednodes = {}
677 self.transactions = []
679 _marker = []
680 class Class(hyperdb.Class):
681 '''The handle to a particular class of nodes in a hyperdatabase.'''
683 def __init__(self, db, classname, **properties):
684 '''Create a new class with a given name and property specification.
686 'classname' must not collide with the name of an existing class,
687 or a ValueError is raised. The keyword arguments in 'properties'
688 must map names to property objects, or a TypeError is raised.
689 '''
690 if (properties.has_key('creation') or properties.has_key('activity')
691 or properties.has_key('creator')):
692 raise ValueError, '"creation", "activity" and "creator" are '\
693 'reserved'
695 self.classname = classname
696 self.properties = properties
697 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
698 self.key = ''
700 # should we journal changes (default yes)
701 self.do_journal = 1
703 # do the db-related init stuff
704 db.addclass(self)
706 self.auditors = {'create': [], 'set': [], 'retire': []}
707 self.reactors = {'create': [], 'set': [], 'retire': []}
709 def enableJournalling(self):
710 '''Turn journalling on for this class
711 '''
712 self.do_journal = 1
714 def disableJournalling(self):
715 '''Turn journalling off for this class
716 '''
717 self.do_journal = 0
719 # Editing nodes:
721 def create(self, **propvalues):
722 '''Create a new node of this class and return its id.
724 The keyword arguments in 'propvalues' map property names to values.
726 The values of arguments must be acceptable for the types of their
727 corresponding properties or a TypeError is raised.
729 If this class has a key property, it must be present and its value
730 must not collide with other key strings or a ValueError is raised.
732 Any other properties on this class that are missing from the
733 'propvalues' dictionary are set to None.
735 If an id in a link or multilink property does not refer to a valid
736 node, an IndexError is raised.
738 These operations trigger detectors and can be vetoed. Attempts
739 to modify the "creation" or "activity" properties cause a KeyError.
740 '''
741 if propvalues.has_key('id'):
742 raise KeyError, '"id" is reserved'
744 if self.db.journaltag is None:
745 raise DatabaseError, 'Database open read-only'
747 if propvalues.has_key('creation') or propvalues.has_key('activity'):
748 raise KeyError, '"creation" and "activity" are reserved'
750 self.fireAuditors('create', None, propvalues)
752 # new node's id
753 newid = self.db.newid(self.classname)
755 # validate propvalues
756 num_re = re.compile('^\d+$')
757 for key, value in propvalues.items():
758 if key == self.key:
759 try:
760 self.lookup(value)
761 except KeyError:
762 pass
763 else:
764 raise ValueError, 'node with key "%s" exists'%value
766 # try to handle this property
767 try:
768 prop = self.properties[key]
769 except KeyError:
770 raise KeyError, '"%s" has no property "%s"'%(self.classname,
771 key)
773 if value is not None and isinstance(prop, Link):
774 if type(value) != type(''):
775 raise ValueError, 'link value must be String'
776 link_class = self.properties[key].classname
777 # if it isn't a number, it's a key
778 if not num_re.match(value):
779 try:
780 value = self.db.classes[link_class].lookup(value)
781 except (TypeError, KeyError):
782 raise IndexError, 'new property "%s": %s not a %s'%(
783 key, value, link_class)
784 elif not self.db.getclass(link_class).hasnode(value):
785 raise IndexError, '%s has no node %s'%(link_class, value)
787 # save off the value
788 propvalues[key] = value
790 # register the link with the newly linked node
791 if self.do_journal and self.properties[key].do_journal:
792 self.db.addjournal(link_class, value, 'link',
793 (self.classname, newid, key))
795 elif isinstance(prop, Multilink):
796 if type(value) != type([]):
797 raise TypeError, 'new property "%s" not a list of ids'%key
799 # clean up and validate the list of links
800 link_class = self.properties[key].classname
801 l = []
802 for entry in value:
803 if type(entry) != type(''):
804 raise ValueError, '"%s" link value (%s) must be '\
805 'String'%(key, value)
806 # if it isn't a number, it's a key
807 if not num_re.match(entry):
808 try:
809 entry = self.db.classes[link_class].lookup(entry)
810 except (TypeError, KeyError):
811 raise IndexError, 'new property "%s": %s not a %s'%(
812 key, entry, self.properties[key].classname)
813 l.append(entry)
814 value = l
815 propvalues[key] = value
817 # handle additions
818 for nodeid in value:
819 if not self.db.getclass(link_class).hasnode(nodeid):
820 raise IndexError, '%s has no node %s'%(link_class,
821 nodeid)
822 # register the link with the newly linked node
823 if self.do_journal and self.properties[key].do_journal:
824 self.db.addjournal(link_class, nodeid, 'link',
825 (self.classname, newid, key))
827 elif isinstance(prop, String):
828 if type(value) != type(''):
829 raise TypeError, 'new property "%s" not a string'%key
831 elif isinstance(prop, Password):
832 if not isinstance(value, password.Password):
833 raise TypeError, 'new property "%s" not a Password'%key
835 elif isinstance(prop, Date):
836 if value is not None and not isinstance(value, date.Date):
837 raise TypeError, 'new property "%s" not a Date'%key
839 elif isinstance(prop, Interval):
840 if value is not None and not isinstance(value, date.Interval):
841 raise TypeError, 'new property "%s" not an Interval'%key
843 elif value is not None and isinstance(prop, Number):
844 try:
845 float(value)
846 except ValueError:
847 raise TypeError, 'new property "%s" not numeric'%key
849 elif value is not None and isinstance(prop, Boolean):
850 try:
851 int(value)
852 except ValueError:
853 raise TypeError, 'new property "%s" not boolean'%key
855 # make sure there's data where there needs to be
856 for key, prop in self.properties.items():
857 if propvalues.has_key(key):
858 continue
859 if key == self.key:
860 raise ValueError, 'key property "%s" is required'%key
861 if isinstance(prop, Multilink):
862 propvalues[key] = []
863 else:
864 propvalues[key] = None
866 # done
867 self.db.addnode(self.classname, newid, propvalues)
868 if self.do_journal:
869 self.db.addjournal(self.classname, newid, 'create', propvalues)
871 self.fireReactors('create', newid, None)
873 return newid
875 def export_list(self, propnames, nodeid):
876 ''' Export a node - generate a list of CSV-able data in the order
877 specified by propnames for the given node.
878 '''
879 properties = self.getprops()
880 l = []
881 for prop in propnames:
882 proptype = properties[prop]
883 value = self.get(nodeid, prop)
884 # "marshal" data where needed
885 if isinstance(proptype, hyperdb.Date):
886 value = value.get_tuple()
887 elif isinstance(proptype, hyperdb.Interval):
888 value = value.get_tuple()
889 elif isinstance(proptype, hyperdb.Password):
890 value = str(value)
891 l.append(repr(value))
892 return l
894 def import_list(self, propnames, proplist):
895 ''' Import a node - all information including "id" is present and
896 should not be sanity checked. Triggers are not triggered. The
897 journal should be initialised using the "creator" and "created"
898 information.
900 Return the nodeid of the node imported.
901 '''
902 if self.db.journaltag is None:
903 raise DatabaseError, 'Database open read-only'
904 properties = self.getprops()
906 # make the new node's property map
907 d = {}
908 for i in range(len(propnames)):
909 # Use eval to reverse the repr() used to output the CSV
910 value = eval(proplist[i])
912 # Figure the property for this column
913 propname = propnames[i]
914 prop = properties[propname]
916 # "unmarshal" where necessary
917 if propname == 'id':
918 newid = value
919 continue
920 elif isinstance(prop, hyperdb.Date):
921 value = date.Date(value)
922 elif isinstance(prop, hyperdb.Interval):
923 value = date.Interval(value)
924 elif isinstance(prop, hyperdb.Password):
925 pwd = password.Password()
926 pwd.unpack(value)
927 value = pwd
928 if value is not None:
929 d[propname] = value
931 # add
932 self.db.addnode(self.classname, newid, d)
933 self.db.addjournal(self.classname, newid, 'create', d)
934 return newid
936 def get(self, nodeid, propname, default=_marker, cache=1):
937 '''Get the value of a property on an existing node of this class.
939 'nodeid' must be the id of an existing node of this class or an
940 IndexError is raised. 'propname' must be the name of a property
941 of this class or a KeyError is raised.
943 'cache' indicates whether the transaction cache should be queried
944 for the node. If the node has been modified and you need to
945 determine what its values prior to modification are, you need to
946 set cache=0.
948 Attempts to get the "creation" or "activity" properties should
949 do the right thing.
950 '''
951 if propname == 'id':
952 return nodeid
954 if propname == 'creation':
955 if not self.do_journal:
956 raise ValueError, 'Journalling is disabled for this class'
957 journal = self.db.getjournal(self.classname, nodeid)
958 if journal:
959 return self.db.getjournal(self.classname, nodeid)[0][1]
960 else:
961 # on the strange chance that there's no journal
962 return date.Date()
963 if propname == 'activity':
964 if not self.do_journal:
965 raise ValueError, 'Journalling is disabled for this class'
966 journal = self.db.getjournal(self.classname, nodeid)
967 if journal:
968 return self.db.getjournal(self.classname, nodeid)[-1][1]
969 else:
970 # on the strange chance that there's no journal
971 return date.Date()
972 if propname == 'creator':
973 if not self.do_journal:
974 raise ValueError, 'Journalling is disabled for this class'
975 journal = self.db.getjournal(self.classname, nodeid)
976 if journal:
977 name = self.db.getjournal(self.classname, nodeid)[0][2]
978 else:
979 return None
980 return self.db.user.lookup(name)
982 # get the property (raises KeyErorr if invalid)
983 prop = self.properties[propname]
985 # get the node's dict
986 d = self.db.getnode(self.classname, nodeid, cache=cache)
988 if not d.has_key(propname):
989 if default is _marker:
990 if isinstance(prop, Multilink):
991 return []
992 else:
993 return None
994 else:
995 return default
997 return d[propname]
999 # XXX not in spec
1000 def getnode(self, nodeid, cache=1):
1001 ''' Return a convenience wrapper for the node.
1003 'nodeid' must be the id of an existing node of this class or an
1004 IndexError is raised.
1006 'cache' indicates whether the transaction cache should be queried
1007 for the node. If the node has been modified and you need to
1008 determine what its values prior to modification are, you need to
1009 set cache=0.
1010 '''
1011 return Node(self, nodeid, cache=cache)
1013 def set(self, nodeid, **propvalues):
1014 '''Modify a property on an existing node of this class.
1016 'nodeid' must be the id of an existing node of this class or an
1017 IndexError is raised.
1019 Each key in 'propvalues' must be the name of a property of this
1020 class or a KeyError is raised.
1022 All values in 'propvalues' must be acceptable types for their
1023 corresponding properties or a TypeError is raised.
1025 If the value of the key property is set, it must not collide with
1026 other key strings or a ValueError is raised.
1028 If the value of a Link or Multilink property contains an invalid
1029 node id, a ValueError is raised.
1031 These operations trigger detectors and can be vetoed. Attempts
1032 to modify the "creation" or "activity" properties cause a KeyError.
1033 '''
1034 if not propvalues:
1035 return propvalues
1037 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1038 raise KeyError, '"creation" and "activity" are reserved'
1040 if propvalues.has_key('id'):
1041 raise KeyError, '"id" is reserved'
1043 if self.db.journaltag is None:
1044 raise DatabaseError, 'Database open read-only'
1046 self.fireAuditors('set', nodeid, propvalues)
1047 # Take a copy of the node dict so that the subsequent set
1048 # operation doesn't modify the oldvalues structure.
1049 try:
1050 # try not using the cache initially
1051 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
1052 cache=0))
1053 except IndexError:
1054 # this will be needed if somone does a create() and set()
1055 # with no intervening commit()
1056 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1058 node = self.db.getnode(self.classname, nodeid)
1059 if node.has_key(self.db.RETIRED_FLAG):
1060 raise IndexError
1061 num_re = re.compile('^\d+$')
1063 # if the journal value is to be different, store it in here
1064 journalvalues = {}
1066 for propname, value in propvalues.items():
1067 # check to make sure we're not duplicating an existing key
1068 if propname == self.key and node[propname] != value:
1069 try:
1070 self.lookup(value)
1071 except KeyError:
1072 pass
1073 else:
1074 raise ValueError, 'node with key "%s" exists'%value
1076 # this will raise the KeyError if the property isn't valid
1077 # ... we don't use getprops() here because we only care about
1078 # the writeable properties.
1079 prop = self.properties[propname]
1081 # if the value's the same as the existing value, no sense in
1082 # doing anything
1083 if node.has_key(propname) and value == node[propname]:
1084 del propvalues[propname]
1085 continue
1087 # do stuff based on the prop type
1088 if isinstance(prop, Link):
1089 link_class = prop.classname
1090 # if it isn't a number, it's a key
1091 if value is not None and not isinstance(value, type('')):
1092 raise ValueError, 'property "%s" link value be a string'%(
1093 propname)
1094 if isinstance(value, type('')) and not num_re.match(value):
1095 try:
1096 value = self.db.classes[link_class].lookup(value)
1097 except (TypeError, KeyError):
1098 raise IndexError, 'new property "%s": %s not a %s'%(
1099 propname, value, prop.classname)
1101 if (value is not None and
1102 not self.db.getclass(link_class).hasnode(value)):
1103 raise IndexError, '%s has no node %s'%(link_class, value)
1105 if self.do_journal and prop.do_journal:
1106 # register the unlink with the old linked node
1107 if node[propname] is not None:
1108 self.db.addjournal(link_class, node[propname], 'unlink',
1109 (self.classname, nodeid, propname))
1111 # register the link with the newly linked node
1112 if value is not None:
1113 self.db.addjournal(link_class, value, 'link',
1114 (self.classname, nodeid, propname))
1116 elif isinstance(prop, Multilink):
1117 if type(value) != type([]):
1118 raise TypeError, 'new property "%s" not a list of'\
1119 ' ids'%propname
1120 link_class = self.properties[propname].classname
1121 l = []
1122 for entry in value:
1123 # if it isn't a number, it's a key
1124 if type(entry) != type(''):
1125 raise ValueError, 'new property "%s" link value ' \
1126 'must be a string'%propname
1127 if not num_re.match(entry):
1128 try:
1129 entry = self.db.classes[link_class].lookup(entry)
1130 except (TypeError, KeyError):
1131 raise IndexError, 'new property "%s": %s not a %s'%(
1132 propname, entry,
1133 self.properties[propname].classname)
1134 l.append(entry)
1135 value = l
1136 propvalues[propname] = value
1138 # figure the journal entry for this property
1139 add = []
1140 remove = []
1142 # handle removals
1143 if node.has_key(propname):
1144 l = node[propname]
1145 else:
1146 l = []
1147 for id in l[:]:
1148 if id in value:
1149 continue
1150 # register the unlink with the old linked node
1151 if self.do_journal and self.properties[propname].do_journal:
1152 self.db.addjournal(link_class, id, 'unlink',
1153 (self.classname, nodeid, propname))
1154 l.remove(id)
1155 remove.append(id)
1157 # handle additions
1158 for id in value:
1159 if not self.db.getclass(link_class).hasnode(id):
1160 raise IndexError, '%s has no node %s'%(link_class, id)
1161 if id in l:
1162 continue
1163 # register the link with the newly linked node
1164 if self.do_journal and self.properties[propname].do_journal:
1165 self.db.addjournal(link_class, id, 'link',
1166 (self.classname, nodeid, propname))
1167 l.append(id)
1168 add.append(id)
1170 # figure the journal entry
1171 l = []
1172 if add:
1173 l.append(('+', add))
1174 if remove:
1175 l.append(('-', remove))
1176 if l:
1177 journalvalues[propname] = tuple(l)
1179 elif isinstance(prop, String):
1180 if value is not None and type(value) != type(''):
1181 raise TypeError, 'new property "%s" not a string'%propname
1183 elif isinstance(prop, Password):
1184 if not isinstance(value, password.Password):
1185 raise TypeError, 'new property "%s" not a Password'%propname
1186 propvalues[propname] = value
1188 elif value is not None and isinstance(prop, Date):
1189 if not isinstance(value, date.Date):
1190 raise TypeError, 'new property "%s" not a Date'% propname
1191 propvalues[propname] = value
1193 elif value is not None and isinstance(prop, Interval):
1194 if not isinstance(value, date.Interval):
1195 raise TypeError, 'new property "%s" not an '\
1196 'Interval'%propname
1197 propvalues[propname] = value
1199 elif value is not None and isinstance(prop, Number):
1200 try:
1201 float(value)
1202 except ValueError:
1203 raise TypeError, 'new property "%s" not numeric'%propname
1205 elif value is not None and isinstance(prop, Boolean):
1206 try:
1207 int(value)
1208 except ValueError:
1209 raise TypeError, 'new property "%s" not boolean'%propname
1211 node[propname] = value
1213 # nothing to do?
1214 if not propvalues:
1215 return propvalues
1217 # do the set, and journal it
1218 self.db.setnode(self.classname, nodeid, node)
1220 if self.do_journal:
1221 propvalues.update(journalvalues)
1222 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
1224 self.fireReactors('set', nodeid, oldvalues)
1226 return propvalues
1228 def retire(self, nodeid):
1229 '''Retire a node.
1231 The properties on the node remain available from the get() method,
1232 and the node's id is never reused.
1234 Retired nodes are not returned by the find(), list(), or lookup()
1235 methods, and other nodes may reuse the values of their key properties.
1237 These operations trigger detectors and can be vetoed. Attempts
1238 to modify the "creation" or "activity" properties cause a KeyError.
1239 '''
1240 if self.db.journaltag is None:
1241 raise DatabaseError, 'Database open read-only'
1243 self.fireAuditors('retire', nodeid, None)
1245 node = self.db.getnode(self.classname, nodeid)
1246 node[self.db.RETIRED_FLAG] = 1
1247 self.db.setnode(self.classname, nodeid, node)
1248 if self.do_journal:
1249 self.db.addjournal(self.classname, nodeid, 'retired', None)
1251 self.fireReactors('retire', nodeid, None)
1253 def is_retired(self, nodeid):
1254 '''Return true if the node is retired.
1255 '''
1256 node = self.db.getnode(cn, nodeid, cldb)
1257 if node.has_key(self.db.RETIRED_FLAG):
1258 return 1
1259 return 0
1261 def destroy(self, nodeid):
1262 '''Destroy a node.
1264 WARNING: this method should never be used except in extremely rare
1265 situations where there could never be links to the node being
1266 deleted
1267 WARNING: use retire() instead
1268 WARNING: the properties of this node will not be available ever again
1269 WARNING: really, use retire() instead
1271 Well, I think that's enough warnings. This method exists mostly to
1272 support the session storage of the cgi interface.
1273 '''
1274 if self.db.journaltag is None:
1275 raise DatabaseError, 'Database open read-only'
1276 self.db.destroynode(self.classname, nodeid)
1278 def history(self, nodeid):
1279 '''Retrieve the journal of edits on a particular node.
1281 'nodeid' must be the id of an existing node of this class or an
1282 IndexError is raised.
1284 The returned list contains tuples of the form
1286 (date, tag, action, params)
1288 'date' is a Timestamp object specifying the time of the change and
1289 'tag' is the journaltag specified when the database was opened.
1290 '''
1291 if not self.do_journal:
1292 raise ValueError, 'Journalling is disabled for this class'
1293 return self.db.getjournal(self.classname, nodeid)
1295 # Locating nodes:
1296 def hasnode(self, nodeid):
1297 '''Determine if the given nodeid actually exists
1298 '''
1299 return self.db.hasnode(self.classname, nodeid)
1301 def setkey(self, propname):
1302 '''Select a String property of this class to be the key property.
1304 'propname' must be the name of a String property of this class or
1305 None, or a TypeError is raised. The values of the key property on
1306 all existing nodes must be unique or a ValueError is raised. If the
1307 property doesn't exist, KeyError is raised.
1308 '''
1309 prop = self.getprops()[propname]
1310 if not isinstance(prop, String):
1311 raise TypeError, 'key properties must be String'
1312 self.key = propname
1314 def getkey(self):
1315 '''Return the name of the key property for this class or None.'''
1316 return self.key
1318 def labelprop(self, default_to_id=0):
1319 ''' Return the property name for a label for the given node.
1321 This method attempts to generate a consistent label for the node.
1322 It tries the following in order:
1323 1. key property
1324 2. "name" property
1325 3. "title" property
1326 4. first property from the sorted property name list
1327 '''
1328 k = self.getkey()
1329 if k:
1330 return k
1331 props = self.getprops()
1332 if props.has_key('name'):
1333 return 'name'
1334 elif props.has_key('title'):
1335 return 'title'
1336 if default_to_id:
1337 return 'id'
1338 props = props.keys()
1339 props.sort()
1340 return props[0]
1342 # TODO: set up a separate index db file for this? profile?
1343 def lookup(self, keyvalue):
1344 '''Locate a particular node by its key property and return its id.
1346 If this class has no key property, a TypeError is raised. If the
1347 'keyvalue' matches one of the values for the key property among
1348 the nodes in this class, the matching node's id is returned;
1349 otherwise a KeyError is raised.
1350 '''
1351 if not self.key:
1352 raise TypeError, 'No key property set'
1353 cldb = self.db.getclassdb(self.classname)
1354 try:
1355 for nodeid in self.db.getnodeids(self.classname, cldb):
1356 node = self.db.getnode(self.classname, nodeid, cldb)
1357 if node.has_key(self.db.RETIRED_FLAG):
1358 continue
1359 if node[self.key] == keyvalue:
1360 cldb.close()
1361 return nodeid
1362 finally:
1363 cldb.close()
1364 raise KeyError, keyvalue
1366 # XXX: change from spec - allows multiple props to match
1367 def find(self, **propspec):
1368 '''Get the ids of nodes in this class which link to the given nodes.
1370 'propspec' consists of keyword args propname={nodeid:1,}
1371 'propname' must be the name of a property in this class, or a
1372 KeyError is raised. That property must be a Link or Multilink
1373 property, or a TypeError is raised.
1375 Any node in this class whose 'propname' property links to any of the
1376 nodeids will be returned. Used by the full text indexing, which knows
1377 that "foo" occurs in msg1, msg3 and file7, so we have hits on these issues:
1378 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1379 '''
1380 propspec = propspec.items()
1381 for propname, nodeids in propspec:
1382 # check the prop is OK
1383 prop = self.properties[propname]
1384 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1385 raise TypeError, "'%s' not a Link/Multilink property"%propname
1387 # ok, now do the find
1388 cldb = self.db.getclassdb(self.classname)
1389 l = []
1390 try:
1391 for id in self.db.getnodeids(self.classname, db=cldb):
1392 node = self.db.getnode(self.classname, id, db=cldb)
1393 if node.has_key(self.db.RETIRED_FLAG):
1394 continue
1395 for propname, nodeids in propspec:
1396 # can't test if the node doesn't have this property
1397 if not node.has_key(propname):
1398 continue
1399 if type(nodeids) is type(''):
1400 nodeids = {nodeids:1}
1401 prop = self.properties[propname]
1402 value = node[propname]
1403 if isinstance(prop, Link) and nodeids.has_key(value):
1404 l.append(id)
1405 break
1406 elif isinstance(prop, Multilink):
1407 hit = 0
1408 for v in value:
1409 if nodeids.has_key(v):
1410 l.append(id)
1411 hit = 1
1412 break
1413 if hit:
1414 break
1415 finally:
1416 cldb.close()
1417 return l
1419 def stringFind(self, **requirements):
1420 '''Locate a particular node by matching a set of its String
1421 properties in a caseless search.
1423 If the property is not a String property, a TypeError is raised.
1425 The return is a list of the id of all nodes that match.
1426 '''
1427 for propname in requirements.keys():
1428 prop = self.properties[propname]
1429 if isinstance(not prop, String):
1430 raise TypeError, "'%s' not a String property"%propname
1431 requirements[propname] = requirements[propname].lower()
1432 l = []
1433 cldb = self.db.getclassdb(self.classname)
1434 try:
1435 for nodeid in self.db.getnodeids(self.classname, cldb):
1436 node = self.db.getnode(self.classname, nodeid, cldb)
1437 if node.has_key(self.db.RETIRED_FLAG):
1438 continue
1439 for key, value in requirements.items():
1440 if node[key] is None or node[key].lower() != value:
1441 break
1442 else:
1443 l.append(nodeid)
1444 finally:
1445 cldb.close()
1446 return l
1448 def list(self):
1449 ''' Return a list of the ids of the active nodes in this class.
1450 '''
1451 l = []
1452 cn = self.classname
1453 cldb = self.db.getclassdb(cn)
1454 try:
1455 for nodeid in self.db.getnodeids(cn, cldb):
1456 node = self.db.getnode(cn, nodeid, cldb)
1457 if node.has_key(self.db.RETIRED_FLAG):
1458 continue
1459 l.append(nodeid)
1460 finally:
1461 cldb.close()
1462 l.sort()
1463 return l
1465 def filter(self, search_matches, filterspec, sort, group,
1466 num_re = re.compile('^\d+$')):
1467 ''' Return a list of the ids of the active nodes in this class that
1468 match the 'filter' spec, sorted by the group spec and then the
1469 sort spec.
1471 "filterspec" is {propname: value(s)}
1472 "sort" is ['+propname', '-propname', 'propname', ...]
1473 "group is ['+propname', '-propname', 'propname', ...]
1474 '''
1475 cn = self.classname
1477 # optimise filterspec
1478 l = []
1479 props = self.getprops()
1480 LINK = 0
1481 MULTILINK = 1
1482 STRING = 2
1483 OTHER = 6
1484 for k, v in filterspec.items():
1485 propclass = props[k]
1486 if isinstance(propclass, Link):
1487 if type(v) is not type([]):
1488 v = [v]
1489 # replace key values with node ids
1490 u = []
1491 link_class = self.db.classes[propclass.classname]
1492 for entry in v:
1493 if entry == '-1': entry = None
1494 elif not num_re.match(entry):
1495 try:
1496 entry = link_class.lookup(entry)
1497 except (TypeError,KeyError):
1498 raise ValueError, 'property "%s": %s not a %s'%(
1499 k, entry, self.properties[k].classname)
1500 u.append(entry)
1502 l.append((LINK, k, u))
1503 elif isinstance(propclass, Multilink):
1504 if type(v) is not type([]):
1505 v = [v]
1506 # replace key values with node ids
1507 u = []
1508 link_class = self.db.classes[propclass.classname]
1509 for entry in v:
1510 if not num_re.match(entry):
1511 try:
1512 entry = link_class.lookup(entry)
1513 except (TypeError,KeyError):
1514 raise ValueError, 'new property "%s": %s not a %s'%(
1515 k, entry, self.properties[k].classname)
1516 u.append(entry)
1517 l.append((MULTILINK, k, u))
1518 elif isinstance(propclass, String):
1519 # simple glob searching
1520 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
1521 v = v.replace('?', '.')
1522 v = v.replace('*', '.*?')
1523 l.append((STRING, k, re.compile(v, re.I)))
1524 elif isinstance(propclass, Boolean):
1525 if type(v) is type(''):
1526 bv = v.lower() in ('yes', 'true', 'on', '1')
1527 else:
1528 bv = v
1529 l.append((OTHER, k, bv))
1530 elif isinstance(propclass, Number):
1531 l.append((OTHER, k, int(v)))
1532 else:
1533 l.append((OTHER, k, v))
1534 filterspec = l
1536 # now, find all the nodes that are active and pass filtering
1537 l = []
1538 cldb = self.db.getclassdb(cn)
1539 try:
1540 # TODO: only full-scan once (use items())
1541 for nodeid in self.db.getnodeids(cn, cldb):
1542 node = self.db.getnode(cn, nodeid, cldb)
1543 if node.has_key(self.db.RETIRED_FLAG):
1544 continue
1545 # apply filter
1546 for t, k, v in filterspec:
1547 # make sure the node has the property
1548 if not node.has_key(k):
1549 # this node doesn't have this property, so reject it
1550 break
1552 # now apply the property filter
1553 if t == LINK:
1554 # link - if this node's property doesn't appear in the
1555 # filterspec's nodeid list, skip it
1556 if node[k] not in v:
1557 break
1558 elif t == MULTILINK:
1559 # multilink - if any of the nodeids required by the
1560 # filterspec aren't in this node's property, then skip
1561 # it
1562 have = node[k]
1563 for want in v:
1564 if want not in have:
1565 break
1566 else:
1567 continue
1568 break
1569 elif t == STRING:
1570 # RE search
1571 if node[k] is None or not v.search(node[k]):
1572 break
1573 elif t == OTHER:
1574 # straight value comparison for the other types
1575 if node[k] != v:
1576 break
1577 else:
1578 l.append((nodeid, node))
1579 finally:
1580 cldb.close()
1581 l.sort()
1583 # filter based on full text search
1584 if search_matches is not None:
1585 k = []
1586 for v in l:
1587 if search_matches.has_key(v[0]):
1588 k.append(v)
1589 l = k
1591 # optimise sort
1592 m = []
1593 for entry in sort:
1594 if entry[0] != '-':
1595 m.append(('+', entry))
1596 else:
1597 m.append((entry[0], entry[1:]))
1598 sort = m
1600 # optimise group
1601 m = []
1602 for entry in group:
1603 if entry[0] != '-':
1604 m.append(('+', entry))
1605 else:
1606 m.append((entry[0], entry[1:]))
1607 group = m
1608 # now, sort the result
1609 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
1610 db = self.db, cl=self):
1611 a_id, an = a
1612 b_id, bn = b
1613 # sort by group and then sort
1614 for list in group, sort:
1615 for dir, prop in list:
1616 # sorting is class-specific
1617 propclass = properties[prop]
1619 # handle the properties that might be "faked"
1620 # also, handle possible missing properties
1621 try:
1622 if not an.has_key(prop):
1623 an[prop] = cl.get(a_id, prop)
1624 av = an[prop]
1625 except KeyError:
1626 # the node doesn't have a value for this property
1627 if isinstance(propclass, Multilink): av = []
1628 else: av = ''
1629 try:
1630 if not bn.has_key(prop):
1631 bn[prop] = cl.get(b_id, prop)
1632 bv = bn[prop]
1633 except KeyError:
1634 # the node doesn't have a value for this property
1635 if isinstance(propclass, Multilink): bv = []
1636 else: bv = ''
1638 # String and Date values are sorted in the natural way
1639 if isinstance(propclass, String):
1640 # clean up the strings
1641 if av and av[0] in string.uppercase:
1642 av = an[prop] = av.lower()
1643 if bv and bv[0] in string.uppercase:
1644 bv = bn[prop] = bv.lower()
1645 if (isinstance(propclass, String) or
1646 isinstance(propclass, Date)):
1647 # it might be a string that's really an integer
1648 try:
1649 av = int(av)
1650 bv = int(bv)
1651 except:
1652 pass
1653 if dir == '+':
1654 r = cmp(av, bv)
1655 if r != 0: return r
1656 elif dir == '-':
1657 r = cmp(bv, av)
1658 if r != 0: return r
1660 # Link properties are sorted according to the value of
1661 # the "order" property on the linked nodes if it is
1662 # present; or otherwise on the key string of the linked
1663 # nodes; or finally on the node ids.
1664 elif isinstance(propclass, Link):
1665 link = db.classes[propclass.classname]
1666 if av is None and bv is not None: return -1
1667 if av is not None and bv is None: return 1
1668 if av is None and bv is None: continue
1669 if link.getprops().has_key('order'):
1670 if dir == '+':
1671 r = cmp(link.get(av, 'order'),
1672 link.get(bv, 'order'))
1673 if r != 0: return r
1674 elif dir == '-':
1675 r = cmp(link.get(bv, 'order'),
1676 link.get(av, 'order'))
1677 if r != 0: return r
1678 elif link.getkey():
1679 key = link.getkey()
1680 if dir == '+':
1681 r = cmp(link.get(av, key), link.get(bv, key))
1682 if r != 0: return r
1683 elif dir == '-':
1684 r = cmp(link.get(bv, key), link.get(av, key))
1685 if r != 0: return r
1686 else:
1687 if dir == '+':
1688 r = cmp(av, bv)
1689 if r != 0: return r
1690 elif dir == '-':
1691 r = cmp(bv, av)
1692 if r != 0: return r
1694 # Multilink properties are sorted according to how many
1695 # links are present.
1696 elif isinstance(propclass, Multilink):
1697 if dir == '+':
1698 r = cmp(len(av), len(bv))
1699 if r != 0: return r
1700 elif dir == '-':
1701 r = cmp(len(bv), len(av))
1702 if r != 0: return r
1703 elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
1704 if dir == '+':
1705 r = cmp(av, bv)
1706 elif dir == '-':
1707 r = cmp(bv, av)
1709 # end for dir, prop in list:
1710 # end for list in sort, group:
1711 # if all else fails, compare the ids
1712 return cmp(a[0], b[0])
1714 l.sort(sortfun)
1715 return [i[0] for i in l]
1717 def count(self):
1718 '''Get the number of nodes in this class.
1720 If the returned integer is 'numnodes', the ids of all the nodes
1721 in this class run from 1 to numnodes, and numnodes+1 will be the
1722 id of the next node to be created in this class.
1723 '''
1724 return self.db.countnodes(self.classname)
1726 # Manipulating properties:
1728 def getprops(self, protected=1):
1729 '''Return a dictionary mapping property names to property objects.
1730 If the "protected" flag is true, we include protected properties -
1731 those which may not be modified.
1733 In addition to the actual properties on the node, these
1734 methods provide the "creation" and "activity" properties. If the
1735 "protected" flag is true, we include protected properties - those
1736 which may not be modified.
1737 '''
1738 d = self.properties.copy()
1739 if protected:
1740 d['id'] = String()
1741 d['creation'] = hyperdb.Date()
1742 d['activity'] = hyperdb.Date()
1743 d['creator'] = hyperdb.Link("user")
1744 return d
1746 def addprop(self, **properties):
1747 '''Add properties to this class.
1749 The keyword arguments in 'properties' must map names to property
1750 objects, or a TypeError is raised. None of the keys in 'properties'
1751 may collide with the names of existing properties, or a ValueError
1752 is raised before any properties have been added.
1753 '''
1754 for key in properties.keys():
1755 if self.properties.has_key(key):
1756 raise ValueError, key
1757 self.properties.update(properties)
1759 def index(self, nodeid):
1760 '''Add (or refresh) the node to search indexes
1761 '''
1762 # find all the String properties that have indexme
1763 for prop, propclass in self.getprops().items():
1764 if isinstance(propclass, String) and propclass.indexme:
1765 try:
1766 value = str(self.get(nodeid, prop))
1767 except IndexError:
1768 # node no longer exists - entry should be removed
1769 self.db.indexer.purge_entry((self.classname, nodeid, prop))
1770 else:
1771 # and index them under (classname, nodeid, property)
1772 self.db.indexer.add_text((self.classname, nodeid, prop),
1773 value)
1775 #
1776 # Detector interface
1777 #
1778 def audit(self, event, detector):
1779 '''Register a detector
1780 '''
1781 l = self.auditors[event]
1782 if detector not in l:
1783 self.auditors[event].append(detector)
1785 def fireAuditors(self, action, nodeid, newvalues):
1786 '''Fire all registered auditors.
1787 '''
1788 for audit in self.auditors[action]:
1789 audit(self.db, self, nodeid, newvalues)
1791 def react(self, event, detector):
1792 '''Register a detector
1793 '''
1794 l = self.reactors[event]
1795 if detector not in l:
1796 self.reactors[event].append(detector)
1798 def fireReactors(self, action, nodeid, oldvalues):
1799 '''Fire all registered reactors.
1800 '''
1801 for react in self.reactors[action]:
1802 react(self.db, self, nodeid, oldvalues)
1804 class FileClass(Class):
1805 '''This class defines a large chunk of data. To support this, it has a
1806 mandatory String property "content" which is typically saved off
1807 externally to the hyperdb.
1809 The default MIME type of this data is defined by the
1810 "default_mime_type" class attribute, which may be overridden by each
1811 node if the class defines a "type" String property.
1812 '''
1813 default_mime_type = 'text/plain'
1815 def create(self, **propvalues):
1816 ''' snaffle the file propvalue and store in a file
1817 '''
1818 content = propvalues['content']
1819 del propvalues['content']
1820 newid = Class.create(self, **propvalues)
1821 self.db.storefile(self.classname, newid, None, content)
1822 return newid
1824 def import_list(self, propnames, proplist):
1825 ''' Trap the "content" property...
1826 '''
1827 # dupe this list so we don't affect others
1828 propnames = propnames[:]
1830 # extract the "content" property from the proplist
1831 i = propnames.index('content')
1832 content = proplist[i]
1833 del propnames[i]
1834 del proplist[i]
1836 # do the normal import
1837 newid = Class.import_list(self, propnames, proplist)
1839 # save off the "content" file
1840 self.db.storefile(self.classname, newid, None, content)
1841 return newid
1843 def get(self, nodeid, propname, default=_marker, cache=1):
1844 ''' trap the content propname and get it from the file
1845 '''
1847 poss_msg = 'Possibly a access right configuration problem.'
1848 if propname == 'content':
1849 try:
1850 return self.db.getfile(self.classname, nodeid, None)
1851 except IOError, (strerror):
1852 # BUG: by catching this we donot see an error in the log.
1853 return 'ERROR reading file: %s%s\n%s\n%s'%(
1854 self.classname, nodeid, poss_msg, strerror)
1855 if default is not _marker:
1856 return Class.get(self, nodeid, propname, default, cache=cache)
1857 else:
1858 return Class.get(self, nodeid, propname, cache=cache)
1860 def getprops(self, protected=1):
1861 ''' In addition to the actual properties on the node, these methods
1862 provide the "content" property. If the "protected" flag is true,
1863 we include protected properties - those which may not be
1864 modified.
1865 '''
1866 d = Class.getprops(self, protected=protected).copy()
1867 if protected:
1868 d['content'] = hyperdb.String()
1869 return d
1871 def index(self, nodeid):
1872 ''' Index the node in the search index.
1874 We want to index the content in addition to the normal String
1875 property indexing.
1876 '''
1877 # perform normal indexing
1878 Class.index(self, nodeid)
1880 # get the content to index
1881 content = self.get(nodeid, 'content')
1883 # figure the mime type
1884 if self.properties.has_key('type'):
1885 mime_type = self.get(nodeid, 'type')
1886 else:
1887 mime_type = self.default_mime_type
1889 # and index!
1890 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
1891 mime_type)
1893 # XXX deviation from spec - was called ItemClass
1894 class IssueClass(Class, roundupdb.IssueClass):
1895 # Overridden methods:
1896 def __init__(self, db, classname, **properties):
1897 '''The newly-created class automatically includes the "messages",
1898 "files", "nosy", and "superseder" properties. If the 'properties'
1899 dictionary attempts to specify any of these properties or a
1900 "creation" or "activity" property, a ValueError is raised.
1901 '''
1902 if not properties.has_key('title'):
1903 properties['title'] = hyperdb.String(indexme='yes')
1904 if not properties.has_key('messages'):
1905 properties['messages'] = hyperdb.Multilink("msg")
1906 if not properties.has_key('files'):
1907 properties['files'] = hyperdb.Multilink("file")
1908 if not properties.has_key('nosy'):
1909 properties['nosy'] = hyperdb.Multilink("user")
1910 if not properties.has_key('superseder'):
1911 properties['superseder'] = hyperdb.Multilink(classname)
1912 Class.__init__(self, db, classname, **properties)
1914 #
1915 #$Log: not supported by cvs2svn $
1916 #Revision 1.63 2002/08/22 04:42:28 richard
1917 #use more robust date stamp comparisons in pack(), make journal smaller too
1918 #
1919 #Revision 1.62 2002/08/21 07:07:27 richard
1920 #In preparing to turn back on link/unlink journal events (by default these
1921 #are turned off) I've:
1922 #- fixed back_anydbm so it can journal those events again (had broken it
1923 # with recent changes)
1924 #- changed the serialisation format for dates and intervals to use a
1925 # numbers-only (and sign for Intervals) string instead of tuple-of-ints.
1926 # Much smaller.
1927 #
1928 #Revision 1.61 2002/08/19 02:53:27 richard
1929 #full database export and import is done
1930 #
1931 #Revision 1.60 2002/08/19 00:23:19 richard
1932 #handle "unset" initial Link values (!)
1933 #
1934 #Revision 1.59 2002/08/16 04:28:13 richard
1935 #added is_retired query to Class
1936 #
1937 #Revision 1.58 2002/08/01 15:06:24 gmcm
1938 #Use same regex to split search terms as used to index text.
1939 #Fix to back_metakit for not changing journaltag on reopen.
1940 #Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
1941 #Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
1942 #
1943 #Revision 1.57 2002/07/31 23:57:36 richard
1944 # . web forms may now unset Link values (like assignedto)
1945 #
1946 #Revision 1.56 2002/07/31 22:04:33 richard
1947 #cleanup
1948 #
1949 #Revision 1.55 2002/07/30 08:22:38 richard
1950 #Session storage in the hyperdb was horribly, horribly inefficient. We use
1951 #a simple anydbm wrapper now - which could be overridden by the metakit
1952 #backend or RDB backend if necessary.
1953 #Much, much better.
1954 #
1955 #Revision 1.54 2002/07/26 08:26:59 richard
1956 #Very close now. The cgi and mailgw now use the new security API. The two
1957 #templates have been migrated to that setup. Lots of unit tests. Still some
1958 #issue in the web form for editing Roles assigned to users.
1959 #
1960 #Revision 1.53 2002/07/25 07:14:06 richard
1961 #Bugger it. Here's the current shape of the new security implementation.
1962 #Still to do:
1963 # . call the security funcs from cgi and mailgw
1964 # . change shipped templates to include correct initialisation and remove
1965 # the old config vars
1966 #... that seems like a lot. The bulk of the work has been done though. Honest :)
1967 #
1968 #Revision 1.52 2002/07/19 03:36:34 richard
1969 #Implemented the destroy() method needed by the session database (and possibly
1970 #others). At the same time, I removed the leading underscores from the hyperdb
1971 #methods that Really Didn't Need Them.
1972 #The journal also raises IndexError now for all situations where there is a
1973 #request for the journal of a node that doesn't have one. It used to return
1974 #[] in _some_ situations, but not all. This _may_ break code, but the tests
1975 #pass...
1976 #
1977 #Revision 1.51 2002/07/18 23:07:08 richard
1978 #Unit tests and a few fixes.
1979 #
1980 #Revision 1.50 2002/07/18 11:50:58 richard
1981 #added tests for number type too
1982 #
1983 #Revision 1.49 2002/07/18 11:41:10 richard
1984 #added tests for boolean type, and fixes to anydbm backend
1985 #
1986 #Revision 1.48 2002/07/18 11:17:31 gmcm
1987 #Add Number and Boolean types to hyperdb.
1988 #Add conversion cases to web, mail & admin interfaces.
1989 #Add storage/serialization cases to back_anydbm & back_metakit.
1990 #
1991 #Revision 1.47 2002/07/14 23:18:20 richard
1992 #. fixed the journal bloat from multilink changes - we just log the add or
1993 # remove operations, not the whole list
1994 #
1995 #Revision 1.46 2002/07/14 06:06:34 richard
1996 #Did some old TODOs
1997 #
1998 #Revision 1.45 2002/07/14 04:03:14 richard
1999 #Implemented a switch to disable journalling for a Class. CGI session
2000 #database now uses it.
2001 #
2002 #Revision 1.44 2002/07/14 02:05:53 richard
2003 #. all storage-specific code (ie. backend) is now implemented by the backends
2004 #
2005 #Revision 1.43 2002/07/10 06:30:30 richard
2006 #...except of course it's nice to use valid Python syntax
2007 #
2008 #Revision 1.42 2002/07/10 06:21:38 richard
2009 #Be extra safe
2010 #
2011 #Revision 1.41 2002/07/10 00:21:45 richard
2012 #explicit database closing
2013 #
2014 #Revision 1.40 2002/07/09 04:19:09 richard
2015 #Added reindex command to roundup-admin.
2016 #Fixed reindex on first access.
2017 #Also fixed reindexing of entries that change.
2018 #
2019 #Revision 1.39 2002/07/09 03:02:52 richard
2020 #More indexer work:
2021 #- all String properties may now be indexed too. Currently there's a bit of
2022 # "issue" specific code in the actual searching which needs to be
2023 # addressed. In a nutshell:
2024 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
2025 # file = FileClass(db, "file", name=String(), type=String(),
2026 # comment=String(indexme="yes"))
2027 # + the comment will then be indexed and be searchable, with the results
2028 # related back to the issue that the file is linked to
2029 #- as a result of this work, the FileClass has a default MIME type that may
2030 # be overridden in a subclass, or by the use of a "type" property as is
2031 # done in the default templates.
2032 #- the regeneration of the indexes (if necessary) is done once the schema is
2033 # set up in the dbinit.
2034 #
2035 #Revision 1.38 2002/07/08 06:58:15 richard
2036 #cleaned up the indexer code:
2037 # - it splits more words out (much simpler, faster splitter)
2038 # - removed code we'll never use (roundup.roundup_indexer has the full
2039 # implementation, and replaces roundup.indexer)
2040 # - only index text/plain and rfc822/message (ideas for other text formats to
2041 # index are welcome)
2042 # - added simple unit test for indexer. Needs more tests for regression.
2043 #
2044 #Revision 1.37 2002/06/20 23:52:35 richard
2045 #More informative error message
2046 #
2047 #Revision 1.36 2002/06/19 03:07:19 richard
2048 #Moved the file storage commit into blobfiles where it belongs.
2049 #
2050 #Revision 1.35 2002/05/25 07:16:24 rochecompaan
2051 #Merged search_indexing-branch with HEAD
2052 #
2053 #Revision 1.34 2002/05/15 06:21:21 richard
2054 # . node caching now works, and gives a small boost in performance
2055 #
2056 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
2057 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
2058 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
2059 #(using if __debug__ which is compiled out with -O)
2060 #
2061 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
2062 #All database files are now created group readable and writable.
2063 #
2064 #Revision 1.32 2002/04/15 23:25:15 richard
2065 #. node ids are now generated from a lockable store - no more race conditions
2066 #
2067 #We're using the portalocker code by Jonathan Feinberg that was contributed
2068 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
2069 #
2070 #Revision 1.31 2002/04/03 05:54:31 richard
2071 #Fixed serialisation problem by moving the serialisation step out of the
2072 #hyperdb.Class (get, set) into the hyperdb.Database.
2073 #
2074 #Also fixed htmltemplate after the showid changes I made yesterday.
2075 #
2076 #Unit tests for all of the above written.
2077 #
2078 #Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
2079 # . Added feature #526730 - search for messages capability
2080 #
2081 #Revision 1.30 2002/02/27 03:40:59 richard
2082 #Ran it through pychecker, made fixes
2083 #
2084 #Revision 1.29 2002/02/25 14:34:31 grubert
2085 # . use blobfiles in back_anydbm which is used in back_bsddb.
2086 # change test_db as dirlist does not work for subdirectories.
2087 # ATTENTION: blobfiles now creates subdirectories for files.
2088 #
2089 #Revision 1.28 2002/02/16 09:14:17 richard
2090 # . #514854 ] History: "User" is always ticket creator
2091 #
2092 #Revision 1.27 2002/01/22 07:21:13 richard
2093 #. fixed back_bsddb so it passed the journal tests
2094 #
2095 #... it didn't seem happy using the back_anydbm _open method, which is odd.
2096 #Yet another occurrance of whichdb not being able to recognise older bsddb
2097 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
2098 #process.
2099 #
2100 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
2101 #last_set_entry was referenced before assignment
2102 #
2103 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
2104 #We need to keep the last 'set' entry in the journal to preserve
2105 #information on 'activity' for nodes.
2106 #
2107 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
2108 #You can now use the roundup-admin tool to pack the database
2109 #
2110 #Revision 1.23 2002/01/18 04:32:04 richard
2111 #Rollback was breaking because a message hadn't actually been written to the file. Needs
2112 #more investigation.
2113 #
2114 #Revision 1.22 2002/01/14 02:20:15 richard
2115 # . changed all config accesses so they access either the instance or the
2116 # config attriubute on the db. This means that all config is obtained from
2117 # instance_config instead of the mish-mash of classes. This will make
2118 # switching to a ConfigParser setup easier too, I hope.
2119 #
2120 #At a minimum, this makes migration a _little_ easier (a lot easier in the
2121 #0.5.0 switch, I hope!)
2122 #
2123 #Revision 1.21 2002/01/02 02:31:38 richard
2124 #Sorry for the huge checkin message - I was only intending to implement #496356
2125 #but I found a number of places where things had been broken by transactions:
2126 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
2127 # for _all_ roundup-generated smtp messages to be sent to.
2128 # . the transaction cache had broken the roundupdb.Class set() reactors
2129 # . newly-created author users in the mailgw weren't being committed to the db
2130 #
2131 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
2132 #on when I found that stuff :):
2133 # . #496356 ] Use threading in messages
2134 # . detectors were being registered multiple times
2135 # . added tests for mailgw
2136 # . much better attaching of erroneous messages in the mail gateway
2137 #
2138 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
2139 #Fixed bugs:
2140 # . Fixed file creation and retrieval in same transaction in anydbm
2141 # backend
2142 # . Cgi interface now renders new issue after issue creation
2143 # . Could not set issue status to resolved through cgi interface
2144 # . Mail gateway was changing status back to 'chatting' if status was
2145 # omitted as an argument
2146 #
2147 #Revision 1.19 2001/12/17 03:52:48 richard
2148 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
2149 #storing more than one file per node - if a property name is supplied,
2150 #the file is called designator.property.
2151 #I decided not to migrate the existing files stored over to the new naming
2152 #scheme - the FileClass just doesn't specify the property name.
2153 #
2154 #Revision 1.18 2001/12/16 10:53:38 richard
2155 #take a copy of the node dict so that the subsequent set
2156 #operation doesn't modify the oldvalues structure
2157 #
2158 #Revision 1.17 2001/12/14 23:42:57 richard
2159 #yuck, a gdbm instance tests false :(
2160 #I've left the debugging code in - it should be removed one day if we're ever
2161 #_really_ anal about performace :)
2162 #
2163 #Revision 1.16 2001/12/12 03:23:14 richard
2164 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
2165 #incorrectly identifies a dbm file as a dbhash file on my system. This has
2166 #been submitted to the python bug tracker as issue #491888:
2167 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
2168 #
2169 #Revision 1.15 2001/12/12 02:30:51 richard
2170 #I fixed the problems with people whose anydbm was using the dbm module at the
2171 #backend. It turns out the dbm module modifies the file name to append ".db"
2172 #and my check to determine if we're opening an existing or new db just
2173 #tested os.path.exists() on the filename. Well, no longer! We now perform a
2174 #much better check _and_ cope with the anydbm implementation module changing
2175 #too!
2176 #I also fixed the backends __init__ so only ImportError is squashed.
2177 #
2178 #Revision 1.14 2001/12/10 22:20:01 richard
2179 #Enabled transaction support in the bsddb backend. It uses the anydbm code
2180 #where possible, only replacing methods where the db is opened (it uses the
2181 #btree opener specifically.)
2182 #Also cleaned up some change note generation.
2183 #Made the backends package work with pydoc too.
2184 #
2185 #Revision 1.13 2001/12/02 05:06:16 richard
2186 #. We now use weakrefs in the Classes to keep the database reference, so
2187 # the close() method on the database is no longer needed.
2188 # I bumped the minimum python requirement up to 2.1 accordingly.
2189 #. #487480 ] roundup-server
2190 #. #487476 ] INSTALL.txt
2191 #
2192 #I also cleaned up the change message / post-edit stuff in the cgi client.
2193 #There's now a clearly marked "TODO: append the change note" where I believe
2194 #the change note should be added there. The "changes" list will obviously
2195 #have to be modified to be a dict of the changes, or somesuch.
2196 #
2197 #More testing needed.
2198 #
2199 #Revision 1.12 2001/12/01 07:17:50 richard
2200 #. We now have basic transaction support! Information is only written to
2201 # the database when the commit() method is called. Only the anydbm
2202 # backend is modified in this way - neither of the bsddb backends have been.
2203 # The mail, admin and cgi interfaces all use commit (except the admin tool
2204 # doesn't have a commit command, so interactive users can't commit...)
2205 #. Fixed login/registration forwarding the user to the right page (or not,
2206 # on a failure)
2207 #
2208 #Revision 1.11 2001/11/21 02:34:18 richard
2209 #Added a target version field to the extended issue schema
2210 #
2211 #Revision 1.10 2001/10/09 23:58:10 richard
2212 #Moved the data stringification up into the hyperdb.Class class' get, set
2213 #and create methods. This means that the data is also stringified for the
2214 #journal call, and removes duplication of code from the backends. The
2215 #backend code now only sees strings.
2216 #
2217 #Revision 1.9 2001/10/09 07:25:59 richard
2218 #Added the Password property type. See "pydoc roundup.password" for
2219 #implementation details. Have updated some of the documentation too.
2220 #
2221 #Revision 1.8 2001/09/29 13:27:00 richard
2222 #CGI interfaces now spit up a top-level index of all the instances they can
2223 #serve.
2224 #
2225 #Revision 1.7 2001/08/12 06:32:36 richard
2226 #using isinstance(blah, Foo) now instead of isFooType
2227 #
2228 #Revision 1.6 2001/08/07 00:24:42 richard
2229 #stupid typo
2230 #
2231 #Revision 1.5 2001/08/07 00:15:51 richard
2232 #Added the copyright/license notice to (nearly) all files at request of
2233 #Bizar Software.
2234 #
2235 #Revision 1.4 2001/07/30 01:41:36 richard
2236 #Makes schema changes mucho easier.
2237 #
2238 #Revision 1.3 2001/07/25 01:23:07 richard
2239 #Added the Roundup spec to the new documentation directory.
2240 #
2241 #Revision 1.2 2001/07/23 08:20:44 richard
2242 #Moved over to using marshal in the bsddb and anydbm backends.
2243 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
2244 # retired - mod hyperdb.Class.list() so it lists retired nodes)
2245 #
2246 #