Code

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