comparison roundup/backends/back_anydbm.py @ 4075:b87d022a2f40

Uniformly use """...""" instead of '''...''' for comments.
author Stefan Seefeld <stefan@seefeld.name>
date Tue, 24 Feb 2009 02:12:23 +0000
parents 27a9906cd8d1
children 94c992852f12
comparison
equal deleted inserted replaced
4074:e039f3cbbb96 4075:b87d022a2f40
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 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" 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, 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 # 17 #
18 #$Id: back_anydbm.py,v 1.211 2008-08-07 05:53:14 richard Exp $ 18 """This module defines a backend that saves the hyperdatabase in a
19 '''This module defines a backend that saves the hyperdatabase in a
20 database chosen by anydbm. It is guaranteed to always be available in python 19 database chosen by anydbm. It is guaranteed to always be available in python
21 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several 20 versions >2.1.1 (the dumbdbm fallback in 2.1.1 and earlier has several
22 serious bugs, and is not available) 21 serious bugs, and is not available)
23 ''' 22 """
24 __docformat__ = 'restructuredtext' 23 __docformat__ = 'restructuredtext'
25 24
26 try: 25 try:
27 import anydbm, sys 26 import anydbm, sys
28 # dumbdbm only works in python 2.1.2+ 27 # dumbdbm only works in python 2.1.2+
60 59
61 # 60 #
62 # Now the database 61 # Now the database
63 # 62 #
64 class Database(FileStorage, hyperdb.Database, roundupdb.Database): 63 class Database(FileStorage, hyperdb.Database, roundupdb.Database):
65 '''A database for storing records containing flexible data types. 64 """A database for storing records containing flexible data types.
66 65
67 Transaction stuff TODO: 66 Transaction stuff TODO:
68 67
69 - check the timestamp of the class file and nuke the cache if it's 68 - check the timestamp of the class file and nuke the cache if it's
70 modified. Do some sort of conflict checking on the dirty stuff. 69 modified. Do some sort of conflict checking on the dirty stuff.
71 - perhaps detect write collisions (related to above)? 70 - perhaps detect write collisions (related to above)?
72 ''' 71 """
73 def __init__(self, config, journaltag=None): 72 def __init__(self, config, journaltag=None):
74 '''Open a hyperdatabase given a specifier to some storage. 73 """Open a hyperdatabase given a specifier to some storage.
75 74
76 The 'storagelocator' is obtained from config.DATABASE. 75 The 'storagelocator' is obtained from config.DATABASE.
77 The meaning of 'storagelocator' depends on the particular 76 The meaning of 'storagelocator' depends on the particular
78 implementation of the hyperdatabase. It could be a file name, 77 implementation of the hyperdatabase. It could be a file name,
79 a directory path, a socket descriptor for a connection to a 78 a directory path, a socket descriptor for a connection to a
82 The 'journaltag' is a token that will be attached to the journal 81 The 'journaltag' is a token that will be attached to the journal
83 entries for any edits done on the database. If 'journaltag' is 82 entries for any edits done on the database. If 'journaltag' is
84 None, the database is opened in read-only mode: the Class.create(), 83 None, the database is opened in read-only mode: the Class.create(),
85 Class.set(), Class.retire(), and Class.restore() methods are 84 Class.set(), Class.retire(), and Class.restore() methods are
86 disabled. 85 disabled.
87 ''' 86 """
88 FileStorage.__init__(self, config.UMASK) 87 FileStorage.__init__(self, config.UMASK)
89 self.config, self.journaltag = config, journaltag 88 self.config, self.journaltag = config, journaltag
90 self.dir = config.DATABASE 89 self.dir = config.DATABASE
91 self.classes = {} 90 self.classes = {}
92 self.cache = {} # cache of nodes loaded or created 91 self.cache = {} # cache of nodes loaded or created
105 self.lockfile = locking.acquire_lock(lockfilenm) 104 self.lockfile = locking.acquire_lock(lockfilenm)
106 self.lockfile.write(str(os.getpid())) 105 self.lockfile.write(str(os.getpid()))
107 self.lockfile.flush() 106 self.lockfile.flush()
108 107
109 def post_init(self): 108 def post_init(self):
110 '''Called once the schema initialisation has finished. 109 """Called once the schema initialisation has finished.
111 ''' 110 """
112 # reindex the db if necessary 111 # reindex the db if necessary
113 if self.indexer.should_reindex(): 112 if self.indexer.should_reindex():
114 self.reindex() 113 self.reindex()
115 114
116 def refresh_database(self): 115 def refresh_database(self):
144 143
145 # 144 #
146 # Classes 145 # Classes
147 # 146 #
148 def __getattr__(self, classname): 147 def __getattr__(self, classname):
149 '''A convenient way of calling self.getclass(classname).''' 148 """A convenient way of calling self.getclass(classname)."""
150 if self.classes.has_key(classname): 149 if self.classes.has_key(classname):
151 return self.classes[classname] 150 return self.classes[classname]
152 raise AttributeError, classname 151 raise AttributeError, classname
153 152
154 def addclass(self, cl): 153 def addclass(self, cl):
164 description="User is allowed to edit "+cn) 163 description="User is allowed to edit "+cn)
165 self.security.addPermission(name="View", klass=cn, 164 self.security.addPermission(name="View", klass=cn,
166 description="User is allowed to access "+cn) 165 description="User is allowed to access "+cn)
167 166
168 def getclasses(self): 167 def getclasses(self):
169 '''Return a list of the names of all existing classes.''' 168 """Return a list of the names of all existing classes."""
170 l = self.classes.keys() 169 l = self.classes.keys()
171 l.sort() 170 l.sort()
172 return l 171 return l
173 172
174 def getclass(self, classname): 173 def getclass(self, classname):
175 '''Get the Class object representing a particular class. 174 """Get the Class object representing a particular class.
176 175
177 If 'classname' is not a valid class name, a KeyError is raised. 176 If 'classname' is not a valid class name, a KeyError is raised.
178 ''' 177 """
179 try: 178 try:
180 return self.classes[classname] 179 return self.classes[classname]
181 except KeyError: 180 except KeyError:
182 raise KeyError, 'There is no class called "%s"'%classname 181 raise KeyError, 'There is no class called "%s"'%classname
183 182
184 # 183 #
185 # Class DBs 184 # Class DBs
186 # 185 #
187 def clear(self): 186 def clear(self):
188 '''Delete all database contents 187 """Delete all database contents
189 ''' 188 """
190 logging.getLogger('hyperdb').info('clear') 189 logging.getLogger('hyperdb').info('clear')
191 for cn in self.classes.keys(): 190 for cn in self.classes.keys():
192 for dummy in 'nodes', 'journals': 191 for dummy in 'nodes', 'journals':
193 path = os.path.join(self.dir, 'journals.%s'%cn) 192 path = os.path.join(self.dir, 'journals.%s'%cn)
194 if os.path.exists(path): 193 if os.path.exists(path):
201 os.remove(path) 200 os.remove(path)
202 elif os.path.exists(path+'.db'): # dbm appends .db 201 elif os.path.exists(path+'.db'): # dbm appends .db
203 os.remove(path+'.db') 202 os.remove(path+'.db')
204 203
205 def getclassdb(self, classname, mode='r'): 204 def getclassdb(self, classname, mode='r'):
206 ''' grab a connection to the class db that will be used for 205 """ grab a connection to the class db that will be used for
207 multiple actions 206 multiple actions
208 ''' 207 """
209 return self.opendb('nodes.%s'%classname, mode) 208 return self.opendb('nodes.%s'%classname, mode)
210 209
211 def determine_db_type(self, path): 210 def determine_db_type(self, path):
212 ''' determine which DB wrote the class file 211 """ determine which DB wrote the class file
213 ''' 212 """
214 db_type = '' 213 db_type = ''
215 if os.path.exists(path): 214 if os.path.exists(path):
216 db_type = whichdb.whichdb(path) 215 db_type = whichdb.whichdb(path)
217 if not db_type: 216 if not db_type:
218 raise hyperdb.DatabaseError, \ 217 raise hyperdb.DatabaseError, \
222 # anydbm says it's dbhash or not! 221 # anydbm says it's dbhash or not!
223 db_type = 'dbm' 222 db_type = 'dbm'
224 return db_type 223 return db_type
225 224
226 def opendb(self, name, mode): 225 def opendb(self, name, mode):
227 '''Low-level database opener that gets around anydbm/dbm 226 """Low-level database opener that gets around anydbm/dbm
228 eccentricities. 227 eccentricities.
229 ''' 228 """
230 # figure the class db type 229 # figure the class db type
231 path = os.path.join(os.getcwd(), self.dir, name) 230 path = os.path.join(os.getcwd(), self.dir, name)
232 db_type = self.determine_db_type(path) 231 db_type = self.determine_db_type(path)
233 232
234 # new database? let anydbm pick the best dbm 233 # new database? let anydbm pick the best dbm
251 250
252 # 251 #
253 # Node IDs 252 # Node IDs
254 # 253 #
255 def newid(self, classname): 254 def newid(self, classname):
256 ''' Generate a new id for the given class 255 """ Generate a new id for the given class
257 ''' 256 """
258 # open the ids DB - create if if doesn't exist 257 # open the ids DB - create if if doesn't exist
259 db = self.opendb('_ids', 'c') 258 db = self.opendb('_ids', 'c')
260 if db.has_key(classname): 259 if db.has_key(classname):
261 newid = db[classname] = str(int(db[classname]) + 1) 260 newid = db[classname] = str(int(db[classname]) + 1)
262 else: 261 else:
265 db[classname] = newid 264 db[classname] = newid
266 db.close() 265 db.close()
267 return newid 266 return newid
268 267
269 def setid(self, classname, setid): 268 def setid(self, classname, setid):
270 ''' Set the id counter: used during import of database 269 """ Set the id counter: used during import of database
271 ''' 270 """
272 # open the ids DB - create if if doesn't exist 271 # open the ids DB - create if if doesn't exist
273 db = self.opendb('_ids', 'c') 272 db = self.opendb('_ids', 'c')
274 db[classname] = str(setid) 273 db[classname] = str(setid)
275 db.close() 274 db.close()
276 275
277 # 276 #
278 # Nodes 277 # Nodes
279 # 278 #
280 def addnode(self, classname, nodeid, node): 279 def addnode(self, classname, nodeid, node):
281 ''' add the specified node to its class's db 280 """ add the specified node to its class's db
282 ''' 281 """
283 # we'll be supplied these props if we're doing an import 282 # we'll be supplied these props if we're doing an import
284 if not node.has_key('creator'): 283 if not node.has_key('creator'):
285 # add in the "calculated" properties (dupe so we don't affect 284 # add in the "calculated" properties (dupe so we don't affect
286 # calling code's node assumptions) 285 # calling code's node assumptions)
287 node = node.copy() 286 node = node.copy()
292 self.newnodes.setdefault(classname, {})[nodeid] = 1 291 self.newnodes.setdefault(classname, {})[nodeid] = 1
293 self.cache.setdefault(classname, {})[nodeid] = node 292 self.cache.setdefault(classname, {})[nodeid] = node
294 self.savenode(classname, nodeid, node) 293 self.savenode(classname, nodeid, node)
295 294
296 def setnode(self, classname, nodeid, node): 295 def setnode(self, classname, nodeid, node):
297 ''' change the specified node 296 """ change the specified node
298 ''' 297 """
299 self.dirtynodes.setdefault(classname, {})[nodeid] = 1 298 self.dirtynodes.setdefault(classname, {})[nodeid] = 1
300 299
301 # can't set without having already loaded the node 300 # can't set without having already loaded the node
302 self.cache[classname][nodeid] = node 301 self.cache[classname][nodeid] = node
303 self.savenode(classname, nodeid, node) 302 self.savenode(classname, nodeid, node)
304 303
305 def savenode(self, classname, nodeid, node): 304 def savenode(self, classname, nodeid, node):
306 ''' perform the saving of data specified by the set/addnode 305 """ perform the saving of data specified by the set/addnode
307 ''' 306 """
308 if __debug__: 307 if __debug__:
309 logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node)) 308 logging.getLogger('hyperdb').debug('save %s%s %r'%(classname, nodeid, node))
310 self.transactions.append((self.doSaveNode, (classname, nodeid, node))) 309 self.transactions.append((self.doSaveNode, (classname, nodeid, node)))
311 310
312 def getnode(self, classname, nodeid, db=None, cache=1): 311 def getnode(self, classname, nodeid, db=None, cache=1):
313 ''' get a node from the database 312 """ get a node from the database
314 313
315 Note the "cache" parameter is not used, and exists purely for 314 Note the "cache" parameter is not used, and exists purely for
316 backward compatibility! 315 backward compatibility!
317 ''' 316 """
318 # try the cache 317 # try the cache
319 cache_dict = self.cache.setdefault(classname, {}) 318 cache_dict = self.cache.setdefault(classname, {})
320 if cache_dict.has_key(nodeid): 319 if cache_dict.has_key(nodeid):
321 if __debug__: 320 if __debug__:
322 logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid)) 321 logging.getLogger('hyperdb').debug('get %s%s cached'%(classname, nodeid))
353 self.stats['get_items'] += (time.time() - start_t) 352 self.stats['get_items'] += (time.time() - start_t)
354 353
355 return res 354 return res
356 355
357 def destroynode(self, classname, nodeid): 356 def destroynode(self, classname, nodeid):
358 '''Remove a node from the database. Called exclusively by the 357 """Remove a node from the database. Called exclusively by the
359 destroy() method on Class. 358 destroy() method on Class.
360 ''' 359 """
361 logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid)) 360 logging.getLogger('hyperdb').info('destroy %s%s'%(classname, nodeid))
362 361
363 # remove from cache and newnodes if it's there 362 # remove from cache and newnodes if it's there
364 if (self.cache.has_key(classname) and 363 if (self.cache.has_key(classname) and
365 self.cache[classname].has_key(nodeid)): 364 self.cache[classname].has_key(nodeid)):
379 # add the destroy commit action 378 # add the destroy commit action
380 self.transactions.append((self.doDestroyNode, (classname, nodeid))) 379 self.transactions.append((self.doDestroyNode, (classname, nodeid)))
381 self.transactions.append((FileStorage.destroy, (self, classname, nodeid))) 380 self.transactions.append((FileStorage.destroy, (self, classname, nodeid)))
382 381
383 def serialise(self, classname, node): 382 def serialise(self, classname, node):
384 '''Copy the node contents, converting non-marshallable data into 383 """Copy the node contents, converting non-marshallable data into
385 marshallable data. 384 marshallable data.
386 ''' 385 """
387 properties = self.getclass(classname).getprops() 386 properties = self.getclass(classname).getprops()
388 d = {} 387 d = {}
389 for k, v in node.items(): 388 for k, v in node.items():
390 if k == self.RETIRED_FLAG: 389 if k == self.RETIRED_FLAG:
391 d[k] = v 390 d[k] = v
407 else: 406 else:
408 d[k] = v 407 d[k] = v
409 return d 408 return d
410 409
411 def unserialise(self, classname, node): 410 def unserialise(self, classname, node):
412 '''Decode the marshalled node data 411 """Decode the marshalled node data
413 ''' 412 """
414 properties = self.getclass(classname).getprops() 413 properties = self.getclass(classname).getprops()
415 d = {} 414 d = {}
416 for k, v in node.items(): 415 for k, v in node.items():
417 # if the property doesn't exist, or is the "retired" flag then 416 # if the property doesn't exist, or is the "retired" flag then
418 # it won't be in the properties dict 417 # it won't be in the properties dict
434 else: 433 else:
435 d[k] = v 434 d[k] = v
436 return d 435 return d
437 436
438 def hasnode(self, classname, nodeid, db=None): 437 def hasnode(self, classname, nodeid, db=None):
439 ''' determine if the database has a given node 438 """ determine if the database has a given node
440 ''' 439 """
441 # try the cache 440 # try the cache
442 cache = self.cache.setdefault(classname, {}) 441 cache = self.cache.setdefault(classname, {})
443 if cache.has_key(nodeid): 442 if cache.has_key(nodeid):
444 return 1 443 return 1
445 444
472 # 471 #
473 # Journal 472 # Journal
474 # 473 #
475 def addjournal(self, classname, nodeid, action, params, creator=None, 474 def addjournal(self, classname, nodeid, action, params, creator=None,
476 creation=None): 475 creation=None):
477 ''' Journal the Action 476 """ Journal the Action
478 'action' may be: 477 'action' may be:
479 478
480 'create' or 'set' -- 'params' is a dictionary of property values 479 'create' or 'set' -- 'params' is a dictionary of property values
481 'link' or 'unlink' -- 'params' is (classname, nodeid, propname) 480 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
482 'retire' -- 'params' is None 481 'retire' -- 'params' is None
483 482
484 'creator' -- the user performing the action, which defaults to 483 'creator' -- the user performing the action, which defaults to
485 the current user. 484 the current user.
486 ''' 485 """
487 if __debug__: 486 if __debug__:
488 logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname, 487 logging.getLogger('hyperdb').debug('addjournal %s%s %s %r %s %r'%(classname,
489 nodeid, action, params, creator, creation)) 488 nodeid, action, params, creator, creation))
490 if creator is None: 489 if creator is None:
491 creator = self.getuid() 490 creator = self.getuid()
492 self.transactions.append((self.doSaveJournal, (classname, nodeid, 491 self.transactions.append((self.doSaveJournal, (classname, nodeid,
493 action, params, creator, creation))) 492 action, params, creator, creation)))
494 493
495 def setjournal(self, classname, nodeid, journal): 494 def setjournal(self, classname, nodeid, journal):
496 '''Set the journal to the "journal" list.''' 495 """Set the journal to the "journal" list."""
497 if __debug__: 496 if __debug__:
498 logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname, 497 logging.getLogger('hyperdb').debug('setjournal %s%s %r'%(classname,
499 nodeid, journal)) 498 nodeid, journal))
500 self.transactions.append((self.doSetJournal, (classname, nodeid, 499 self.transactions.append((self.doSetJournal, (classname, nodeid,
501 journal))) 500 journal)))
502 501
503 def getjournal(self, classname, nodeid): 502 def getjournal(self, classname, nodeid):
504 ''' get the journal for id 503 """ get the journal for id
505 504
506 Raise IndexError if the node doesn't exist (as per history()'s 505 Raise IndexError if the node doesn't exist (as per history()'s
507 API) 506 API)
508 ''' 507 """
509 # our journal result 508 # our journal result
510 res = [] 509 res = []
511 510
512 # add any journal entries for transactions not committed to the 511 # add any journal entries for transactions not committed to the
513 # database 512 # database
552 for nodeid, date_stamp, user, action, params in journal: 551 for nodeid, date_stamp, user, action, params in journal:
553 res.append((nodeid, date.Date(date_stamp), user, action, params)) 552 res.append((nodeid, date.Date(date_stamp), user, action, params))
554 return res 553 return res
555 554
556 def pack(self, pack_before): 555 def pack(self, pack_before):
557 ''' Delete all journal entries except "create" before 'pack_before'. 556 """ Delete all journal entries except "create" before 'pack_before'.
558 ''' 557 """
559 pack_before = pack_before.serialise() 558 pack_before = pack_before.serialise()
560 for classname in self.getclasses(): 559 for classname in self.getclasses():
561 packed = 0 560 packed = 0
562 # get the journal db 561 # get the journal db
563 db_name = 'journals.%s'%classname 562 db_name = 'journals.%s'%classname
592 591
593 # 592 #
594 # Basic transaction support 593 # Basic transaction support
595 # 594 #
596 def commit(self, fail_ok=False): 595 def commit(self, fail_ok=False):
597 ''' Commit the current transactions. 596 """ Commit the current transactions.
598 597
599 Save all data changed since the database was opened or since the 598 Save all data changed since the database was opened or since the
600 last commit() or rollback(). 599 last commit() or rollback().
601 600
602 fail_ok indicates that the commit is allowed to fail. This is used 601 fail_ok indicates that the commit is allowed to fail. This is used
603 in the web interface when committing cleaning of the session 602 in the web interface when committing cleaning of the session
604 database. We don't care if there's a concurrency issue there. 603 database. We don't care if there's a concurrency issue there.
605 604
606 The only backend this seems to affect is postgres. 605 The only backend this seems to affect is postgres.
607 ''' 606 """
608 logging.getLogger('hyperdb').info('commit %s transactions'%( 607 logging.getLogger('hyperdb').info('commit %s transactions'%(
609 len(self.transactions))) 608 len(self.transactions)))
610 609
611 # keep a handle to all the database files opened 610 # keep a handle to all the database files opened
612 self.databases = {} 611 self.databases = {}
643 self.newnodes = {} 642 self.newnodes = {}
644 self.destroyednodes = {} 643 self.destroyednodes = {}
645 self.transactions = [] 644 self.transactions = []
646 645
647 def getCachedClassDB(self, classname): 646 def getCachedClassDB(self, classname):
648 ''' get the class db, looking in our cache of databases for commit 647 """ get the class db, looking in our cache of databases for commit
649 ''' 648 """
650 # get the database handle 649 # get the database handle
651 db_name = 'nodes.%s'%classname 650 db_name = 'nodes.%s'%classname
652 if not self.databases.has_key(db_name): 651 if not self.databases.has_key(db_name):
653 self.databases[db_name] = self.getclassdb(classname, 'c') 652 self.databases[db_name] = self.getclassdb(classname, 'c')
654 return self.databases[db_name] 653 return self.databases[db_name]
661 660
662 # return the classname, nodeid so we reindex this content 661 # return the classname, nodeid so we reindex this content
663 return (classname, nodeid) 662 return (classname, nodeid)
664 663
665 def getCachedJournalDB(self, classname): 664 def getCachedJournalDB(self, classname):
666 ''' get the journal db, looking in our cache of databases for commit 665 """ get the journal db, looking in our cache of databases for commit
667 ''' 666 """
668 # get the database handle 667 # get the database handle
669 db_name = 'journals.%s'%classname 668 db_name = 'journals.%s'%classname
670 if not self.databases.has_key(db_name): 669 if not self.databases.has_key(db_name):
671 self.databases[db_name] = self.opendb(db_name, 'c') 670 self.databases[db_name] = self.opendb(db_name, 'c')
672 return self.databases[db_name] 671 return self.databases[db_name]
724 db = self.getCachedJournalDB(classname) 723 db = self.getCachedJournalDB(classname)
725 if db.has_key(nodeid): 724 if db.has_key(nodeid):
726 del db[nodeid] 725 del db[nodeid]
727 726
728 def rollback(self): 727 def rollback(self):
729 ''' Reverse all actions from the current transaction. 728 """ Reverse all actions from the current transaction.
730 ''' 729 """
731 logging.getLogger('hyperdb').info('rollback %s transactions'%( 730 logging.getLogger('hyperdb').info('rollback %s transactions'%(
732 len(self.transactions))) 731 len(self.transactions)))
733 732
734 for method, args in self.transactions: 733 for method, args in self.transactions:
735 # delete temporary files 734 # delete temporary files
740 self.newnodes = {} 739 self.newnodes = {}
741 self.destroyednodes = {} 740 self.destroyednodes = {}
742 self.transactions = [] 741 self.transactions = []
743 742
744 def close(self): 743 def close(self):
745 ''' Nothing to do 744 """ Nothing to do
746 ''' 745 """
747 if self.lockfile is not None: 746 if self.lockfile is not None:
748 locking.release_lock(self.lockfile) 747 locking.release_lock(self.lockfile)
749 self.lockfile.close() 748 self.lockfile.close()
750 self.lockfile = None 749 self.lockfile = None
751 750
752 _marker = [] 751 _marker = []
753 class Class(hyperdb.Class): 752 class Class(hyperdb.Class):
754 '''The handle to a particular class of nodes in a hyperdatabase.''' 753 """The handle to a particular class of nodes in a hyperdatabase."""
755 754
756 def enableJournalling(self): 755 def enableJournalling(self):
757 '''Turn journalling on for this class 756 """Turn journalling on for this class
758 ''' 757 """
759 self.do_journal = 1 758 self.do_journal = 1
760 759
761 def disableJournalling(self): 760 def disableJournalling(self):
762 '''Turn journalling off for this class 761 """Turn journalling off for this class
763 ''' 762 """
764 self.do_journal = 0 763 self.do_journal = 0
765 764
766 # Editing nodes: 765 # Editing nodes:
767 766
768 def create(self, **propvalues): 767 def create(self, **propvalues):
769 '''Create a new node of this class and return its id. 768 """Create a new node of this class and return its id.
770 769
771 The keyword arguments in 'propvalues' map property names to values. 770 The keyword arguments in 'propvalues' map property names to values.
772 771
773 The values of arguments must be acceptable for the types of their 772 The values of arguments must be acceptable for the types of their
774 corresponding properties or a TypeError is raised. 773 corresponding properties or a TypeError is raised.
782 If an id in a link or multilink property does not refer to a valid 781 If an id in a link or multilink property does not refer to a valid
783 node, an IndexError is raised. 782 node, an IndexError is raised.
784 783
785 These operations trigger detectors and can be vetoed. Attempts 784 These operations trigger detectors and can be vetoed. Attempts
786 to modify the "creation" or "activity" properties cause a KeyError. 785 to modify the "creation" or "activity" properties cause a KeyError.
787 ''' 786 """
788 self.fireAuditors('create', None, propvalues) 787 self.fireAuditors('create', None, propvalues)
789 newid = self.create_inner(**propvalues) 788 newid = self.create_inner(**propvalues)
790 self.fireReactors('create', newid, None) 789 self.fireReactors('create', newid, None)
791 return newid 790 return newid
792 791
793 def create_inner(self, **propvalues): 792 def create_inner(self, **propvalues):
794 ''' Called by create, in-between the audit and react calls. 793 """ Called by create, in-between the audit and react calls.
795 ''' 794 """
796 if propvalues.has_key('id'): 795 if propvalues.has_key('id'):
797 raise KeyError, '"id" is reserved' 796 raise KeyError, '"id" is reserved'
798 797
799 if self.db.journaltag is None: 798 if self.db.journaltag is None:
800 raise hyperdb.DatabaseError, _('Database open read-only') 799 raise hyperdb.DatabaseError, _('Database open read-only')
924 self.db.addjournal(self.classname, newid, 'create', {}) 923 self.db.addjournal(self.classname, newid, 'create', {})
925 924
926 return newid 925 return newid
927 926
928 def get(self, nodeid, propname, default=_marker, cache=1): 927 def get(self, nodeid, propname, default=_marker, cache=1):
929 '''Get the value of a property on an existing node of this class. 928 """Get the value of a property on an existing node of this class.
930 929
931 'nodeid' must be the id of an existing node of this class or an 930 'nodeid' must be the id of an existing node of this class or an
932 IndexError is raised. 'propname' must be the name of a property 931 IndexError is raised. 'propname' must be the name of a property
933 of this class or a KeyError is raised. 932 of this class or a KeyError is raised.
934 933
935 'cache' exists for backward compatibility, and is not used. 934 'cache' exists for backward compatibility, and is not used.
936 935
937 Attempts to get the "creation" or "activity" properties should 936 Attempts to get the "creation" or "activity" properties should
938 do the right thing. 937 do the right thing.
939 ''' 938 """
940 if propname == 'id': 939 if propname == 'id':
941 return nodeid 940 return nodeid
942 941
943 # get the node's dict 942 # get the node's dict
944 d = self.db.getnode(self.classname, nodeid) 943 d = self.db.getnode(self.classname, nodeid)
1024 return d[propname][:] 1023 return d[propname][:]
1025 1024
1026 return d[propname] 1025 return d[propname]
1027 1026
1028 def set(self, nodeid, **propvalues): 1027 def set(self, nodeid, **propvalues):
1029 '''Modify a property on an existing node of this class. 1028 """Modify a property on an existing node of this class.
1030 1029
1031 'nodeid' must be the id of an existing node of this class or an 1030 'nodeid' must be the id of an existing node of this class or an
1032 IndexError is raised. 1031 IndexError is raised.
1033 1032
1034 Each key in 'propvalues' must be the name of a property of this 1033 Each key in 'propvalues' must be the name of a property of this
1043 If the value of a Link or Multilink property contains an invalid 1042 If the value of a Link or Multilink property contains an invalid
1044 node id, a ValueError is raised. 1043 node id, a ValueError is raised.
1045 1044
1046 These operations trigger detectors and can be vetoed. Attempts 1045 These operations trigger detectors and can be vetoed. Attempts
1047 to modify the "creation" or "activity" properties cause a KeyError. 1046 to modify the "creation" or "activity" properties cause a KeyError.
1048 ''' 1047 """
1049 self.fireAuditors('set', nodeid, propvalues) 1048 self.fireAuditors('set', nodeid, propvalues)
1050 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid)) 1049 oldvalues = copy.deepcopy(self.db.getnode(self.classname, nodeid))
1051 for name,prop in self.getprops(protected=0).items(): 1050 for name,prop in self.getprops(protected=0).items():
1052 if oldvalues.has_key(name): 1051 if oldvalues.has_key(name):
1053 continue 1052 continue
1058 propvalues = self.set_inner(nodeid, **propvalues) 1057 propvalues = self.set_inner(nodeid, **propvalues)
1059 self.fireReactors('set', nodeid, oldvalues) 1058 self.fireReactors('set', nodeid, oldvalues)
1060 return propvalues 1059 return propvalues
1061 1060
1062 def set_inner(self, nodeid, **propvalues): 1061 def set_inner(self, nodeid, **propvalues):
1063 ''' Called by set, in-between the audit and react calls. 1062 """ Called by set, in-between the audit and react calls.
1064 ''' 1063 """
1065 if not propvalues: 1064 if not propvalues:
1066 return propvalues 1065 return propvalues
1067 1066
1068 if propvalues.has_key('creation') or propvalues.has_key('activity'): 1067 if propvalues.has_key('creation') or propvalues.has_key('activity'):
1069 raise KeyError, '"creation" and "activity" are reserved' 1068 raise KeyError, '"creation" and "activity" are reserved'
1255 self.db.addjournal(self.classname, nodeid, 'set', journalvalues) 1254 self.db.addjournal(self.classname, nodeid, 'set', journalvalues)
1256 1255
1257 return propvalues 1256 return propvalues
1258 1257
1259 def retire(self, nodeid): 1258 def retire(self, nodeid):
1260 '''Retire a node. 1259 """Retire a node.
1261 1260
1262 The properties on the node remain available from the get() method, 1261 The properties on the node remain available from the get() method,
1263 and the node's id is never reused. 1262 and the node's id is never reused.
1264 1263
1265 Retired nodes are not returned by the find(), list(), or lookup() 1264 Retired nodes are not returned by the find(), list(), or lookup()
1266 methods, and other nodes may reuse the values of their key properties. 1265 methods, and other nodes may reuse the values of their key properties.
1267 1266
1268 These operations trigger detectors and can be vetoed. Attempts 1267 These operations trigger detectors and can be vetoed. Attempts
1269 to modify the "creation" or "activity" properties cause a KeyError. 1268 to modify the "creation" or "activity" properties cause a KeyError.
1270 ''' 1269 """
1271 if self.db.journaltag is None: 1270 if self.db.journaltag is None:
1272 raise hyperdb.DatabaseError, _('Database open read-only') 1271 raise hyperdb.DatabaseError, _('Database open read-only')
1273 1272
1274 self.fireAuditors('retire', nodeid, None) 1273 self.fireAuditors('retire', nodeid, None)
1275 1274
1280 self.db.addjournal(self.classname, nodeid, 'retired', None) 1279 self.db.addjournal(self.classname, nodeid, 'retired', None)
1281 1280
1282 self.fireReactors('retire', nodeid, None) 1281 self.fireReactors('retire', nodeid, None)
1283 1282
1284 def restore(self, nodeid): 1283 def restore(self, nodeid):
1285 '''Restpre a retired node. 1284 """Restpre a retired node.
1286 1285
1287 Make node available for all operations like it was before retirement. 1286 Make node available for all operations like it was before retirement.
1288 ''' 1287 """
1289 if self.db.journaltag is None: 1288 if self.db.journaltag is None:
1290 raise hyperdb.DatabaseError, _('Database open read-only') 1289 raise hyperdb.DatabaseError, _('Database open read-only')
1291 1290
1292 node = self.db.getnode(self.classname, nodeid) 1291 node = self.db.getnode(self.classname, nodeid)
1293 # check if key property was overrided 1292 # check if key property was overrided
1307 self.db.addjournal(self.classname, nodeid, 'restored', None) 1306 self.db.addjournal(self.classname, nodeid, 'restored', None)
1308 1307
1309 self.fireReactors('restore', nodeid, None) 1308 self.fireReactors('restore', nodeid, None)
1310 1309
1311 def is_retired(self, nodeid, cldb=None): 1310 def is_retired(self, nodeid, cldb=None):
1312 '''Return true if the node is retired. 1311 """Return true if the node is retired.
1313 ''' 1312 """
1314 node = self.db.getnode(self.classname, nodeid, cldb) 1313 node = self.db.getnode(self.classname, nodeid, cldb)
1315 if node.has_key(self.db.RETIRED_FLAG): 1314 if node.has_key(self.db.RETIRED_FLAG):
1316 return 1 1315 return 1
1317 return 0 1316 return 0
1318 1317
1319 def destroy(self, nodeid): 1318 def destroy(self, nodeid):
1320 '''Destroy a node. 1319 """Destroy a node.
1321 1320
1322 WARNING: this method should never be used except in extremely rare 1321 WARNING: this method should never be used except in extremely rare
1323 situations where there could never be links to the node being 1322 situations where there could never be links to the node being
1324 deleted 1323 deleted
1325 1324
1329 1328
1330 WARNING: really, use retire() instead 1329 WARNING: really, use retire() instead
1331 1330
1332 Well, I think that's enough warnings. This method exists mostly to 1331 Well, I think that's enough warnings. This method exists mostly to
1333 support the session storage of the cgi interface. 1332 support the session storage of the cgi interface.
1334 ''' 1333 """
1335 if self.db.journaltag is None: 1334 if self.db.journaltag is None:
1336 raise hyperdb.DatabaseError, _('Database open read-only') 1335 raise hyperdb.DatabaseError, _('Database open read-only')
1337 self.db.destroynode(self.classname, nodeid) 1336 self.db.destroynode(self.classname, nodeid)
1338 1337
1339 def history(self, nodeid): 1338 def history(self, nodeid):
1340 '''Retrieve the journal of edits on a particular node. 1339 """Retrieve the journal of edits on a particular node.
1341 1340
1342 'nodeid' must be the id of an existing node of this class or an 1341 'nodeid' must be the id of an existing node of this class or an
1343 IndexError is raised. 1342 IndexError is raised.
1344 1343
1345 The returned list contains tuples of the form 1344 The returned list contains tuples of the form
1346 1345
1347 (nodeid, date, tag, action, params) 1346 (nodeid, date, tag, action, params)
1348 1347
1349 'date' is a Timestamp object specifying the time of the change and 1348 'date' is a Timestamp object specifying the time of the change and
1350 'tag' is the journaltag specified when the database was opened. 1349 'tag' is the journaltag specified when the database was opened.
1351 ''' 1350 """
1352 if not self.do_journal: 1351 if not self.do_journal:
1353 raise ValueError, 'Journalling is disabled for this class' 1352 raise ValueError, 'Journalling is disabled for this class'
1354 return self.db.getjournal(self.classname, nodeid) 1353 return self.db.getjournal(self.classname, nodeid)
1355 1354
1356 # Locating nodes: 1355 # Locating nodes:
1357 def hasnode(self, nodeid): 1356 def hasnode(self, nodeid):
1358 '''Determine if the given nodeid actually exists 1357 """Determine if the given nodeid actually exists
1359 ''' 1358 """
1360 return self.db.hasnode(self.classname, nodeid) 1359 return self.db.hasnode(self.classname, nodeid)
1361 1360
1362 def setkey(self, propname): 1361 def setkey(self, propname):
1363 '''Select a String property of this class to be the key property. 1362 """Select a String property of this class to be the key property.
1364 1363
1365 'propname' must be the name of a String property of this class or 1364 'propname' must be the name of a String property of this class or
1366 None, or a TypeError is raised. The values of the key property on 1365 None, or a TypeError is raised. The values of the key property on
1367 all existing nodes must be unique or a ValueError is raised. If the 1366 all existing nodes must be unique or a ValueError is raised. If the
1368 property doesn't exist, KeyError is raised. 1367 property doesn't exist, KeyError is raised.
1369 ''' 1368 """
1370 prop = self.getprops()[propname] 1369 prop = self.getprops()[propname]
1371 if not isinstance(prop, hyperdb.String): 1370 if not isinstance(prop, hyperdb.String):
1372 raise TypeError, 'key properties must be String' 1371 raise TypeError, 'key properties must be String'
1373 self.key = propname 1372 self.key = propname
1374 1373
1375 def getkey(self): 1374 def getkey(self):
1376 '''Return the name of the key property for this class or None.''' 1375 """Return the name of the key property for this class or None."""
1377 return self.key 1376 return self.key
1378 1377
1379 # TODO: set up a separate index db file for this? profile? 1378 # TODO: set up a separate index db file for this? profile?
1380 def lookup(self, keyvalue): 1379 def lookup(self, keyvalue):
1381 '''Locate a particular node by its key property and return its id. 1380 """Locate a particular node by its key property and return its id.
1382 1381
1383 If this class has no key property, a TypeError is raised. If the 1382 If this class has no key property, a TypeError is raised. If the
1384 'keyvalue' matches one of the values for the key property among 1383 'keyvalue' matches one of the values for the key property among
1385 the nodes in this class, the matching node's id is returned; 1384 the nodes in this class, the matching node's id is returned;
1386 otherwise a KeyError is raised. 1385 otherwise a KeyError is raised.
1387 ''' 1386 """
1388 if not self.key: 1387 if not self.key:
1389 raise TypeError, 'No key property set for class %s'%self.classname 1388 raise TypeError, 'No key property set for class %s'%self.classname
1390 cldb = self.db.getclassdb(self.classname) 1389 cldb = self.db.getclassdb(self.classname)
1391 try: 1390 try:
1392 for nodeid in self.getnodeids(cldb): 1391 for nodeid in self.getnodeids(cldb):
1402 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key, 1401 raise KeyError, 'No key (%s) value "%s" for "%s"'%(self.key,
1403 keyvalue, self.classname) 1402 keyvalue, self.classname)
1404 1403
1405 # change from spec - allows multiple props to match 1404 # change from spec - allows multiple props to match
1406 def find(self, **propspec): 1405 def find(self, **propspec):
1407 '''Get the ids of nodes in this class which link to the given nodes. 1406 """Get the ids of nodes in this class which link to the given nodes.
1408 1407
1409 'propspec' consists of keyword args propname=nodeid or 1408 'propspec' consists of keyword args propname=nodeid or
1410 propname={nodeid:1, } 1409 propname={nodeid:1, }
1411 'propname' must be the name of a property in this class, or a 1410 'propname' must be the name of a property in this class, or a
1412 KeyError is raised. That property must be a Link or 1411 KeyError is raised. That property must be a Link or
1415 Any node in this class whose 'propname' property links to any of 1414 Any node in this class whose 'propname' property links to any of
1416 the nodeids will be returned. Examples:: 1415 the nodeids will be returned. Examples::
1417 1416
1418 db.issue.find(messages='1') 1417 db.issue.find(messages='1')
1419 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) 1418 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1420 ''' 1419 """
1421 propspec = propspec.items() 1420 propspec = propspec.items()
1422 for propname, itemids in propspec: 1421 for propname, itemids in propspec:
1423 # check the prop is OK 1422 # check the prop is OK
1424 prop = self.properties[propname] 1423 prop = self.properties[propname]
1425 if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink): 1424 if not isinstance(prop, hyperdb.Link) and not isinstance(prop, hyperdb.Multilink):
1462 finally: 1461 finally:
1463 cldb.close() 1462 cldb.close()
1464 return l 1463 return l
1465 1464
1466 def stringFind(self, **requirements): 1465 def stringFind(self, **requirements):
1467 '''Locate a particular node by matching a set of its String 1466 """Locate a particular node by matching a set of its String
1468 properties in a caseless search. 1467 properties in a caseless search.
1469 1468
1470 If the property is not a String property, a TypeError is raised. 1469 If the property is not a String property, a TypeError is raised.
1471 1470
1472 The return is a list of the id of all nodes that match. 1471 The return is a list of the id of all nodes that match.
1473 ''' 1472 """
1474 for propname in requirements.keys(): 1473 for propname in requirements.keys():
1475 prop = self.properties[propname] 1474 prop = self.properties[propname]
1476 if not isinstance(prop, hyperdb.String): 1475 if not isinstance(prop, hyperdb.String):
1477 raise TypeError, "'%s' not a String property"%propname 1476 raise TypeError, "'%s' not a String property"%propname
1478 requirements[propname] = requirements[propname].lower() 1477 requirements[propname] = requirements[propname].lower()
1493 finally: 1492 finally:
1494 cldb.close() 1493 cldb.close()
1495 return l 1494 return l
1496 1495
1497 def list(self): 1496 def list(self):
1498 ''' Return a list of the ids of the active nodes in this class. 1497 """ Return a list of the ids of the active nodes in this class.
1499 ''' 1498 """
1500 l = [] 1499 l = []
1501 cn = self.classname 1500 cn = self.classname
1502 cldb = self.db.getclassdb(cn) 1501 cldb = self.db.getclassdb(cn)
1503 try: 1502 try:
1504 for nodeid in self.getnodeids(cldb): 1503 for nodeid in self.getnodeids(cldb):
1510 cldb.close() 1509 cldb.close()
1511 l.sort() 1510 l.sort()
1512 return l 1511 return l
1513 1512
1514 def getnodeids(self, db=None, retired=None): 1513 def getnodeids(self, db=None, retired=None):
1515 ''' Return a list of ALL nodeids 1514 """ Return a list of ALL nodeids
1516 1515
1517 Set retired=None to get all nodes. Otherwise it'll get all the 1516 Set retired=None to get all nodes. Otherwise it'll get all the
1518 retired or non-retired nodes, depending on the flag. 1517 retired or non-retired nodes, depending on the flag.
1519 ''' 1518 """
1520 res = [] 1519 res = []
1521 1520
1522 # start off with the new nodes 1521 # start off with the new nodes
1523 if self.db.newnodes.has_key(self.classname): 1522 if self.db.newnodes.has_key(self.classname):
1524 res += self.db.newnodes[self.classname].keys() 1523 res += self.db.newnodes[self.classname].keys()
1809 if __debug__: 1808 if __debug__:
1810 self.db.stats['filtering'] += (time.time() - start_t) 1809 self.db.stats['filtering'] += (time.time() - start_t)
1811 return matches 1810 return matches
1812 1811
1813 def count(self): 1812 def count(self):
1814 '''Get the number of nodes in this class. 1813 """Get the number of nodes in this class.
1815 1814
1816 If the returned integer is 'numnodes', the ids of all the nodes 1815 If the returned integer is 'numnodes', the ids of all the nodes
1817 in this class run from 1 to numnodes, and numnodes+1 will be the 1816 in this class run from 1 to numnodes, and numnodes+1 will be the
1818 id of the next node to be created in this class. 1817 id of the next node to be created in this class.
1819 ''' 1818 """
1820 return self.db.countnodes(self.classname) 1819 return self.db.countnodes(self.classname)
1821 1820
1822 # Manipulating properties: 1821 # Manipulating properties:
1823 1822
1824 def getprops(self, protected=1): 1823 def getprops(self, protected=1):
1825 '''Return a dictionary mapping property names to property objects. 1824 """Return a dictionary mapping property names to property objects.
1826 If the "protected" flag is true, we include protected properties - 1825 If the "protected" flag is true, we include protected properties -
1827 those which may not be modified. 1826 those which may not be modified.
1828 1827
1829 In addition to the actual properties on the node, these 1828 In addition to the actual properties on the node, these
1830 methods provide the "creation" and "activity" properties. If the 1829 methods provide the "creation" and "activity" properties. If the
1831 "protected" flag is true, we include protected properties - those 1830 "protected" flag is true, we include protected properties - those
1832 which may not be modified. 1831 which may not be modified.
1833 ''' 1832 """
1834 d = self.properties.copy() 1833 d = self.properties.copy()
1835 if protected: 1834 if protected:
1836 d['id'] = hyperdb.String() 1835 d['id'] = hyperdb.String()
1837 d['creation'] = hyperdb.Date() 1836 d['creation'] = hyperdb.Date()
1838 d['activity'] = hyperdb.Date() 1837 d['activity'] = hyperdb.Date()
1839 d['creator'] = hyperdb.Link('user') 1838 d['creator'] = hyperdb.Link('user')
1840 d['actor'] = hyperdb.Link('user') 1839 d['actor'] = hyperdb.Link('user')
1841 return d 1840 return d
1842 1841
1843 def addprop(self, **properties): 1842 def addprop(self, **properties):
1844 '''Add properties to this class. 1843 """Add properties to this class.
1845 1844
1846 The keyword arguments in 'properties' must map names to property 1845 The keyword arguments in 'properties' must map names to property
1847 objects, or a TypeError is raised. None of the keys in 'properties' 1846 objects, or a TypeError is raised. None of the keys in 'properties'
1848 may collide with the names of existing properties, or a ValueError 1847 may collide with the names of existing properties, or a ValueError
1849 is raised before any properties have been added. 1848 is raised before any properties have been added.
1850 ''' 1849 """
1851 for key in properties.keys(): 1850 for key in properties.keys():
1852 if self.properties.has_key(key): 1851 if self.properties.has_key(key):
1853 raise ValueError, key 1852 raise ValueError, key
1854 self.properties.update(properties) 1853 self.properties.update(properties)
1855 1854
1856 def index(self, nodeid): 1855 def index(self, nodeid):
1857 ''' Add (or refresh) the node to search indexes ''' 1856 """ Add (or refresh) the node to search indexes """
1858 # find all the String properties that have indexme 1857 # find all the String properties that have indexme
1859 for prop, propclass in self.getprops().items(): 1858 for prop, propclass in self.getprops().items():
1860 if isinstance(propclass, hyperdb.String) and propclass.indexme: 1859 if isinstance(propclass, hyperdb.String) and propclass.indexme:
1861 # index them under (classname, nodeid, property) 1860 # index them under (classname, nodeid, property)
1862 try: 1861 try:
1868 1867
1869 # 1868 #
1870 # import / export support 1869 # import / export support
1871 # 1870 #
1872 def export_list(self, propnames, nodeid): 1871 def export_list(self, propnames, nodeid):
1873 ''' Export a node - generate a list of CSV-able data in the order 1872 """ Export a node - generate a list of CSV-able data in the order
1874 specified by propnames for the given node. 1873 specified by propnames for the given node.
1875 ''' 1874 """
1876 properties = self.getprops() 1875 properties = self.getprops()
1877 l = [] 1876 l = []
1878 for prop in propnames: 1877 for prop in propnames:
1879 proptype = properties[prop] 1878 proptype = properties[prop]
1880 value = self.get(nodeid, prop) 1879 value = self.get(nodeid, prop)
1893 l.append(repr(self.is_retired(nodeid))) 1892 l.append(repr(self.is_retired(nodeid)))
1894 1893
1895 return l 1894 return l
1896 1895
1897 def import_list(self, propnames, proplist): 1896 def import_list(self, propnames, proplist):
1898 ''' Import a node - all information including "id" is present and 1897 """ Import a node - all information including "id" is present and
1899 should not be sanity checked. Triggers are not triggered. The 1898 should not be sanity checked. Triggers are not triggered. The
1900 journal should be initialised using the "creator" and "created" 1899 journal should be initialised using the "creator" and "created"
1901 information. 1900 information.
1902 1901
1903 Return the nodeid of the node imported. 1902 Return the nodeid of the node imported.
1904 ''' 1903 """
1905 if self.db.journaltag is None: 1904 if self.db.journaltag is None:
1906 raise hyperdb.DatabaseError, _('Database open read-only') 1905 raise hyperdb.DatabaseError, _('Database open read-only')
1907 properties = self.getprops() 1906 properties = self.getprops()
1908 1907
1909 # make the new node's property map 1908 # make the new node's property map
1947 # add the node and journal 1946 # add the node and journal
1948 self.db.addnode(self.classname, newid, d) 1947 self.db.addnode(self.classname, newid, d)
1949 return newid 1948 return newid
1950 1949
1951 def export_journals(self): 1950 def export_journals(self):
1952 '''Export a class's journal - generate a list of lists of 1951 """Export a class's journal - generate a list of lists of
1953 CSV-able data: 1952 CSV-able data:
1954 1953
1955 nodeid, date, user, action, params 1954 nodeid, date, user, action, params
1956 1955
1957 No heading here - the columns are fixed. 1956 No heading here - the columns are fixed.
1958 ''' 1957 """
1959 properties = self.getprops() 1958 properties = self.getprops()
1960 r = [] 1959 r = []
1961 for nodeid in self.getnodeids(): 1960 for nodeid in self.getnodeids():
1962 for nodeid, date, user, action, params in self.history(nodeid): 1961 for nodeid, date, user, action, params in self.history(nodeid):
1963 date = date.get_tuple() 1962 date = date.get_tuple()
1987 l = [nodeid, date, user, action, params] 1986 l = [nodeid, date, user, action, params]
1988 r.append(map(repr, l)) 1987 r.append(map(repr, l))
1989 return r 1988 return r
1990 1989
1991 def import_journals(self, entries): 1990 def import_journals(self, entries):
1992 '''Import a class's journal. 1991 """Import a class's journal.
1993 1992
1994 Uses setjournal() to set the journal for each item.''' 1993 Uses setjournal() to set the journal for each item."""
1995 properties = self.getprops() 1994 properties = self.getprops()
1996 d = {} 1995 d = {}
1997 for l in entries: 1996 for l in entries:
1998 l = map(eval, l) 1997 l = map(eval, l)
1999 nodeid, jdate, user, action, params = l 1998 nodeid, jdate, user, action, params = l
2019 2018
2020 for nodeid, l in d.items(): 2019 for nodeid, l in d.items():
2021 self.db.setjournal(self.classname, nodeid, l) 2020 self.db.setjournal(self.classname, nodeid, l)
2022 2021
2023 class FileClass(hyperdb.FileClass, Class): 2022 class FileClass(hyperdb.FileClass, Class):
2024 '''This class defines a large chunk of data. To support this, it has a 2023 """This class defines a large chunk of data. To support this, it has a
2025 mandatory String property "content" which is typically saved off 2024 mandatory String property "content" which is typically saved off
2026 externally to the hyperdb. 2025 externally to the hyperdb.
2027 2026
2028 The default MIME type of this data is defined by the 2027 The default MIME type of this data is defined by the
2029 "default_mime_type" class attribute, which may be overridden by each 2028 "default_mime_type" class attribute, which may be overridden by each
2030 node if the class defines a "type" String property. 2029 node if the class defines a "type" String property.
2031 ''' 2030 """
2032 def __init__(self, db, classname, **properties): 2031 def __init__(self, db, classname, **properties):
2033 '''The newly-created class automatically includes the "content" 2032 """The newly-created class automatically includes the "content"
2034 and "type" properties. 2033 and "type" properties.
2035 ''' 2034 """
2036 if not properties.has_key('content'): 2035 if not properties.has_key('content'):
2037 properties['content'] = hyperdb.String(indexme='yes') 2036 properties['content'] = hyperdb.String(indexme='yes')
2038 if not properties.has_key('type'): 2037 if not properties.has_key('type'):
2039 properties['type'] = hyperdb.String() 2038 properties['type'] = hyperdb.String()
2040 Class.__init__(self, db, classname, **properties) 2039 Class.__init__(self, db, classname, **properties)
2041 2040
2042 def create(self, **propvalues): 2041 def create(self, **propvalues):
2043 ''' Snarf the "content" propvalue and store in a file 2042 """ Snarf the "content" propvalue and store in a file
2044 ''' 2043 """
2045 # we need to fire the auditors now, or the content property won't 2044 # we need to fire the auditors now, or the content property won't
2046 # be in propvalues for the auditors to play with 2045 # be in propvalues for the auditors to play with
2047 self.fireAuditors('create', None, propvalues) 2046 self.fireAuditors('create', None, propvalues)
2048 2047
2049 # now remove the content property so it's not stored in the db 2048 # now remove the content property so it's not stored in the db
2063 self.fireReactors('create', newid, None) 2062 self.fireReactors('create', newid, None)
2064 2063
2065 return newid 2064 return newid
2066 2065
2067 def get(self, nodeid, propname, default=_marker, cache=1): 2066 def get(self, nodeid, propname, default=_marker, cache=1):
2068 ''' Trap the content propname and get it from the file 2067 """ Trap the content propname and get it from the file
2069 2068
2070 'cache' exists for backwards compatibility, and is not used. 2069 'cache' exists for backwards compatibility, and is not used.
2071 ''' 2070 """
2072 poss_msg = 'Possibly an access right configuration problem.' 2071 poss_msg = 'Possibly an access right configuration problem.'
2073 if propname == 'content': 2072 if propname == 'content':
2074 try: 2073 try:
2075 return self.db.getfile(self.classname, nodeid, None) 2074 return self.db.getfile(self.classname, nodeid, None)
2076 except IOError, (strerror): 2075 except IOError, (strerror):
2081 return Class.get(self, nodeid, propname, default) 2080 return Class.get(self, nodeid, propname, default)
2082 else: 2081 else:
2083 return Class.get(self, nodeid, propname) 2082 return Class.get(self, nodeid, propname)
2084 2083
2085 def set(self, itemid, **propvalues): 2084 def set(self, itemid, **propvalues):
2086 ''' Snarf the "content" propvalue and update it in a file 2085 """ Snarf the "content" propvalue and update it in a file
2087 ''' 2086 """
2088 self.fireAuditors('set', itemid, propvalues) 2087 self.fireAuditors('set', itemid, propvalues)
2089 2088
2090 # create the oldvalues dict - fill in any missing values 2089 # create the oldvalues dict - fill in any missing values
2091 oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid)) 2090 oldvalues = copy.deepcopy(self.db.getnode(self.classname, itemid))
2092 for name,prop in self.getprops(protected=0).items(): 2091 for name,prop in self.getprops(protected=0).items():
2119 # fire reactors 2118 # fire reactors
2120 self.fireReactors('set', itemid, oldvalues) 2119 self.fireReactors('set', itemid, oldvalues)
2121 return propvalues 2120 return propvalues
2122 2121
2123 def index(self, nodeid): 2122 def index(self, nodeid):
2124 ''' Add (or refresh) the node to search indexes. 2123 """ Add (or refresh) the node to search indexes.
2125 2124
2126 Use the content-type property for the content property. 2125 Use the content-type property for the content property.
2127 ''' 2126 """
2128 # find all the String properties that have indexme 2127 # find all the String properties that have indexme
2129 for prop, propclass in self.getprops().items(): 2128 for prop, propclass in self.getprops().items():
2130 if prop == 'content' and propclass.indexme: 2129 if prop == 'content' and propclass.indexme:
2131 mime_type = self.get(nodeid, 'type', self.default_mime_type) 2130 mime_type = self.get(nodeid, 'type', self.default_mime_type)
2132 self.db.indexer.add_text((self.classname, nodeid, 'content'), 2131 self.db.indexer.add_text((self.classname, nodeid, 'content'),
2142 2141
2143 # deviation from spec - was called ItemClass 2142 # deviation from spec - was called ItemClass
2144 class IssueClass(Class, roundupdb.IssueClass): 2143 class IssueClass(Class, roundupdb.IssueClass):
2145 # Overridden methods: 2144 # Overridden methods:
2146 def __init__(self, db, classname, **properties): 2145 def __init__(self, db, classname, **properties):
2147 '''The newly-created class automatically includes the "messages", 2146 """The newly-created class automatically includes the "messages",
2148 "files", "nosy", and "superseder" properties. If the 'properties' 2147 "files", "nosy", and "superseder" properties. If the 'properties'
2149 dictionary attempts to specify any of these properties or a 2148 dictionary attempts to specify any of these properties or a
2150 "creation" or "activity" property, a ValueError is raised. 2149 "creation" or "activity" property, a ValueError is raised.
2151 ''' 2150 """
2152 if not properties.has_key('title'): 2151 if not properties.has_key('title'):
2153 properties['title'] = hyperdb.String(indexme='yes') 2152 properties['title'] = hyperdb.String(indexme='yes')
2154 if not properties.has_key('messages'): 2153 if not properties.has_key('messages'):
2155 properties['messages'] = hyperdb.Multilink("msg") 2154 properties['messages'] = hyperdb.Multilink("msg")
2156 if not properties.has_key('files'): 2155 if not properties.has_key('files'):

Roundup Issue Tracker: http://roundup-tracker.org/