bb15ad2d397ba1b0022b5599c418ecc851264162
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.63 2002-08-22 04:42:28 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)
630 print 'doSaveJournal', entry
632 if __debug__:
633 print >>hyperdb.DEBUG, 'doSaveJournal', entry
635 db = self.getCachedJournalDB(classname)
637 # now insert the journal entry
638 if db.has_key(nodeid):
639 # append to existing
640 s = db[nodeid]
641 l = marshal.loads(s)
642 l.append(entry)
643 else:
644 l = [entry]
646 db[nodeid] = marshal.dumps(l)
648 def doDestroyNode(self, classname, nodeid):
649 if __debug__:
650 print >>hyperdb.DEBUG, 'doDestroyNode', (self, classname, nodeid)
652 # delete from the class database
653 db = self.getCachedClassDB(classname)
654 if db.has_key(nodeid):
655 del db[nodeid]
657 # delete from the database
658 db = self.getCachedJournalDB(classname)
659 if db.has_key(nodeid):
660 del db[nodeid]
662 # return the classname, nodeid so we reindex this content
663 return (classname, nodeid)
665 def rollback(self):
666 ''' Reverse all actions from the current transaction.
667 '''
668 if __debug__:
669 print >>hyperdb.DEBUG, 'rollback', (self, )
670 for method, args in self.transactions:
671 # delete temporary files
672 if method == self.doStoreFile:
673 self.rollbackStoreFile(*args)
674 self.cache = {}
675 self.dirtynodes = {}
676 self.newnodes = {}
677 self.destroyednodes = {}
678 self.transactions = []
680 _marker = []
681 class Class(hyperdb.Class):
682 """The handle to a particular class of nodes in a hyperdatabase."""
684 def __init__(self, db, classname, **properties):
685 """Create a new class with a given name and property specification.
687 'classname' must not collide with the name of an existing class,
688 or a ValueError is raised. The keyword arguments in 'properties'
689 must map names to property objects, or a TypeError is raised.
690 """
691 if (properties.has_key('creation') or properties.has_key('activity')
692 or properties.has_key('creator')):
693 raise ValueError, '"creation", "activity" and "creator" are '\
694 'reserved'
696 self.classname = classname
697 self.properties = properties
698 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
699 self.key = ''
701 # should we journal changes (default yes)
702 self.do_journal = 1
704 # do the db-related init stuff
705 db.addclass(self)
707 self.auditors = {'create': [], 'set': [], 'retire': []}
708 self.reactors = {'create': [], 'set': [], 'retire': []}
710 def enableJournalling(self):
711 '''Turn journalling on for this class
712 '''
713 self.do_journal = 1
715 def disableJournalling(self):
716 '''Turn journalling off for this class
717 '''
718 self.do_journal = 0
720 # Editing nodes:
722 def create(self, **propvalues):
723 """Create a new node of this class and return its id.
725 The keyword arguments in 'propvalues' map property names to values.
727 The values of arguments must be acceptable for the types of their
728 corresponding properties or a TypeError is raised.
730 If this class has a key property, it must be present and its value
731 must not collide with other key strings or a ValueError is raised.
733 Any other properties on this class that are missing from the
734 'propvalues' dictionary are set to None.
736 If an id in a link or multilink property does not refer to a valid
737 node, an IndexError is raised.
739 These operations trigger detectors and can be vetoed. Attempts
740 to modify the "creation" or "activity" properties cause a KeyError.
741 """
742 if propvalues.has_key('id'):
743 raise KeyError, '"id" is reserved'
745 if self.db.journaltag is None:
746 raise DatabaseError, 'Database open read-only'
748 if propvalues.has_key('creation') or propvalues.has_key('activity'):
749 raise KeyError, '"creation" and "activity" are reserved'
751 self.fireAuditors('create', None, propvalues)
753 # new node's id
754 newid = self.db.newid(self.classname)
756 # validate propvalues
757 num_re = re.compile('^\d+$')
758 for key, value in propvalues.items():
759 if key == self.key:
760 try:
761 self.lookup(value)
762 except KeyError:
763 pass
764 else:
765 raise ValueError, 'node with key "%s" exists'%value
767 # try to handle this property
768 try:
769 prop = self.properties[key]
770 except KeyError:
771 raise KeyError, '"%s" has no property "%s"'%(self.classname,
772 key)
774 if value is not None and isinstance(prop, Link):
775 if type(value) != type(''):
776 raise ValueError, 'link value must be String'
777 link_class = self.properties[key].classname
778 # if it isn't a number, it's a key
779 if not num_re.match(value):
780 try:
781 value = self.db.classes[link_class].lookup(value)
782 except (TypeError, KeyError):
783 raise IndexError, 'new property "%s": %s not a %s'%(
784 key, value, link_class)
785 elif not self.db.getclass(link_class).hasnode(value):
786 raise IndexError, '%s has no node %s'%(link_class, value)
788 # save off the value
789 propvalues[key] = value
791 # register the link with the newly linked node
792 if self.do_journal and self.properties[key].do_journal:
793 self.db.addjournal(link_class, value, 'link',
794 (self.classname, newid, key))
796 elif isinstance(prop, Multilink):
797 if type(value) != type([]):
798 raise TypeError, 'new property "%s" not a list of ids'%key
800 # clean up and validate the list of links
801 link_class = self.properties[key].classname
802 l = []
803 for entry in value:
804 if type(entry) != type(''):
805 raise ValueError, '"%s" link value (%s) must be '\
806 'String'%(key, value)
807 # if it isn't a number, it's a key
808 if not num_re.match(entry):
809 try:
810 entry = self.db.classes[link_class].lookup(entry)
811 except (TypeError, KeyError):
812 raise IndexError, 'new property "%s": %s not a %s'%(
813 key, entry, self.properties[key].classname)
814 l.append(entry)
815 value = l
816 propvalues[key] = value
818 # handle additions
819 for nodeid in value:
820 if not self.db.getclass(link_class).hasnode(nodeid):
821 raise IndexError, '%s has no node %s'%(link_class,
822 nodeid)
823 # register the link with the newly linked node
824 if self.do_journal and self.properties[key].do_journal:
825 self.db.addjournal(link_class, nodeid, 'link',
826 (self.classname, newid, key))
828 elif isinstance(prop, String):
829 if type(value) != type(''):
830 raise TypeError, 'new property "%s" not a string'%key
832 elif isinstance(prop, Password):
833 if not isinstance(value, password.Password):
834 raise TypeError, 'new property "%s" not a Password'%key
836 elif isinstance(prop, Date):
837 if value is not None and not isinstance(value, date.Date):
838 raise TypeError, 'new property "%s" not a Date'%key
840 elif isinstance(prop, Interval):
841 if value is not None and not isinstance(value, date.Interval):
842 raise TypeError, 'new property "%s" not an Interval'%key
844 elif value is not None and isinstance(prop, Number):
845 try:
846 float(value)
847 except ValueError:
848 raise TypeError, 'new property "%s" not numeric'%key
850 elif value is not None and isinstance(prop, Boolean):
851 try:
852 int(value)
853 except ValueError:
854 raise TypeError, 'new property "%s" not boolean'%key
856 # make sure there's data where there needs to be
857 for key, prop in self.properties.items():
858 if propvalues.has_key(key):
859 continue
860 if key == self.key:
861 raise ValueError, 'key property "%s" is required'%key
862 if isinstance(prop, Multilink):
863 propvalues[key] = []
864 else:
865 propvalues[key] = None
867 # done
868 self.db.addnode(self.classname, newid, propvalues)
869 if self.do_journal:
870 self.db.addjournal(self.classname, newid, 'create', propvalues)
872 self.fireReactors('create', newid, None)
874 return newid
876 def export_list(self, propnames, nodeid):
877 ''' Export a node - generate a list of CSV-able data in the order
878 specified by propnames for the given node.
879 '''
880 properties = self.getprops()
881 l = []
882 for prop in propnames:
883 proptype = properties[prop]
884 value = self.get(nodeid, prop)
885 # "marshal" data where needed
886 if isinstance(proptype, hyperdb.Date):
887 value = value.get_tuple()
888 elif isinstance(proptype, hyperdb.Interval):
889 value = value.get_tuple()
890 elif isinstance(proptype, hyperdb.Password):
891 value = str(value)
892 l.append(repr(value))
893 return l
895 def import_list(self, propnames, proplist):
896 ''' Import a node - all information including "id" is present and
897 should not be sanity checked. Triggers are not triggered. The
898 journal should be initialised using the "creator" and "created"
899 information.
901 Return the nodeid of the node imported.
902 '''
903 if self.db.journaltag is None:
904 raise DatabaseError, 'Database open read-only'
905 properties = self.getprops()
907 # make the new node's property map
908 d = {}
909 for i in range(len(propnames)):
910 # Use eval to reverse the repr() used to output the CSV
911 value = eval(proplist[i])
913 # Figure the property for this column
914 propname = propnames[i]
915 prop = properties[propname]
917 # "unmarshal" where necessary
918 if propname == 'id':
919 newid = value
920 continue
921 elif isinstance(prop, hyperdb.Date):
922 value = date.Date(value)
923 elif isinstance(prop, hyperdb.Interval):
924 value = date.Interval(value)
925 elif isinstance(prop, hyperdb.Password):
926 pwd = password.Password()
927 pwd.unpack(value)
928 value = pwd
929 if value is not None:
930 d[propname] = value
932 # add
933 self.db.addnode(self.classname, newid, d)
934 self.db.addjournal(self.classname, newid, 'create', d)
935 return newid
937 def get(self, nodeid, propname, default=_marker, cache=1):
938 """Get the value of a property on an existing node of this class.
940 'nodeid' must be the id of an existing node of this class or an
941 IndexError is raised. 'propname' must be the name of a property
942 of this class or a KeyError is raised.
944 'cache' indicates whether the transaction cache should be queried
945 for the node. If the node has been modified and you need to
946 determine what its values prior to modification are, you need to
947 set cache=0.
949 Attempts to get the "creation" or "activity" properties should
950 do the right thing.
951 """
952 if propname == 'id':
953 return nodeid
955 if propname == 'creation':
956 if not self.do_journal:
957 raise ValueError, 'Journalling is disabled for this class'
958 journal = self.db.getjournal(self.classname, nodeid)
959 if journal:
960 return self.db.getjournal(self.classname, nodeid)[0][1]
961 else:
962 # on the strange chance that there's no journal
963 return date.Date()
964 if propname == 'activity':
965 if not self.do_journal:
966 raise ValueError, 'Journalling is disabled for this class'
967 journal = self.db.getjournal(self.classname, nodeid)
968 if journal:
969 return self.db.getjournal(self.classname, nodeid)[-1][1]
970 else:
971 # on the strange chance that there's no journal
972 return date.Date()
973 if propname == 'creator':
974 if not self.do_journal:
975 raise ValueError, 'Journalling is disabled for this class'
976 journal = self.db.getjournal(self.classname, nodeid)
977 if journal:
978 name = self.db.getjournal(self.classname, nodeid)[0][2]
979 else:
980 return None
981 return self.db.user.lookup(name)
983 # get the property (raises KeyErorr if invalid)
984 prop = self.properties[propname]
986 # get the node's dict
987 d = self.db.getnode(self.classname, nodeid, cache=cache)
989 if not d.has_key(propname):
990 if default is _marker:
991 if isinstance(prop, Multilink):
992 return []
993 else:
994 return None
995 else:
996 return default
998 return d[propname]
1000 # XXX not in spec
1001 def getnode(self, nodeid, cache=1):
1002 ''' Return a convenience wrapper for the node.
1004 'nodeid' must be the id of an existing node of this class or an
1005 IndexError is raised.
1007 'cache' indicates whether the transaction cache should be queried
1008 for the node. If the node has been modified and you need to
1009 determine what its values prior to modification are, you need to
1010 set cache=0.
1011 '''
1012 return Node(self, nodeid, cache=cache)
1014 def set(self, nodeid, **propvalues):
1015 """Modify a property on an existing node of this class.
1017 'nodeid' must be the id of an existing node of this class or an
1018 IndexError is raised.
1020 Each key in 'propvalues' must be the name of a property of this
1021 class or a KeyError is raised.
1023 All values in 'propvalues' must be acceptable types for their
1024 corresponding properties or a TypeError is raised.
1026 If the value of the key property is set, it must not collide with
1027 other key strings or a ValueError is raised.
1029 If the value of a Link or Multilink property contains an invalid
1030 node id, a ValueError is raised.
1032 These operations trigger detectors and can be vetoed. Attempts
1033 to modify the "creation" or "activity" properties cause a KeyError.
1034 """
1035 if not propvalues:
1036 return propvalues
1038 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1039 raise KeyError, '"creation" and "activity" are reserved'
1041 if propvalues.has_key('id'):
1042 raise KeyError, '"id" is reserved'
1044 if self.db.journaltag is None:
1045 raise DatabaseError, 'Database open read-only'
1047 self.fireAuditors('set', nodeid, propvalues)
1048 # Take a copy of the node dict so that the subsequent set
1049 # operation doesn't modify the oldvalues structure.
1050 try:
1051 # try not using the cache initially
1052 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid,
1053 cache=0))
1054 except IndexError:
1055 # this will be needed if somone does a create() and set()
1056 # with no intervening commit()
1057 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1059 node = self.db.getnode(self.classname, nodeid)
1060 if node.has_key(self.db.RETIRED_FLAG):
1061 raise IndexError
1062 num_re = re.compile('^\d+$')
1064 # if the journal value is to be different, store it in here
1065 journalvalues = {}
1067 for propname, value in propvalues.items():
1068 # check to make sure we're not duplicating an existing key
1069 if propname == self.key and node[propname] != value:
1070 try:
1071 self.lookup(value)
1072 except KeyError:
1073 pass
1074 else:
1075 raise ValueError, 'node with key "%s" exists'%value
1077 # this will raise the KeyError if the property isn't valid
1078 # ... we don't use getprops() here because we only care about
1079 # the writeable properties.
1080 prop = self.properties[propname]
1082 # if the value's the same as the existing value, no sense in
1083 # doing anything
1084 if node.has_key(propname) and value == node[propname]:
1085 del propvalues[propname]
1086 continue
1088 # do stuff based on the prop type
1089 if isinstance(prop, Link):
1090 link_class = prop.classname
1091 # if it isn't a number, it's a key
1092 if value is not None and not isinstance(value, type('')):
1093 raise ValueError, 'property "%s" link value be a string'%(
1094 propname)
1095 if isinstance(value, type('')) and not num_re.match(value):
1096 try:
1097 value = self.db.classes[link_class].lookup(value)
1098 except (TypeError, KeyError):
1099 raise IndexError, 'new property "%s": %s not a %s'%(
1100 propname, value, prop.classname)
1102 if (value is not None and
1103 not self.db.getclass(link_class).hasnode(value)):
1104 raise IndexError, '%s has no node %s'%(link_class, value)
1106 if self.do_journal and prop.do_journal:
1107 # register the unlink with the old linked node
1108 if node[propname] is not None:
1109 self.db.addjournal(link_class, node[propname], 'unlink',
1110 (self.classname, nodeid, propname))
1112 # register the link with the newly linked node
1113 if value is not None:
1114 self.db.addjournal(link_class, value, 'link',
1115 (self.classname, nodeid, propname))
1117 elif isinstance(prop, Multilink):
1118 if type(value) != type([]):
1119 raise TypeError, 'new property "%s" not a list of'\
1120 ' ids'%propname
1121 link_class = self.properties[propname].classname
1122 l = []
1123 for entry in value:
1124 # if it isn't a number, it's a key
1125 if type(entry) != type(''):
1126 raise ValueError, 'new property "%s" link value ' \
1127 'must be a string'%propname
1128 if not num_re.match(entry):
1129 try:
1130 entry = self.db.classes[link_class].lookup(entry)
1131 except (TypeError, KeyError):
1132 raise IndexError, 'new property "%s": %s not a %s'%(
1133 propname, entry,
1134 self.properties[propname].classname)
1135 l.append(entry)
1136 value = l
1137 propvalues[propname] = value
1139 # figure the journal entry for this property
1140 add = []
1141 remove = []
1143 # handle removals
1144 if node.has_key(propname):
1145 l = node[propname]
1146 else:
1147 l = []
1148 for id in l[:]:
1149 if id in value:
1150 continue
1151 # register the unlink with the old linked node
1152 if self.do_journal and self.properties[propname].do_journal:
1153 self.db.addjournal(link_class, id, 'unlink',
1154 (self.classname, nodeid, propname))
1155 l.remove(id)
1156 remove.append(id)
1158 # handle additions
1159 for id in value:
1160 if not self.db.getclass(link_class).hasnode(id):
1161 raise IndexError, '%s has no node %s'%(link_class, id)
1162 if id in l:
1163 continue
1164 # register the link with the newly linked node
1165 if self.do_journal and self.properties[propname].do_journal:
1166 self.db.addjournal(link_class, id, 'link',
1167 (self.classname, nodeid, propname))
1168 l.append(id)
1169 add.append(id)
1171 # figure the journal entry
1172 l = []
1173 if add:
1174 l.append(('+', add))
1175 if remove:
1176 l.append(('-', remove))
1177 if l:
1178 journalvalues[propname] = tuple(l)
1180 elif isinstance(prop, String):
1181 if value is not None and type(value) != type(''):
1182 raise TypeError, 'new property "%s" not a string'%propname
1184 elif isinstance(prop, Password):
1185 if not isinstance(value, password.Password):
1186 raise TypeError, 'new property "%s" not a Password'%propname
1187 propvalues[propname] = value
1189 elif value is not None and isinstance(prop, Date):
1190 if not isinstance(value, date.Date):
1191 raise TypeError, 'new property "%s" not a Date'% propname
1192 propvalues[propname] = value
1194 elif value is not None and isinstance(prop, Interval):
1195 if not isinstance(value, date.Interval):
1196 raise TypeError, 'new property "%s" not an '\
1197 'Interval'%propname
1198 propvalues[propname] = value
1200 elif value is not None and isinstance(prop, Number):
1201 try:
1202 float(value)
1203 except ValueError:
1204 raise TypeError, 'new property "%s" not numeric'%propname
1206 elif value is not None and isinstance(prop, Boolean):
1207 try:
1208 int(value)
1209 except ValueError:
1210 raise TypeError, 'new property "%s" not boolean'%propname
1212 node[propname] = value
1214 # nothing to do?
1215 if not propvalues:
1216 return propvalues
1218 # do the set, and journal it
1219 self.db.setnode(self.classname, nodeid, node)
1221 if self.do_journal:
1222 propvalues.update(journalvalues)
1223 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
1225 self.fireReactors('set', nodeid, oldvalues)
1227 return propvalues
1229 def retire(self, nodeid):
1230 """Retire a node.
1232 The properties on the node remain available from the get() method,
1233 and the node's id is never reused.
1235 Retired nodes are not returned by the find(), list(), or lookup()
1236 methods, and other nodes may reuse the values of their key properties.
1238 These operations trigger detectors and can be vetoed. Attempts
1239 to modify the "creation" or "activity" properties cause a KeyError.
1240 """
1241 if self.db.journaltag is None:
1242 raise DatabaseError, 'Database open read-only'
1244 self.fireAuditors('retire', nodeid, None)
1246 node = self.db.getnode(self.classname, nodeid)
1247 node[self.db.RETIRED_FLAG] = 1
1248 self.db.setnode(self.classname, nodeid, node)
1249 if self.do_journal:
1250 self.db.addjournal(self.classname, nodeid, 'retired', None)
1252 self.fireReactors('retire', nodeid, None)
1254 def is_retired(self, nodeid):
1255 '''Return true if the node is retired.
1256 '''
1257 node = self.db.getnode(cn, nodeid, cldb)
1258 if node.has_key(self.db.RETIRED_FLAG):
1259 return 1
1260 return 0
1262 def destroy(self, nodeid):
1263 """Destroy a node.
1265 WARNING: this method should never be used except in extremely rare
1266 situations where there could never be links to the node being
1267 deleted
1268 WARNING: use retire() instead
1269 WARNING: the properties of this node will not be available ever again
1270 WARNING: really, use retire() instead
1272 Well, I think that's enough warnings. This method exists mostly to
1273 support the session storage of the cgi interface.
1274 """
1275 if self.db.journaltag is None:
1276 raise DatabaseError, 'Database open read-only'
1277 self.db.destroynode(self.classname, nodeid)
1279 def history(self, nodeid):
1280 """Retrieve the journal of edits on a particular node.
1282 'nodeid' must be the id of an existing node of this class or an
1283 IndexError is raised.
1285 The returned list contains tuples of the form
1287 (date, tag, action, params)
1289 'date' is a Timestamp object specifying the time of the change and
1290 'tag' is the journaltag specified when the database was opened.
1291 """
1292 if not self.do_journal:
1293 raise ValueError, 'Journalling is disabled for this class'
1294 return self.db.getjournal(self.classname, nodeid)
1296 # Locating nodes:
1297 def hasnode(self, nodeid):
1298 '''Determine if the given nodeid actually exists
1299 '''
1300 return self.db.hasnode(self.classname, nodeid)
1302 def setkey(self, propname):
1303 """Select a String property of this class to be the key property.
1305 'propname' must be the name of a String property of this class or
1306 None, or a TypeError is raised. The values of the key property on
1307 all existing nodes must be unique or a ValueError is raised. If the
1308 property doesn't exist, KeyError is raised.
1309 """
1310 prop = self.getprops()[propname]
1311 if not isinstance(prop, String):
1312 raise TypeError, 'key properties must be String'
1313 self.key = propname
1315 def getkey(self):
1316 """Return the name of the key property for this class or None."""
1317 return self.key
1319 def labelprop(self, default_to_id=0):
1320 ''' Return the property name for a label for the given node.
1322 This method attempts to generate a consistent label for the node.
1323 It tries the following in order:
1324 1. key property
1325 2. "name" property
1326 3. "title" property
1327 4. first property from the sorted property name list
1328 '''
1329 k = self.getkey()
1330 if k:
1331 return k
1332 props = self.getprops()
1333 if props.has_key('name'):
1334 return 'name'
1335 elif props.has_key('title'):
1336 return 'title'
1337 if default_to_id:
1338 return 'id'
1339 props = props.keys()
1340 props.sort()
1341 return props[0]
1343 # TODO: set up a separate index db file for this? profile?
1344 def lookup(self, keyvalue):
1345 """Locate a particular node by its key property and return its id.
1347 If this class has no key property, a TypeError is raised. If the
1348 'keyvalue' matches one of the values for the key property among
1349 the nodes in this class, the matching node's id is returned;
1350 otherwise a KeyError is raised.
1351 """
1352 cldb = self.db.getclassdb(self.classname)
1353 try:
1354 for nodeid in self.db.getnodeids(self.classname, cldb):
1355 node = self.db.getnode(self.classname, nodeid, cldb)
1356 if node.has_key(self.db.RETIRED_FLAG):
1357 continue
1358 if node[self.key] == keyvalue:
1359 cldb.close()
1360 return nodeid
1361 finally:
1362 cldb.close()
1363 raise KeyError, keyvalue
1365 # XXX: change from spec - allows multiple props to match
1366 def find(self, **propspec):
1367 """Get the ids of nodes in this class which link to the given nodes.
1369 'propspec' consists of keyword args propname={nodeid:1,}
1370 'propname' must be the name of a property in this class, or a
1371 KeyError is raised. That property must be a Link or Multilink
1372 property, or a TypeError is raised.
1374 Any node in this class whose 'propname' property links to any of the
1375 nodeids will be returned. Used by the full text indexing, which knows
1376 that "foo" occurs in msg1, msg3 and file7, so we have hits on these issues:
1377 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1378 """
1379 propspec = propspec.items()
1380 for propname, nodeids in propspec:
1381 # check the prop is OK
1382 prop = self.properties[propname]
1383 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
1384 raise TypeError, "'%s' not a Link/Multilink property"%propname
1386 # ok, now do the find
1387 cldb = self.db.getclassdb(self.classname)
1388 l = []
1389 try:
1390 for id in self.db.getnodeids(self.classname, db=cldb):
1391 node = self.db.getnode(self.classname, id, db=cldb)
1392 if node.has_key(self.db.RETIRED_FLAG):
1393 continue
1394 for propname, nodeids in propspec:
1395 # can't test if the node doesn't have this property
1396 if not node.has_key(propname):
1397 continue
1398 if type(nodeids) is type(''):
1399 nodeids = {nodeids:1}
1400 prop = self.properties[propname]
1401 value = node[propname]
1402 if isinstance(prop, Link) and nodeids.has_key(value):
1403 l.append(id)
1404 break
1405 elif isinstance(prop, Multilink):
1406 hit = 0
1407 for v in value:
1408 if nodeids.has_key(v):
1409 l.append(id)
1410 hit = 1
1411 break
1412 if hit:
1413 break
1414 finally:
1415 cldb.close()
1416 return l
1418 def stringFind(self, **requirements):
1419 """Locate a particular node by matching a set of its String
1420 properties in a caseless search.
1422 If the property is not a String property, a TypeError is raised.
1424 The return is a list of the id of all nodes that match.
1425 """
1426 for propname in requirements.keys():
1427 prop = self.properties[propname]
1428 if isinstance(not prop, String):
1429 raise TypeError, "'%s' not a String property"%propname
1430 requirements[propname] = requirements[propname].lower()
1431 l = []
1432 cldb = self.db.getclassdb(self.classname)
1433 try:
1434 for nodeid in self.db.getnodeids(self.classname, cldb):
1435 node = self.db.getnode(self.classname, nodeid, cldb)
1436 if node.has_key(self.db.RETIRED_FLAG):
1437 continue
1438 for key, value in requirements.items():
1439 if node[key] is None or node[key].lower() != value:
1440 break
1441 else:
1442 l.append(nodeid)
1443 finally:
1444 cldb.close()
1445 return l
1447 def list(self):
1448 """Return a list of the ids of the active nodes in this class."""
1449 l = []
1450 cn = self.classname
1451 cldb = self.db.getclassdb(cn)
1452 try:
1453 for nodeid in self.db.getnodeids(cn, cldb):
1454 node = self.db.getnode(cn, nodeid, cldb)
1455 if node.has_key(self.db.RETIRED_FLAG):
1456 continue
1457 l.append(nodeid)
1458 finally:
1459 cldb.close()
1460 l.sort()
1461 return l
1463 def filter(self, search_matches, filterspec, sort, group,
1464 num_re = re.compile('^\d+$')):
1465 ''' Return a list of the ids of the active nodes in this class that
1466 match the 'filter' spec, sorted by the group spec and then the
1467 sort spec.
1469 "filterspec" is {propname: value(s)}
1470 "sort" is ['+propname', '-propname', 'propname', ...]
1471 "group is ['+propname', '-propname', 'propname', ...]
1472 '''
1473 cn = self.classname
1475 # optimise filterspec
1476 l = []
1477 props = self.getprops()
1478 LINK = 0
1479 MULTILINK = 1
1480 STRING = 2
1481 OTHER = 6
1482 for k, v in filterspec.items():
1483 propclass = props[k]
1484 if isinstance(propclass, Link):
1485 if type(v) is not type([]):
1486 v = [v]
1487 # replace key values with node ids
1488 u = []
1489 link_class = self.db.classes[propclass.classname]
1490 for entry in v:
1491 if entry == '-1': entry = None
1492 elif not num_re.match(entry):
1493 try:
1494 entry = link_class.lookup(entry)
1495 except (TypeError,KeyError):
1496 raise ValueError, 'property "%s": %s not a %s'%(
1497 k, entry, self.properties[k].classname)
1498 u.append(entry)
1500 l.append((LINK, k, u))
1501 elif isinstance(propclass, Multilink):
1502 if type(v) is not type([]):
1503 v = [v]
1504 # replace key values with node ids
1505 u = []
1506 link_class = self.db.classes[propclass.classname]
1507 for entry in v:
1508 if not num_re.match(entry):
1509 try:
1510 entry = link_class.lookup(entry)
1511 except (TypeError,KeyError):
1512 raise ValueError, 'new property "%s": %s not a %s'%(
1513 k, entry, self.properties[k].classname)
1514 u.append(entry)
1515 l.append((MULTILINK, k, u))
1516 elif isinstance(propclass, String):
1517 # simple glob searching
1518 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
1519 v = v.replace('?', '.')
1520 v = v.replace('*', '.*?')
1521 l.append((STRING, k, re.compile(v, re.I)))
1522 elif isinstance(propclass, Boolean):
1523 if type(v) is type(''):
1524 bv = v.lower() in ('yes', 'true', 'on', '1')
1525 else:
1526 bv = v
1527 l.append((OTHER, k, bv))
1528 elif isinstance(propclass, Number):
1529 l.append((OTHER, k, int(v)))
1530 else:
1531 l.append((OTHER, k, v))
1532 filterspec = l
1534 # now, find all the nodes that are active and pass filtering
1535 l = []
1536 cldb = self.db.getclassdb(cn)
1537 try:
1538 # TODO: only full-scan once (use items())
1539 for nodeid in self.db.getnodeids(cn, cldb):
1540 node = self.db.getnode(cn, nodeid, cldb)
1541 if node.has_key(self.db.RETIRED_FLAG):
1542 continue
1543 # apply filter
1544 for t, k, v in filterspec:
1545 # make sure the node has the property
1546 if not node.has_key(k):
1547 # this node doesn't have this property, so reject it
1548 break
1550 # now apply the property filter
1551 if t == LINK:
1552 # link - if this node's property doesn't appear in the
1553 # filterspec's nodeid list, skip it
1554 if node[k] not in v:
1555 break
1556 elif t == MULTILINK:
1557 # multilink - if any of the nodeids required by the
1558 # filterspec aren't in this node's property, then skip
1559 # it
1560 have = node[k]
1561 for want in v:
1562 if want not in have:
1563 break
1564 else:
1565 continue
1566 break
1567 elif t == STRING:
1568 # RE search
1569 if node[k] is None or not v.search(node[k]):
1570 break
1571 elif t == OTHER:
1572 # straight value comparison for the other types
1573 if node[k] != v:
1574 break
1575 else:
1576 l.append((nodeid, node))
1577 finally:
1578 cldb.close()
1579 l.sort()
1581 # filter based on full text search
1582 if search_matches is not None:
1583 k = []
1584 for v in l:
1585 if search_matches.has_key(v[0]):
1586 k.append(v)
1587 l = k
1589 # optimise sort
1590 m = []
1591 for entry in sort:
1592 if entry[0] != '-':
1593 m.append(('+', entry))
1594 else:
1595 m.append((entry[0], entry[1:]))
1596 sort = m
1598 # optimise group
1599 m = []
1600 for entry in group:
1601 if entry[0] != '-':
1602 m.append(('+', entry))
1603 else:
1604 m.append((entry[0], entry[1:]))
1605 group = m
1606 # now, sort the result
1607 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
1608 db = self.db, cl=self):
1609 a_id, an = a
1610 b_id, bn = b
1611 # sort by group and then sort
1612 for list in group, sort:
1613 for dir, prop in list:
1614 # sorting is class-specific
1615 propclass = properties[prop]
1617 # handle the properties that might be "faked"
1618 # also, handle possible missing properties
1619 try:
1620 if not an.has_key(prop):
1621 an[prop] = cl.get(a_id, prop)
1622 av = an[prop]
1623 except KeyError:
1624 # the node doesn't have a value for this property
1625 if isinstance(propclass, Multilink): av = []
1626 else: av = ''
1627 try:
1628 if not bn.has_key(prop):
1629 bn[prop] = cl.get(b_id, prop)
1630 bv = bn[prop]
1631 except KeyError:
1632 # the node doesn't have a value for this property
1633 if isinstance(propclass, Multilink): bv = []
1634 else: bv = ''
1636 # String and Date values are sorted in the natural way
1637 if isinstance(propclass, String):
1638 # clean up the strings
1639 if av and av[0] in string.uppercase:
1640 av = an[prop] = av.lower()
1641 if bv and bv[0] in string.uppercase:
1642 bv = bn[prop] = bv.lower()
1643 if (isinstance(propclass, String) or
1644 isinstance(propclass, Date)):
1645 # it might be a string that's really an integer
1646 try:
1647 av = int(av)
1648 bv = int(bv)
1649 except:
1650 pass
1651 if dir == '+':
1652 r = cmp(av, bv)
1653 if r != 0: return r
1654 elif dir == '-':
1655 r = cmp(bv, av)
1656 if r != 0: return r
1658 # Link properties are sorted according to the value of
1659 # the "order" property on the linked nodes if it is
1660 # present; or otherwise on the key string of the linked
1661 # nodes; or finally on the node ids.
1662 elif isinstance(propclass, Link):
1663 link = db.classes[propclass.classname]
1664 if av is None and bv is not None: return -1
1665 if av is not None and bv is None: return 1
1666 if av is None and bv is None: continue
1667 if link.getprops().has_key('order'):
1668 if dir == '+':
1669 r = cmp(link.get(av, 'order'),
1670 link.get(bv, 'order'))
1671 if r != 0: return r
1672 elif dir == '-':
1673 r = cmp(link.get(bv, 'order'),
1674 link.get(av, 'order'))
1675 if r != 0: return r
1676 elif link.getkey():
1677 key = link.getkey()
1678 if dir == '+':
1679 r = cmp(link.get(av, key), link.get(bv, key))
1680 if r != 0: return r
1681 elif dir == '-':
1682 r = cmp(link.get(bv, key), link.get(av, key))
1683 if r != 0: return r
1684 else:
1685 if dir == '+':
1686 r = cmp(av, bv)
1687 if r != 0: return r
1688 elif dir == '-':
1689 r = cmp(bv, av)
1690 if r != 0: return r
1692 # Multilink properties are sorted according to how many
1693 # links are present.
1694 elif isinstance(propclass, Multilink):
1695 if dir == '+':
1696 r = cmp(len(av), len(bv))
1697 if r != 0: return r
1698 elif dir == '-':
1699 r = cmp(len(bv), len(av))
1700 if r != 0: return r
1701 elif isinstance(propclass, Number) or isinstance(propclass, Boolean):
1702 if dir == '+':
1703 r = cmp(av, bv)
1704 elif dir == '-':
1705 r = cmp(bv, av)
1707 # end for dir, prop in list:
1708 # end for list in sort, group:
1709 # if all else fails, compare the ids
1710 return cmp(a[0], b[0])
1712 l.sort(sortfun)
1713 return [i[0] for i in l]
1715 def count(self):
1716 """Get the number of nodes in this class.
1718 If the returned integer is 'numnodes', the ids of all the nodes
1719 in this class run from 1 to numnodes, and numnodes+1 will be the
1720 id of the next node to be created in this class.
1721 """
1722 return self.db.countnodes(self.classname)
1724 # Manipulating properties:
1726 def getprops(self, protected=1):
1727 """Return a dictionary mapping property names to property objects.
1728 If the "protected" flag is true, we include protected properties -
1729 those which may not be modified.
1731 In addition to the actual properties on the node, these
1732 methods provide the "creation" and "activity" properties. If the
1733 "protected" flag is true, we include protected properties - those
1734 which may not be modified.
1735 """
1736 d = self.properties.copy()
1737 if protected:
1738 d['id'] = String()
1739 d['creation'] = hyperdb.Date()
1740 d['activity'] = hyperdb.Date()
1741 d['creator'] = hyperdb.Link("user")
1742 return d
1744 def addprop(self, **properties):
1745 """Add properties to this class.
1747 The keyword arguments in 'properties' must map names to property
1748 objects, or a TypeError is raised. None of the keys in 'properties'
1749 may collide with the names of existing properties, or a ValueError
1750 is raised before any properties have been added.
1751 """
1752 for key in properties.keys():
1753 if self.properties.has_key(key):
1754 raise ValueError, key
1755 self.properties.update(properties)
1757 def index(self, nodeid):
1758 '''Add (or refresh) the node to search indexes
1759 '''
1760 # find all the String properties that have indexme
1761 for prop, propclass in self.getprops().items():
1762 if isinstance(propclass, String) and propclass.indexme:
1763 try:
1764 value = str(self.get(nodeid, prop))
1765 except IndexError:
1766 # node no longer exists - entry should be removed
1767 self.db.indexer.purge_entry((self.classname, nodeid, prop))
1768 else:
1769 # and index them under (classname, nodeid, property)
1770 self.db.indexer.add_text((self.classname, nodeid, prop),
1771 value)
1773 #
1774 # Detector interface
1775 #
1776 def audit(self, event, detector):
1777 """Register a detector
1778 """
1779 l = self.auditors[event]
1780 if detector not in l:
1781 self.auditors[event].append(detector)
1783 def fireAuditors(self, action, nodeid, newvalues):
1784 """Fire all registered auditors.
1785 """
1786 for audit in self.auditors[action]:
1787 audit(self.db, self, nodeid, newvalues)
1789 def react(self, event, detector):
1790 """Register a detector
1791 """
1792 l = self.reactors[event]
1793 if detector not in l:
1794 self.reactors[event].append(detector)
1796 def fireReactors(self, action, nodeid, oldvalues):
1797 """Fire all registered reactors.
1798 """
1799 for react in self.reactors[action]:
1800 react(self.db, self, nodeid, oldvalues)
1802 class FileClass(Class):
1803 '''This class defines a large chunk of data. To support this, it has a
1804 mandatory String property "content" which is typically saved off
1805 externally to the hyperdb.
1807 The default MIME type of this data is defined by the
1808 "default_mime_type" class attribute, which may be overridden by each
1809 node if the class defines a "type" String property.
1810 '''
1811 default_mime_type = 'text/plain'
1813 def create(self, **propvalues):
1814 ''' snaffle the file propvalue and store in a file
1815 '''
1816 content = propvalues['content']
1817 del propvalues['content']
1818 newid = Class.create(self, **propvalues)
1819 self.db.storefile(self.classname, newid, None, content)
1820 return newid
1822 def import_list(self, propnames, proplist):
1823 ''' Trap the "content" property...
1824 '''
1825 # dupe this list so we don't affect others
1826 propnames = propnames[:]
1828 # extract the "content" property from the proplist
1829 i = propnames.index('content')
1830 content = proplist[i]
1831 del propnames[i]
1832 del proplist[i]
1834 # do the normal import
1835 newid = Class.import_list(self, propnames, proplist)
1837 # save off the "content" file
1838 self.db.storefile(self.classname, newid, None, content)
1839 return newid
1841 def get(self, nodeid, propname, default=_marker, cache=1):
1842 ''' trap the content propname and get it from the file
1843 '''
1845 poss_msg = 'Possibly a access right configuration problem.'
1846 if propname == 'content':
1847 try:
1848 return self.db.getfile(self.classname, nodeid, None)
1849 except IOError, (strerror):
1850 # BUG: by catching this we donot see an error in the log.
1851 return 'ERROR reading file: %s%s\n%s\n%s'%(
1852 self.classname, nodeid, poss_msg, strerror)
1853 if default is not _marker:
1854 return Class.get(self, nodeid, propname, default, cache=cache)
1855 else:
1856 return Class.get(self, nodeid, propname, cache=cache)
1858 def getprops(self, protected=1):
1859 ''' In addition to the actual properties on the node, these methods
1860 provide the "content" property. If the "protected" flag is true,
1861 we include protected properties - those which may not be
1862 modified.
1863 '''
1864 d = Class.getprops(self, protected=protected).copy()
1865 if protected:
1866 d['content'] = hyperdb.String()
1867 return d
1869 def index(self, nodeid):
1870 ''' Index the node in the search index.
1872 We want to index the content in addition to the normal String
1873 property indexing.
1874 '''
1875 # perform normal indexing
1876 Class.index(self, nodeid)
1878 # get the content to index
1879 content = self.get(nodeid, 'content')
1881 # figure the mime type
1882 if self.properties.has_key('type'):
1883 mime_type = self.get(nodeid, 'type')
1884 else:
1885 mime_type = self.default_mime_type
1887 # and index!
1888 self.db.indexer.add_text((self.classname, nodeid, 'content'), content,
1889 mime_type)
1891 # XXX deviation from spec - was called ItemClass
1892 class IssueClass(Class, roundupdb.IssueClass):
1893 # Overridden methods:
1894 def __init__(self, db, classname, **properties):
1895 """The newly-created class automatically includes the "messages",
1896 "files", "nosy", and "superseder" properties. If the 'properties'
1897 dictionary attempts to specify any of these properties or a
1898 "creation" or "activity" property, a ValueError is raised.
1899 """
1900 if not properties.has_key('title'):
1901 properties['title'] = hyperdb.String(indexme='yes')
1902 if not properties.has_key('messages'):
1903 properties['messages'] = hyperdb.Multilink("msg")
1904 if not properties.has_key('files'):
1905 properties['files'] = hyperdb.Multilink("file")
1906 if not properties.has_key('nosy'):
1907 properties['nosy'] = hyperdb.Multilink("user")
1908 if not properties.has_key('superseder'):
1909 properties['superseder'] = hyperdb.Multilink(classname)
1910 Class.__init__(self, db, classname, **properties)
1912 #
1913 #$Log: not supported by cvs2svn $
1914 #Revision 1.62 2002/08/21 07:07:27 richard
1915 #In preparing to turn back on link/unlink journal events (by default these
1916 #are turned off) I've:
1917 #- fixed back_anydbm so it can journal those events again (had broken it
1918 # with recent changes)
1919 #- changed the serialisation format for dates and intervals to use a
1920 # numbers-only (and sign for Intervals) string instead of tuple-of-ints.
1921 # Much smaller.
1922 #
1923 #Revision 1.61 2002/08/19 02:53:27 richard
1924 #full database export and import is done
1925 #
1926 #Revision 1.60 2002/08/19 00:23:19 richard
1927 #handle "unset" initial Link values (!)
1928 #
1929 #Revision 1.59 2002/08/16 04:28:13 richard
1930 #added is_retired query to Class
1931 #
1932 #Revision 1.58 2002/08/01 15:06:24 gmcm
1933 #Use same regex to split search terms as used to index text.
1934 #Fix to back_metakit for not changing journaltag on reopen.
1935 #Fix htmltemplate's do_link so [No <whatever>] strings are href'd.
1936 #Fix bogus "nosy edited ok" msg - the **d syntax does NOT share d between caller and callee.
1937 #
1938 #Revision 1.57 2002/07/31 23:57:36 richard
1939 # . web forms may now unset Link values (like assignedto)
1940 #
1941 #Revision 1.56 2002/07/31 22:04:33 richard
1942 #cleanup
1943 #
1944 #Revision 1.55 2002/07/30 08:22:38 richard
1945 #Session storage in the hyperdb was horribly, horribly inefficient. We use
1946 #a simple anydbm wrapper now - which could be overridden by the metakit
1947 #backend or RDB backend if necessary.
1948 #Much, much better.
1949 #
1950 #Revision 1.54 2002/07/26 08:26:59 richard
1951 #Very close now. The cgi and mailgw now use the new security API. The two
1952 #templates have been migrated to that setup. Lots of unit tests. Still some
1953 #issue in the web form for editing Roles assigned to users.
1954 #
1955 #Revision 1.53 2002/07/25 07:14:06 richard
1956 #Bugger it. Here's the current shape of the new security implementation.
1957 #Still to do:
1958 # . call the security funcs from cgi and mailgw
1959 # . change shipped templates to include correct initialisation and remove
1960 # the old config vars
1961 #... that seems like a lot. The bulk of the work has been done though. Honest :)
1962 #
1963 #Revision 1.52 2002/07/19 03:36:34 richard
1964 #Implemented the destroy() method needed by the session database (and possibly
1965 #others). At the same time, I removed the leading underscores from the hyperdb
1966 #methods that Really Didn't Need Them.
1967 #The journal also raises IndexError now for all situations where there is a
1968 #request for the journal of a node that doesn't have one. It used to return
1969 #[] in _some_ situations, but not all. This _may_ break code, but the tests
1970 #pass...
1971 #
1972 #Revision 1.51 2002/07/18 23:07:08 richard
1973 #Unit tests and a few fixes.
1974 #
1975 #Revision 1.50 2002/07/18 11:50:58 richard
1976 #added tests for number type too
1977 #
1978 #Revision 1.49 2002/07/18 11:41:10 richard
1979 #added tests for boolean type, and fixes to anydbm backend
1980 #
1981 #Revision 1.48 2002/07/18 11:17:31 gmcm
1982 #Add Number and Boolean types to hyperdb.
1983 #Add conversion cases to web, mail & admin interfaces.
1984 #Add storage/serialization cases to back_anydbm & back_metakit.
1985 #
1986 #Revision 1.47 2002/07/14 23:18:20 richard
1987 #. fixed the journal bloat from multilink changes - we just log the add or
1988 # remove operations, not the whole list
1989 #
1990 #Revision 1.46 2002/07/14 06:06:34 richard
1991 #Did some old TODOs
1992 #
1993 #Revision 1.45 2002/07/14 04:03:14 richard
1994 #Implemented a switch to disable journalling for a Class. CGI session
1995 #database now uses it.
1996 #
1997 #Revision 1.44 2002/07/14 02:05:53 richard
1998 #. all storage-specific code (ie. backend) is now implemented by the backends
1999 #
2000 #Revision 1.43 2002/07/10 06:30:30 richard
2001 #...except of course it's nice to use valid Python syntax
2002 #
2003 #Revision 1.42 2002/07/10 06:21:38 richard
2004 #Be extra safe
2005 #
2006 #Revision 1.41 2002/07/10 00:21:45 richard
2007 #explicit database closing
2008 #
2009 #Revision 1.40 2002/07/09 04:19:09 richard
2010 #Added reindex command to roundup-admin.
2011 #Fixed reindex on first access.
2012 #Also fixed reindexing of entries that change.
2013 #
2014 #Revision 1.39 2002/07/09 03:02:52 richard
2015 #More indexer work:
2016 #- all String properties may now be indexed too. Currently there's a bit of
2017 # "issue" specific code in the actual searching which needs to be
2018 # addressed. In a nutshell:
2019 # + pass 'indexme="yes"' as a String() property initialisation arg, eg:
2020 # file = FileClass(db, "file", name=String(), type=String(),
2021 # comment=String(indexme="yes"))
2022 # + the comment will then be indexed and be searchable, with the results
2023 # related back to the issue that the file is linked to
2024 #- as a result of this work, the FileClass has a default MIME type that may
2025 # be overridden in a subclass, or by the use of a "type" property as is
2026 # done in the default templates.
2027 #- the regeneration of the indexes (if necessary) is done once the schema is
2028 # set up in the dbinit.
2029 #
2030 #Revision 1.38 2002/07/08 06:58:15 richard
2031 #cleaned up the indexer code:
2032 # - it splits more words out (much simpler, faster splitter)
2033 # - removed code we'll never use (roundup.roundup_indexer has the full
2034 # implementation, and replaces roundup.indexer)
2035 # - only index text/plain and rfc822/message (ideas for other text formats to
2036 # index are welcome)
2037 # - added simple unit test for indexer. Needs more tests for regression.
2038 #
2039 #Revision 1.37 2002/06/20 23:52:35 richard
2040 #More informative error message
2041 #
2042 #Revision 1.36 2002/06/19 03:07:19 richard
2043 #Moved the file storage commit into blobfiles where it belongs.
2044 #
2045 #Revision 1.35 2002/05/25 07:16:24 rochecompaan
2046 #Merged search_indexing-branch with HEAD
2047 #
2048 #Revision 1.34 2002/05/15 06:21:21 richard
2049 # . node caching now works, and gives a small boost in performance
2050 #
2051 #As a part of this, I cleaned up the DEBUG output and implemented TRACE
2052 #output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
2053 #CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
2054 #(using if __debug__ which is compiled out with -O)
2055 #
2056 #Revision 1.33 2002/04/24 10:38:26 rochecompaan
2057 #All database files are now created group readable and writable.
2058 #
2059 #Revision 1.32 2002/04/15 23:25:15 richard
2060 #. node ids are now generated from a lockable store - no more race conditions
2061 #
2062 #We're using the portalocker code by Jonathan Feinberg that was contributed
2063 #to the ASPN Python cookbook. This gives us locking across Unix and Windows.
2064 #
2065 #Revision 1.31 2002/04/03 05:54:31 richard
2066 #Fixed serialisation problem by moving the serialisation step out of the
2067 #hyperdb.Class (get, set) into the hyperdb.Database.
2068 #
2069 #Also fixed htmltemplate after the showid changes I made yesterday.
2070 #
2071 #Unit tests for all of the above written.
2072 #
2073 #Revision 1.30.2.1 2002/04/03 11:55:57 rochecompaan
2074 # . Added feature #526730 - search for messages capability
2075 #
2076 #Revision 1.30 2002/02/27 03:40:59 richard
2077 #Ran it through pychecker, made fixes
2078 #
2079 #Revision 1.29 2002/02/25 14:34:31 grubert
2080 # . use blobfiles in back_anydbm which is used in back_bsddb.
2081 # change test_db as dirlist does not work for subdirectories.
2082 # ATTENTION: blobfiles now creates subdirectories for files.
2083 #
2084 #Revision 1.28 2002/02/16 09:14:17 richard
2085 # . #514854 ] History: "User" is always ticket creator
2086 #
2087 #Revision 1.27 2002/01/22 07:21:13 richard
2088 #. fixed back_bsddb so it passed the journal tests
2089 #
2090 #... it didn't seem happy using the back_anydbm _open method, which is odd.
2091 #Yet another occurrance of whichdb not being able to recognise older bsddb
2092 #databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
2093 #process.
2094 #
2095 #Revision 1.26 2002/01/22 05:18:38 rochecompaan
2096 #last_set_entry was referenced before assignment
2097 #
2098 #Revision 1.25 2002/01/22 05:06:08 rochecompaan
2099 #We need to keep the last 'set' entry in the journal to preserve
2100 #information on 'activity' for nodes.
2101 #
2102 #Revision 1.24 2002/01/21 16:33:20 rochecompaan
2103 #You can now use the roundup-admin tool to pack the database
2104 #
2105 #Revision 1.23 2002/01/18 04:32:04 richard
2106 #Rollback was breaking because a message hadn't actually been written to the file. Needs
2107 #more investigation.
2108 #
2109 #Revision 1.22 2002/01/14 02:20:15 richard
2110 # . changed all config accesses so they access either the instance or the
2111 # config attriubute on the db. This means that all config is obtained from
2112 # instance_config instead of the mish-mash of classes. This will make
2113 # switching to a ConfigParser setup easier too, I hope.
2114 #
2115 #At a minimum, this makes migration a _little_ easier (a lot easier in the
2116 #0.5.0 switch, I hope!)
2117 #
2118 #Revision 1.21 2002/01/02 02:31:38 richard
2119 #Sorry for the huge checkin message - I was only intending to implement #496356
2120 #but I found a number of places where things had been broken by transactions:
2121 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
2122 # for _all_ roundup-generated smtp messages to be sent to.
2123 # . the transaction cache had broken the roundupdb.Class set() reactors
2124 # . newly-created author users in the mailgw weren't being committed to the db
2125 #
2126 #Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
2127 #on when I found that stuff :):
2128 # . #496356 ] Use threading in messages
2129 # . detectors were being registered multiple times
2130 # . added tests for mailgw
2131 # . much better attaching of erroneous messages in the mail gateway
2132 #
2133 #Revision 1.20 2001/12/18 15:30:34 rochecompaan
2134 #Fixed bugs:
2135 # . Fixed file creation and retrieval in same transaction in anydbm
2136 # backend
2137 # . Cgi interface now renders new issue after issue creation
2138 # . Could not set issue status to resolved through cgi interface
2139 # . Mail gateway was changing status back to 'chatting' if status was
2140 # omitted as an argument
2141 #
2142 #Revision 1.19 2001/12/17 03:52:48 richard
2143 #Implemented file store rollback. As a bonus, the hyperdb is now capable of
2144 #storing more than one file per node - if a property name is supplied,
2145 #the file is called designator.property.
2146 #I decided not to migrate the existing files stored over to the new naming
2147 #scheme - the FileClass just doesn't specify the property name.
2148 #
2149 #Revision 1.18 2001/12/16 10:53:38 richard
2150 #take a copy of the node dict so that the subsequent set
2151 #operation doesn't modify the oldvalues structure
2152 #
2153 #Revision 1.17 2001/12/14 23:42:57 richard
2154 #yuck, a gdbm instance tests false :(
2155 #I've left the debugging code in - it should be removed one day if we're ever
2156 #_really_ anal about performace :)
2157 #
2158 #Revision 1.16 2001/12/12 03:23:14 richard
2159 #Cor blimey this anydbm/whichdb stuff is yecchy. Turns out that whichdb
2160 #incorrectly identifies a dbm file as a dbhash file on my system. This has
2161 #been submitted to the python bug tracker as issue #491888:
2162 #https://sourceforge.net/tracker/index.php?func=detail&aid=491888&group_id=5470&atid=105470
2163 #
2164 #Revision 1.15 2001/12/12 02:30:51 richard
2165 #I fixed the problems with people whose anydbm was using the dbm module at the
2166 #backend. It turns out the dbm module modifies the file name to append ".db"
2167 #and my check to determine if we're opening an existing or new db just
2168 #tested os.path.exists() on the filename. Well, no longer! We now perform a
2169 #much better check _and_ cope with the anydbm implementation module changing
2170 #too!
2171 #I also fixed the backends __init__ so only ImportError is squashed.
2172 #
2173 #Revision 1.14 2001/12/10 22:20:01 richard
2174 #Enabled transaction support in the bsddb backend. It uses the anydbm code
2175 #where possible, only replacing methods where the db is opened (it uses the
2176 #btree opener specifically.)
2177 #Also cleaned up some change note generation.
2178 #Made the backends package work with pydoc too.
2179 #
2180 #Revision 1.13 2001/12/02 05:06:16 richard
2181 #. We now use weakrefs in the Classes to keep the database reference, so
2182 # the close() method on the database is no longer needed.
2183 # I bumped the minimum python requirement up to 2.1 accordingly.
2184 #. #487480 ] roundup-server
2185 #. #487476 ] INSTALL.txt
2186 #
2187 #I also cleaned up the change message / post-edit stuff in the cgi client.
2188 #There's now a clearly marked "TODO: append the change note" where I believe
2189 #the change note should be added there. The "changes" list will obviously
2190 #have to be modified to be a dict of the changes, or somesuch.
2191 #
2192 #More testing needed.
2193 #
2194 #Revision 1.12 2001/12/01 07:17:50 richard
2195 #. We now have basic transaction support! Information is only written to
2196 # the database when the commit() method is called. Only the anydbm
2197 # backend is modified in this way - neither of the bsddb backends have been.
2198 # The mail, admin and cgi interfaces all use commit (except the admin tool
2199 # doesn't have a commit command, so interactive users can't commit...)
2200 #. Fixed login/registration forwarding the user to the right page (or not,
2201 # on a failure)
2202 #
2203 #Revision 1.11 2001/11/21 02:34:18 richard
2204 #Added a target version field to the extended issue schema
2205 #
2206 #Revision 1.10 2001/10/09 23:58:10 richard
2207 #Moved the data stringification up into the hyperdb.Class class' get, set
2208 #and create methods. This means that the data is also stringified for the
2209 #journal call, and removes duplication of code from the backends. The
2210 #backend code now only sees strings.
2211 #
2212 #Revision 1.9 2001/10/09 07:25:59 richard
2213 #Added the Password property type. See "pydoc roundup.password" for
2214 #implementation details. Have updated some of the documentation too.
2215 #
2216 #Revision 1.8 2001/09/29 13:27:00 richard
2217 #CGI interfaces now spit up a top-level index of all the instances they can
2218 #serve.
2219 #
2220 #Revision 1.7 2001/08/12 06:32:36 richard
2221 #using isinstance(blah, Foo) now instead of isFooType
2222 #
2223 #Revision 1.6 2001/08/07 00:24:42 richard
2224 #stupid typo
2225 #
2226 #Revision 1.5 2001/08/07 00:15:51 richard
2227 #Added the copyright/license notice to (nearly) all files at request of
2228 #Bizar Software.
2229 #
2230 #Revision 1.4 2001/07/30 01:41:36 richard
2231 #Makes schema changes mucho easier.
2232 #
2233 #Revision 1.3 2001/07/25 01:23:07 richard
2234 #Added the Roundup spec to the new documentation directory.
2235 #
2236 #Revision 1.2 2001/07/23 08:20:44 richard
2237 #Moved over to using marshal in the bsddb and anydbm backends.
2238 #roundup-admin now has a "freshen" command that'll load/save all nodes (not
2239 # retired - mod hyperdb.Class.list() so it lists retired nodes)
2240 #
2241 #