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: hyperdb.py,v 1.70 2002-06-27 12:06:20 gmcm Exp $
20 __doc__ = """
21 Hyperdatabase implementation, especially field types.
22 """
24 # standard python modules
25 import sys, re, string, weakref, os, time
27 # roundup modules
28 import date, password
30 # configure up the DEBUG and TRACE captures
31 class Sink:
32 def write(self, content):
33 pass
34 DEBUG = os.environ.get('HYPERDBDEBUG', '')
35 if DEBUG and __debug__:
36 if DEBUG == 'stdout':
37 DEBUG = sys.stdout
38 else:
39 DEBUG = open(DEBUG, 'a')
40 else:
41 DEBUG = Sink()
42 TRACE = os.environ.get('HYPERDBTRACE', '')
43 if TRACE and __debug__:
44 if TRACE == 'stdout':
45 TRACE = sys.stdout
46 else:
47 TRACE = open(TRACE, 'w')
48 else:
49 TRACE = Sink()
50 def traceMark():
51 print >>TRACE, '**MARK', time.ctime()
52 del Sink
54 #
55 # Types
56 #
57 class String:
58 """An object designating a String property."""
59 def __repr__(self):
60 ' more useful for dumps '
61 return '<%s>'%self.__class__
63 class Password:
64 """An object designating a Password property."""
65 def __repr__(self):
66 ' more useful for dumps '
67 return '<%s>'%self.__class__
69 class Date:
70 """An object designating a Date property."""
71 def __repr__(self):
72 ' more useful for dumps '
73 return '<%s>'%self.__class__
75 class Interval:
76 """An object designating an Interval property."""
77 def __repr__(self):
78 ' more useful for dumps '
79 return '<%s>'%self.__class__
81 class Link:
82 """An object designating a Link property that links to a
83 node in a specified class."""
84 def __init__(self, classname, do_journal='no'):
85 ''' Default is to not journal link and unlink events
86 '''
87 self.classname = classname
88 self.do_journal = do_journal == 'yes'
89 def __repr__(self):
90 ' more useful for dumps '
91 return '<%s to "%s">'%(self.__class__, self.classname)
93 class Multilink:
94 """An object designating a Multilink property that links
95 to nodes in a specified class.
97 "classname" indicates the class to link to
99 "do_journal" indicates whether the linked-to nodes should have
100 'link' and 'unlink' events placed in their journal
101 """
102 def __init__(self, classname, do_journal='no'):
103 ''' Default is to not journal link and unlink events
104 '''
105 self.classname = classname
106 self.do_journal = do_journal == 'yes'
107 def __repr__(self):
108 ' more useful for dumps '
109 return '<%s to "%s">'%(self.__class__, self.classname)
111 class DatabaseError(ValueError):
112 '''Error to be raised when there is some problem in the database code
113 '''
114 pass
117 #
118 # the base Database class
119 #
120 class Database:
121 '''A database for storing records containing flexible data types.
123 This class defines a hyperdatabase storage layer, which the Classes use to
124 store their data.
127 Transactions
128 ------------
129 The Database should support transactions through the commit() and
130 rollback() methods. All other Database methods should be transaction-aware,
131 using data from the current transaction before looking up the database.
133 An implementation must provide an override for the get() method so that the
134 in-database value is returned in preference to the in-transaction value.
135 This is necessary to determine if any values have changed during a
136 transaction.
138 '''
140 # flag to set on retired entries
141 RETIRED_FLAG = '__hyperdb_retired'
143 # XXX deviates from spec: storagelocator is obtained from the config
144 def __init__(self, config, journaltag=None):
145 """Open a hyperdatabase given a specifier to some storage.
147 The 'storagelocator' is obtained from config.DATABASE.
148 The meaning of 'storagelocator' depends on the particular
149 implementation of the hyperdatabase. It could be a file name,
150 a directory path, a socket descriptor for a connection to a
151 database over the network, etc.
153 The 'journaltag' is a token that will be attached to the journal
154 entries for any edits done on the database. If 'journaltag' is
155 None, the database is opened in read-only mode: the Class.create(),
156 Class.set(), and Class.retire() methods are disabled.
157 """
158 raise NotImplementedError
160 def __getattr__(self, classname):
161 """A convenient way of calling self.getclass(classname)."""
162 raise NotImplementedError
164 def addclass(self, cl):
165 '''Add a Class to the hyperdatabase.
166 '''
167 raise NotImplementedError
169 def getclasses(self):
170 """Return a list of the names of all existing classes."""
171 raise NotImplementedError
173 def getclass(self, classname):
174 """Get the Class object representing a particular class.
176 If 'classname' is not a valid class name, a KeyError is raised.
177 """
178 raise NotImplementedError
180 def clear(self):
181 '''Delete all database contents.
182 '''
183 raise NotImplementedError
185 def getclassdb(self, classname, mode='r'):
186 '''Obtain a connection to the class db that will be used for
187 multiple actions.
188 '''
189 raise NotImplementedError
191 def addnode(self, classname, nodeid, node):
192 '''Add the specified node to its class's db.
193 '''
194 raise NotImplementedError
196 def serialise(self, classname, node):
197 '''Copy the node contents, converting non-marshallable data into
198 marshallable data.
199 '''
200 if __debug__:
201 print >>DEBUG, 'serialise', classname, node
202 properties = self.getclass(classname).getprops()
203 d = {}
204 for k, v in node.items():
205 # if the property doesn't exist, or is the "retired" flag then
206 # it won't be in the properties dict
207 if not properties.has_key(k):
208 d[k] = v
209 continue
211 # get the property spec
212 prop = properties[k]
214 if isinstance(prop, Password):
215 d[k] = str(v)
216 elif isinstance(prop, Date) and v is not None:
217 d[k] = v.get_tuple()
218 elif isinstance(prop, Interval) and v is not None:
219 d[k] = v.get_tuple()
220 else:
221 d[k] = v
222 return d
224 def setnode(self, classname, nodeid, node):
225 '''Change the specified node.
226 '''
227 raise NotImplementedError
229 def unserialise(self, classname, node):
230 '''Decode the marshalled node data
231 '''
232 if __debug__:
233 print >>DEBUG, 'unserialise', classname, node
234 properties = self.getclass(classname).getprops()
235 d = {}
236 for k, v in node.items():
237 # if the property doesn't exist, or is the "retired" flag then
238 # it won't be in the properties dict
239 if not properties.has_key(k):
240 d[k] = v
241 continue
243 # get the property spec
244 prop = properties[k]
246 if isinstance(prop, Date) and v is not None:
247 d[k] = date.Date(v)
248 elif isinstance(prop, Interval) and v is not None:
249 d[k] = date.Interval(v)
250 elif isinstance(prop, Password):
251 p = password.Password()
252 p.unpack(v)
253 d[k] = p
254 else:
255 d[k] = v
256 return d
258 def getnode(self, classname, nodeid, db=None, cache=1):
259 '''Get a node from the database.
260 '''
261 raise NotImplementedError
263 def hasnode(self, classname, nodeid, db=None):
264 '''Determine if the database has a given node.
265 '''
266 raise NotImplementedError
268 def countnodes(self, classname, db=None):
269 '''Count the number of nodes that exist for a particular Class.
270 '''
271 raise NotImplementedError
273 def getnodeids(self, classname, db=None):
274 '''Retrieve all the ids of the nodes for a particular Class.
275 '''
276 raise NotImplementedError
278 def storefile(self, classname, nodeid, property, content):
279 '''Store the content of the file in the database.
281 The property may be None, in which case the filename does not
282 indicate which property is being saved.
283 '''
284 raise NotImplementedError
286 def getfile(self, classname, nodeid, property):
287 '''Store the content of the file in the database.
288 '''
289 raise NotImplementedError
291 def addjournal(self, classname, nodeid, action, params):
292 ''' Journal the Action
293 'action' may be:
295 'create' or 'set' -- 'params' is a dictionary of property values
296 'link' or 'unlink' -- 'params' is (classname, nodeid, propname)
297 'retire' -- 'params' is None
298 '''
299 raise NotImplementedError
301 def getjournal(self, classname, nodeid):
302 ''' get the journal for id
303 '''
304 raise NotImplementedError
306 def pack(self, pack_before):
307 ''' pack the database
308 '''
309 raise NotImplementedError
311 def commit(self):
312 ''' Commit the current transactions.
314 Save all data changed since the database was opened or since the
315 last commit() or rollback().
316 '''
317 raise NotImplementedError
319 def rollback(self):
320 ''' Reverse all actions from the current transaction.
322 Undo all the changes made since the database was opened or the last
323 commit() or rollback() was performed.
324 '''
325 raise NotImplementedError
327 _marker = []
328 #
329 # The base Class class
330 #
331 class Class:
332 """The handle to a particular class of nodes in a hyperdatabase."""
334 def __init__(self, db, classname, **properties):
335 """Create a new class with a given name and property specification.
337 'classname' must not collide with the name of an existing class,
338 or a ValueError is raised. The keyword arguments in 'properties'
339 must map names to property objects, or a TypeError is raised.
340 """
341 self.classname = classname
342 self.properties = properties
343 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
344 self.key = ''
346 # do the db-related init stuff
347 db.addclass(self)
349 def __repr__(self):
350 '''Slightly more useful representation
351 '''
352 return '<hypderdb.Class "%s">'%self.classname
354 # Editing nodes:
356 def create(self, **propvalues):
357 """Create a new node of this class and return its id.
359 The keyword arguments in 'propvalues' map property names to values.
361 The values of arguments must be acceptable for the types of their
362 corresponding properties or a TypeError is raised.
364 If this class has a key property, it must be present and its value
365 must not collide with other key strings or a ValueError is raised.
367 Any other properties on this class that are missing from the
368 'propvalues' dictionary are set to None.
370 If an id in a link or multilink property does not refer to a valid
371 node, an IndexError is raised.
372 """
373 if propvalues.has_key('id'):
374 raise KeyError, '"id" is reserved'
376 if self.db.journaltag is None:
377 raise DatabaseError, 'Database open read-only'
379 # new node's id
380 newid = self.db.newid(self.classname)
382 # validate propvalues
383 num_re = re.compile('^\d+$')
384 for key, value in propvalues.items():
385 if key == self.key:
386 try:
387 self.lookup(value)
388 except KeyError:
389 pass
390 else:
391 raise ValueError, 'node with key "%s" exists'%value
393 # try to handle this property
394 try:
395 prop = self.properties[key]
396 except KeyError:
397 raise KeyError, '"%s" has no property "%s"'%(self.classname,
398 key)
400 if isinstance(prop, Link):
401 if type(value) != type(''):
402 raise ValueError, 'link value must be String'
403 link_class = self.properties[key].classname
404 # if it isn't a number, it's a key
405 if not num_re.match(value):
406 try:
407 value = self.db.classes[link_class].lookup(value)
408 except (TypeError, KeyError):
409 raise IndexError, 'new property "%s": %s not a %s'%(
410 key, value, link_class)
411 elif not self.db.hasnode(link_class, value):
412 raise IndexError, '%s has no node %s'%(link_class, value)
414 # save off the value
415 propvalues[key] = value
417 # register the link with the newly linked node
418 if self.properties[key].do_journal:
419 self.db.addjournal(link_class, value, 'link',
420 (self.classname, newid, key))
422 elif isinstance(prop, Multilink):
423 if type(value) != type([]):
424 raise TypeError, 'new property "%s" not a list of ids'%key
426 # clean up and validate the list of links
427 link_class = self.properties[key].classname
428 l = []
429 for entry in value:
430 if type(entry) != type(''):
431 raise ValueError, '"%s" link value (%s) must be String' % (key, value)
432 # if it isn't a number, it's a key
433 if not num_re.match(entry):
434 try:
435 entry = self.db.classes[link_class].lookup(entry)
436 except (TypeError, KeyError):
437 raise IndexError, 'new property "%s": %s not a %s'%(
438 key, entry, self.properties[key].classname)
439 l.append(entry)
440 value = l
441 propvalues[key] = value
443 # handle additions
444 for id in value:
445 if not self.db.hasnode(link_class, id):
446 raise IndexError, '%s has no node %s'%(link_class, id)
447 # register the link with the newly linked node
448 if self.properties[key].do_journal:
449 self.db.addjournal(link_class, id, 'link',
450 (self.classname, newid, key))
452 elif isinstance(prop, String):
453 if type(value) != type(''):
454 raise TypeError, 'new property "%s" not a string'%key
456 elif isinstance(prop, Password):
457 if not isinstance(value, password.Password):
458 raise TypeError, 'new property "%s" not a Password'%key
460 elif isinstance(prop, Date):
461 if value is not None and not isinstance(value, date.Date):
462 raise TypeError, 'new property "%s" not a Date'%key
464 elif isinstance(prop, Interval):
465 if value is not None and not isinstance(value, date.Interval):
466 raise TypeError, 'new property "%s" not an Interval'%key
468 # make sure there's data where there needs to be
469 for key, prop in self.properties.items():
470 if propvalues.has_key(key):
471 continue
472 if key == self.key:
473 raise ValueError, 'key property "%s" is required'%key
474 if isinstance(prop, Multilink):
475 propvalues[key] = []
476 else:
477 # TODO: None isn't right here, I think...
478 propvalues[key] = None
480 # done
481 self.db.addnode(self.classname, newid, propvalues)
482 self.db.addjournal(self.classname, newid, 'create', propvalues)
483 return newid
485 def get(self, nodeid, propname, default=_marker, cache=1):
486 """Get the value of a property on an existing node of this class.
488 'nodeid' must be the id of an existing node of this class or an
489 IndexError is raised. 'propname' must be the name of a property
490 of this class or a KeyError is raised.
492 'cache' indicates whether the transaction cache should be queried
493 for the node. If the node has been modified and you need to
494 determine what its values prior to modification are, you need to
495 set cache=0.
496 """
497 if propname == 'id':
498 return nodeid
500 # get the property (raises KeyErorr if invalid)
501 prop = self.properties[propname]
503 # get the node's dict
504 d = self.db.getnode(self.classname, nodeid, cache=cache)
506 if not d.has_key(propname):
507 if default is _marker:
508 if isinstance(prop, Multilink):
509 return []
510 else:
511 # TODO: None isn't right here, I think...
512 return None
513 else:
514 return default
516 return d[propname]
518 # XXX not in spec
519 def getnode(self, nodeid, cache=1):
520 ''' Return a convenience wrapper for the node.
522 'nodeid' must be the id of an existing node of this class or an
523 IndexError is raised.
525 'cache' indicates whether the transaction cache should be queried
526 for the node. If the node has been modified and you need to
527 determine what its values prior to modification are, you need to
528 set cache=0.
529 '''
530 return Node(self, nodeid, cache=cache)
532 def set(self, nodeid, **propvalues):
533 """Modify a property on an existing node of this class.
535 'nodeid' must be the id of an existing node of this class or an
536 IndexError is raised.
538 Each key in 'propvalues' must be the name of a property of this
539 class or a KeyError is raised.
541 All values in 'propvalues' must be acceptable types for their
542 corresponding properties or a TypeError is raised.
544 If the value of the key property is set, it must not collide with
545 other key strings or a ValueError is raised.
547 If the value of a Link or Multilink property contains an invalid
548 node id, a ValueError is raised.
549 """
550 if not propvalues:
551 return
553 if propvalues.has_key('id'):
554 raise KeyError, '"id" is reserved'
556 if self.db.journaltag is None:
557 raise DatabaseError, 'Database open read-only'
559 node = self.db.getnode(self.classname, nodeid)
560 if node.has_key(self.db.RETIRED_FLAG):
561 raise IndexError
562 num_re = re.compile('^\d+$')
563 for key, value in propvalues.items():
564 # check to make sure we're not duplicating an existing key
565 if key == self.key and node[key] != value:
566 try:
567 self.lookup(value)
568 except KeyError:
569 pass
570 else:
571 raise ValueError, 'node with key "%s" exists'%value
573 # this will raise the KeyError if the property isn't valid
574 # ... we don't use getprops() here because we only care about
575 # the writeable properties.
576 prop = self.properties[key]
578 # if the value's the same as the existing value, no sense in
579 # doing anything
580 if node.has_key(key) and value == node[key]:
581 del propvalues[key]
582 continue
584 # do stuff based on the prop type
585 if isinstance(prop, Link):
586 link_class = self.properties[key].classname
587 # if it isn't a number, it's a key
588 if type(value) != type(''):
589 raise ValueError, 'link value must be String'
590 if not num_re.match(value):
591 try:
592 value = self.db.classes[link_class].lookup(value)
593 except (TypeError, KeyError):
594 raise IndexError, 'new property "%s": %s not a %s'%(
595 key, value, self.properties[key].classname)
597 if not self.db.hasnode(link_class, value):
598 raise IndexError, '%s has no node %s'%(link_class, value)
600 if self.properties[key].do_journal:
601 # register the unlink with the old linked node
602 if node[key] is not None:
603 self.db.addjournal(link_class, node[key], 'unlink',
604 (self.classname, nodeid, key))
606 # register the link with the newly linked node
607 if value is not None:
608 self.db.addjournal(link_class, value, 'link',
609 (self.classname, nodeid, key))
611 elif isinstance(prop, Multilink):
612 if type(value) != type([]):
613 raise TypeError, 'new property "%s" not a list of ids'%key
614 link_class = self.properties[key].classname
615 l = []
616 for entry in value:
617 # if it isn't a number, it's a key
618 if type(entry) != type(''):
619 raise ValueError, 'new property "%s" link value ' \
620 'must be a string'%key
621 if not num_re.match(entry):
622 try:
623 entry = self.db.classes[link_class].lookup(entry)
624 except (TypeError, KeyError):
625 raise IndexError, 'new property "%s": %s not a %s'%(
626 key, entry, self.properties[key].classname)
627 l.append(entry)
628 value = l
629 propvalues[key] = value
631 # handle removals
632 if node.has_key(key):
633 l = node[key]
634 else:
635 l = []
636 for id in l[:]:
637 if id in value:
638 continue
639 # register the unlink with the old linked node
640 if self.properties[key].do_journal:
641 self.db.addjournal(link_class, id, 'unlink',
642 (self.classname, nodeid, key))
643 l.remove(id)
645 # handle additions
646 for id in value:
647 if not self.db.hasnode(link_class, id):
648 raise IndexError, '%s has no node %s'%(
649 link_class, id)
650 if id in l:
651 continue
652 # register the link with the newly linked node
653 if self.properties[key].do_journal:
654 self.db.addjournal(link_class, id, 'link',
655 (self.classname, nodeid, key))
656 l.append(id)
658 elif isinstance(prop, String):
659 if value is not None and type(value) != type(''):
660 raise TypeError, 'new property "%s" not a string'%key
662 elif isinstance(prop, Password):
663 if not isinstance(value, password.Password):
664 raise TypeError, 'new property "%s" not a Password'% key
665 propvalues[key] = value
667 elif value is not None and isinstance(prop, Date):
668 if not isinstance(value, date.Date):
669 raise TypeError, 'new property "%s" not a Date'% key
670 propvalues[key] = value
672 elif value is not None and isinstance(prop, Interval):
673 if not isinstance(value, date.Interval):
674 raise TypeError, 'new property "%s" not an Interval'% key
675 propvalues[key] = value
677 node[key] = value
679 # nothing to do?
680 if not propvalues:
681 return
683 # do the set, and journal it
684 self.db.setnode(self.classname, nodeid, node)
685 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
687 def retire(self, nodeid):
688 """Retire a node.
690 The properties on the node remain available from the get() method,
691 and the node's id is never reused.
693 Retired nodes are not returned by the find(), list(), or lookup()
694 methods, and other nodes may reuse the values of their key properties.
695 """
696 if self.db.journaltag is None:
697 raise DatabaseError, 'Database open read-only'
698 node = self.db.getnode(self.classname, nodeid)
699 node[self.db.RETIRED_FLAG] = 1
700 self.db.setnode(self.classname, nodeid, node)
701 self.db.addjournal(self.classname, nodeid, 'retired', None)
703 def history(self, nodeid):
704 """Retrieve the journal of edits on a particular node.
706 'nodeid' must be the id of an existing node of this class or an
707 IndexError is raised.
709 The returned list contains tuples of the form
711 (date, tag, action, params)
713 'date' is a Timestamp object specifying the time of the change and
714 'tag' is the journaltag specified when the database was opened.
715 """
716 return self.db.getjournal(self.classname, nodeid)
718 # Locating nodes:
719 def hasnode(self, nodeid):
720 '''Determine if the given nodeid actually exists
721 '''
722 return self.db.hasnode(self.classname, nodeid)
724 def setkey(self, propname):
725 """Select a String property of this class to be the key property.
727 'propname' must be the name of a String property of this class or
728 None, or a TypeError is raised. The values of the key property on
729 all existing nodes must be unique or a ValueError is raised.
730 """
731 # TODO: validate that the property is a String!
732 self.key = propname
734 def getkey(self):
735 """Return the name of the key property for this class or None."""
736 return self.key
738 def labelprop(self, default_to_id=0):
739 ''' Return the property name for a label for the given node.
741 This method attempts to generate a consistent label for the node.
742 It tries the following in order:
743 1. key property
744 2. "name" property
745 3. "title" property
746 4. first property from the sorted property name list
747 '''
748 k = self.getkey()
749 if k:
750 return k
751 props = self.getprops()
752 if props.has_key('name'):
753 return 'name'
754 elif props.has_key('title'):
755 return 'title'
756 if default_to_id:
757 return 'id'
758 props = props.keys()
759 props.sort()
760 return props[0]
762 # TODO: set up a separate index db file for this? profile?
763 def lookup(self, keyvalue):
764 """Locate a particular node by its key property and return its id.
766 If this class has no key property, a TypeError is raised. If the
767 'keyvalue' matches one of the values for the key property among
768 the nodes in this class, the matching node's id is returned;
769 otherwise a KeyError is raised.
770 """
771 cldb = self.db.getclassdb(self.classname)
772 for nodeid in self.db.getnodeids(self.classname, cldb):
773 node = self.db.getnode(self.classname, nodeid, cldb)
774 if node.has_key(self.db.RETIRED_FLAG):
775 continue
776 if node[self.key] == keyvalue:
777 return nodeid
778 raise KeyError, keyvalue
780 # XXX: change from spec - allows multiple props to match
781 def find(self, **propspec):
782 """Get the ids of nodes in this class which link to a given node.
784 'propspec' consists of keyword args propname=nodeid
785 'propname' must be the name of a property in this class, or a
786 KeyError is raised. That property must be a Link or Multilink
787 property, or a TypeError is raised.
789 'nodeid' must be the id of an existing node in the class linked
790 to by the given property, or an IndexError is raised.
791 """
792 propspec = propspec.items()
793 for propname, nodeid in propspec:
794 # check the prop is OK
795 prop = self.properties[propname]
796 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
797 raise TypeError, "'%s' not a Link/Multilink property"%propname
798 if not self.db.hasnode(prop.classname, nodeid):
799 raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
801 # ok, now do the find
802 cldb = self.db.getclassdb(self.classname)
803 l = []
804 for id in self.db.getnodeids(self.classname, db=cldb):
805 node = self.db.getnode(self.classname, id, db=cldb)
806 if node.has_key(self.db.RETIRED_FLAG):
807 continue
808 for propname, nodeid in propspec:
809 # can't test if the node doesn't have this property
810 if not node.has_key(propname):
811 continue
812 prop = self.properties[propname]
813 property = node[propname]
814 if isinstance(prop, Link) and nodeid == property:
815 l.append(id)
816 elif isinstance(prop, Multilink) and nodeid in property:
817 l.append(id)
818 return l
820 def stringFind(self, **requirements):
821 """Locate a particular node by matching a set of its String
822 properties in a caseless search.
824 If the property is not a String property, a TypeError is raised.
826 The return is a list of the id of all nodes that match.
827 """
828 for propname in requirements.keys():
829 prop = self.properties[propname]
830 if isinstance(not prop, String):
831 raise TypeError, "'%s' not a String property"%propname
832 requirements[propname] = requirements[propname].lower()
833 l = []
834 cldb = self.db.getclassdb(self.classname)
835 for nodeid in self.db.getnodeids(self.classname, cldb):
836 node = self.db.getnode(self.classname, nodeid, cldb)
837 if node.has_key(self.db.RETIRED_FLAG):
838 continue
839 for key, value in requirements.items():
840 if node[key] and node[key].lower() != value:
841 break
842 else:
843 l.append(nodeid)
844 return l
846 def list(self):
847 """Return a list of the ids of the active nodes in this class."""
848 l = []
849 cn = self.classname
850 cldb = self.db.getclassdb(cn)
851 for nodeid in self.db.getnodeids(cn, cldb):
852 node = self.db.getnode(cn, nodeid, cldb)
853 if node.has_key(self.db.RETIRED_FLAG):
854 continue
855 l.append(nodeid)
856 l.sort()
857 return l
859 # XXX not in spec
860 def filter(self, search_matches, filterspec, sort, group,
861 num_re = re.compile('^\d+$')):
862 ''' Return a list of the ids of the active nodes in this class that
863 match the 'filter' spec, sorted by the group spec and then the
864 sort spec
865 '''
866 cn = self.classname
868 # optimise filterspec
869 l = []
870 props = self.getprops()
871 for k, v in filterspec.items():
872 propclass = props[k]
873 if isinstance(propclass, Link):
874 if type(v) is not type([]):
875 v = [v]
876 # replace key values with node ids
877 u = []
878 link_class = self.db.classes[propclass.classname]
879 for entry in v:
880 if entry == '-1': entry = None
881 elif not num_re.match(entry):
882 try:
883 entry = link_class.lookup(entry)
884 except (TypeError,KeyError):
885 raise ValueError, 'property "%s": %s not a %s'%(
886 k, entry, self.properties[k].classname)
887 u.append(entry)
889 l.append((0, k, u))
890 elif isinstance(propclass, Multilink):
891 if type(v) is not type([]):
892 v = [v]
893 # replace key values with node ids
894 u = []
895 link_class = self.db.classes[propclass.classname]
896 for entry in v:
897 if not num_re.match(entry):
898 try:
899 entry = link_class.lookup(entry)
900 except (TypeError,KeyError):
901 raise ValueError, 'new property "%s": %s not a %s'%(
902 k, entry, self.properties[k].classname)
903 u.append(entry)
904 l.append((1, k, u))
905 elif isinstance(propclass, String):
906 # simple glob searching
907 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
908 v = v.replace('?', '.')
909 v = v.replace('*', '.*?')
910 l.append((2, k, re.compile(v, re.I)))
911 else:
912 l.append((6, k, v))
913 filterspec = l
915 # now, find all the nodes that are active and pass filtering
916 l = []
917 cldb = self.db.getclassdb(cn)
918 for nodeid in self.db.getnodeids(cn, cldb):
919 node = self.db.getnode(cn, nodeid, cldb)
920 if node.has_key(self.db.RETIRED_FLAG):
921 continue
922 # apply filter
923 for t, k, v in filterspec:
924 # this node doesn't have this property, so reject it
925 if not node.has_key(k): break
927 if t == 0 and node[k] not in v:
928 # link - if this node'd property doesn't appear in the
929 # filterspec's nodeid list, skip it
930 break
931 elif t == 1:
932 # multilink - if any of the nodeids required by the
933 # filterspec aren't in this node's property, then skip
934 # it
935 for value in v:
936 if value not in node[k]:
937 break
938 else:
939 continue
940 break
941 elif t == 2 and (node[k] is None or not v.search(node[k])):
942 # RE search
943 break
944 elif t == 6 and node[k] != v:
945 # straight value comparison for the other types
946 break
947 else:
948 l.append((nodeid, node))
949 l.sort()
951 # filter based on full text search
952 if search_matches is not None:
953 k = []
954 l_debug = []
955 for v in l:
956 l_debug.append(v[0])
957 if search_matches.has_key(v[0]):
958 k.append(v)
959 l = k
961 # optimise sort
962 m = []
963 for entry in sort:
964 if entry[0] != '-':
965 m.append(('+', entry))
966 else:
967 m.append((entry[0], entry[1:]))
968 sort = m
970 # optimise group
971 m = []
972 for entry in group:
973 if entry[0] != '-':
974 m.append(('+', entry))
975 else:
976 m.append((entry[0], entry[1:]))
977 group = m
978 # now, sort the result
979 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
980 db = self.db, cl=self):
981 a_id, an = a
982 b_id, bn = b
983 # sort by group and then sort
984 for list in group, sort:
985 for dir, prop in list:
986 # sorting is class-specific
987 propclass = properties[prop]
989 # handle the properties that might be "faked"
990 # also, handle possible missing properties
991 try:
992 if not an.has_key(prop):
993 an[prop] = cl.get(a_id, prop)
994 av = an[prop]
995 except KeyError:
996 # the node doesn't have a value for this property
997 if isinstance(propclass, Multilink): av = []
998 else: av = ''
999 try:
1000 if not bn.has_key(prop):
1001 bn[prop] = cl.get(b_id, prop)
1002 bv = bn[prop]
1003 except KeyError:
1004 # the node doesn't have a value for this property
1005 if isinstance(propclass, Multilink): bv = []
1006 else: bv = ''
1008 # String and Date values are sorted in the natural way
1009 if isinstance(propclass, String):
1010 # clean up the strings
1011 if av and av[0] in string.uppercase:
1012 av = an[prop] = av.lower()
1013 if bv and bv[0] in string.uppercase:
1014 bv = bn[prop] = bv.lower()
1015 if (isinstance(propclass, String) or
1016 isinstance(propclass, Date)):
1017 # it might be a string that's really an integer
1018 try:
1019 av = int(av)
1020 bv = int(bv)
1021 except:
1022 pass
1023 if dir == '+':
1024 r = cmp(av, bv)
1025 if r != 0: return r
1026 elif dir == '-':
1027 r = cmp(bv, av)
1028 if r != 0: return r
1030 # Link properties are sorted according to the value of
1031 # the "order" property on the linked nodes if it is
1032 # present; or otherwise on the key string of the linked
1033 # nodes; or finally on the node ids.
1034 elif isinstance(propclass, Link):
1035 link = db.classes[propclass.classname]
1036 if av is None and bv is not None: return -1
1037 if av is not None and bv is None: return 1
1038 if av is None and bv is None: continue
1039 if link.getprops().has_key('order'):
1040 if dir == '+':
1041 r = cmp(link.get(av, 'order'),
1042 link.get(bv, 'order'))
1043 if r != 0: return r
1044 elif dir == '-':
1045 r = cmp(link.get(bv, 'order'),
1046 link.get(av, 'order'))
1047 if r != 0: return r
1048 elif link.getkey():
1049 key = link.getkey()
1050 if dir == '+':
1051 r = cmp(link.get(av, key), link.get(bv, key))
1052 if r != 0: return r
1053 elif dir == '-':
1054 r = cmp(link.get(bv, key), link.get(av, key))
1055 if r != 0: return r
1056 else:
1057 if dir == '+':
1058 r = cmp(av, bv)
1059 if r != 0: return r
1060 elif dir == '-':
1061 r = cmp(bv, av)
1062 if r != 0: return r
1064 # Multilink properties are sorted according to how many
1065 # links are present.
1066 elif isinstance(propclass, Multilink):
1067 if dir == '+':
1068 r = cmp(len(av), len(bv))
1069 if r != 0: return r
1070 elif dir == '-':
1071 r = cmp(len(bv), len(av))
1072 if r != 0: return r
1073 # end for dir, prop in list:
1074 # end for list in sort, group:
1075 # if all else fails, compare the ids
1076 return cmp(a[0], b[0])
1078 l.sort(sortfun)
1079 return [i[0] for i in l]
1081 def count(self):
1082 """Get the number of nodes in this class.
1084 If the returned integer is 'numnodes', the ids of all the nodes
1085 in this class run from 1 to numnodes, and numnodes+1 will be the
1086 id of the next node to be created in this class.
1087 """
1088 return self.db.countnodes(self.classname)
1090 # Manipulating properties:
1092 def getprops(self, protected=1):
1093 """Return a dictionary mapping property names to property objects.
1094 If the "protected" flag is true, we include protected properties -
1095 those which may not be modified."""
1096 d = self.properties.copy()
1097 if protected:
1098 d['id'] = String()
1099 return d
1101 def addprop(self, **properties):
1102 """Add properties to this class.
1104 The keyword arguments in 'properties' must map names to property
1105 objects, or a TypeError is raised. None of the keys in 'properties'
1106 may collide with the names of existing properties, or a ValueError
1107 is raised before any properties have been added.
1108 """
1109 for key in properties.keys():
1110 if self.properties.has_key(key):
1111 raise ValueError, key
1112 self.properties.update(properties)
1114 # XXX not in spec
1115 class Node:
1116 ''' A convenience wrapper for the given node
1117 '''
1118 def __init__(self, cl, nodeid, cache=1):
1119 self.__dict__['cl'] = cl
1120 self.__dict__['nodeid'] = nodeid
1121 self.__dict__['cache'] = cache
1122 def keys(self, protected=1):
1123 return self.cl.getprops(protected=protected).keys()
1124 def values(self, protected=1):
1125 l = []
1126 for name in self.cl.getprops(protected=protected).keys():
1127 l.append(self.cl.get(self.nodeid, name, cache=self.cache))
1128 return l
1129 def items(self, protected=1):
1130 l = []
1131 for name in self.cl.getprops(protected=protected).keys():
1132 l.append((name, self.cl.get(self.nodeid, name, cache=self.cache)))
1133 return l
1134 def has_key(self, name):
1135 return self.cl.getprops().has_key(name)
1136 def __getattr__(self, name):
1137 if self.__dict__.has_key(name):
1138 return self.__dict__[name]
1139 try:
1140 return self.cl.get(self.nodeid, name, cache=self.cache)
1141 except KeyError, value:
1142 # we trap this but re-raise it as AttributeError - all other
1143 # exceptions should pass through untrapped
1144 pass
1145 # nope, no such attribute
1146 raise AttributeError, str(value)
1147 def __getitem__(self, name):
1148 return self.cl.get(self.nodeid, name, cache=self.cache)
1149 def __setattr__(self, name, value):
1150 try:
1151 return self.cl.set(self.nodeid, **{name: value})
1152 except KeyError, value:
1153 raise AttributeError, str(value)
1154 def __setitem__(self, name, value):
1155 self.cl.set(self.nodeid, **{name: value})
1156 def history(self):
1157 return self.cl.history(self.nodeid)
1158 def retire(self):
1159 return self.cl.retire(self.nodeid)
1162 def Choice(name, db, *options):
1163 '''Quick helper to create a simple class with choices
1164 '''
1165 cl = Class(db, name, name=String(), order=String())
1166 for i in range(len(options)):
1167 cl.create(name=options[i], order=i)
1168 return hyperdb.Link(name)
1170 #
1171 # $Log: not supported by cvs2svn $
1172 # Revision 1.69 2002/06/17 23:15:29 richard
1173 # Can debug to stdout now
1174 #
1175 # Revision 1.68 2002/06/11 06:52:03 richard
1176 # . #564271 ] find() and new properties
1177 #
1178 # Revision 1.67 2002/06/11 05:02:37 richard
1179 # . #565979 ] code error in hyperdb.Class.find
1180 #
1181 # Revision 1.66 2002/05/25 07:16:24 rochecompaan
1182 # Merged search_indexing-branch with HEAD
1183 #
1184 # Revision 1.65 2002/05/22 04:12:05 richard
1185 # . applied patch #558876 ] cgi client customization
1186 # ... with significant additions and modifications ;)
1187 # - extended handling of ML assignedto to all places it's handled
1188 # - added more NotFound info
1189 #
1190 # Revision 1.64 2002/05/15 06:21:21 richard
1191 # . node caching now works, and gives a small boost in performance
1192 #
1193 # As a part of this, I cleaned up the DEBUG output and implemented TRACE
1194 # output (HYPERDBTRACE='file to trace to') with checkpoints at the start of
1195 # CGI requests. Run roundup with python -O to skip all the DEBUG/TRACE stuff
1196 # (using if __debug__ which is compiled out with -O)
1197 #
1198 # Revision 1.63 2002/04/15 23:25:15 richard
1199 # . node ids are now generated from a lockable store - no more race conditions
1200 #
1201 # We're using the portalocker code by Jonathan Feinberg that was contributed
1202 # to the ASPN Python cookbook. This gives us locking across Unix and Windows.
1203 #
1204 # Revision 1.62 2002/04/03 07:05:50 richard
1205 # d'oh! killed retirement of nodes :(
1206 # all better now...
1207 #
1208 # Revision 1.61 2002/04/03 06:11:51 richard
1209 # Fix for old databases that contain properties that don't exist any more.
1210 #
1211 # Revision 1.60 2002/04/03 05:54:31 richard
1212 # Fixed serialisation problem by moving the serialisation step out of the
1213 # hyperdb.Class (get, set) into the hyperdb.Database.
1214 #
1215 # Also fixed htmltemplate after the showid changes I made yesterday.
1216 #
1217 # Unit tests for all of the above written.
1218 #
1219 # Revision 1.59.2.2 2002/04/20 13:23:33 rochecompaan
1220 # We now have a separate search page for nodes. Search links for
1221 # different classes can be customized in instance_config similar to
1222 # index links.
1223 #
1224 # Revision 1.59.2.1 2002/04/19 19:54:42 rochecompaan
1225 # cgi_client.py
1226 # removed search link for the time being
1227 # moved rendering of matches to htmltemplate
1228 # hyperdb.py
1229 # filtering of nodes on full text search incorporated in filter method
1230 # roundupdb.py
1231 # added paramater to call of filter method
1232 # roundup_indexer.py
1233 # added search method to RoundupIndexer class
1234 #
1235 # Revision 1.59 2002/03/12 22:52:26 richard
1236 # more pychecker warnings removed
1237 #
1238 # Revision 1.58 2002/02/27 03:23:16 richard
1239 # Ran it through pychecker, made fixes
1240 #
1241 # Revision 1.57 2002/02/20 05:23:24 richard
1242 # Didn't accomodate new values for new properties
1243 #
1244 # Revision 1.56 2002/02/20 05:05:28 richard
1245 # . Added simple editing for classes that don't define a templated interface.
1246 # - access using the admin "class list" interface
1247 # - limited to admin-only
1248 # - requires the csv module from object-craft (url given if it's missing)
1249 #
1250 # Revision 1.55 2002/02/15 07:27:12 richard
1251 # Oops, precedences around the way w0rng.
1252 #
1253 # Revision 1.54 2002/02/15 07:08:44 richard
1254 # . Alternate email addresses are now available for users. See the MIGRATION
1255 # file for info on how to activate the feature.
1256 #
1257 # Revision 1.53 2002/01/22 07:21:13 richard
1258 # . fixed back_bsddb so it passed the journal tests
1259 #
1260 # ... it didn't seem happy using the back_anydbm _open method, which is odd.
1261 # Yet another occurrance of whichdb not being able to recognise older bsddb
1262 # databases. Yadda yadda. Made the HYPERDBDEBUG stuff more sane in the
1263 # process.
1264 #
1265 # Revision 1.52 2002/01/21 16:33:19 rochecompaan
1266 # You can now use the roundup-admin tool to pack the database
1267 #
1268 # Revision 1.51 2002/01/21 03:01:29 richard
1269 # brief docco on the do_journal argument
1270 #
1271 # Revision 1.50 2002/01/19 13:16:04 rochecompaan
1272 # Journal entries for link and multilink properties can now be switched on
1273 # or off.
1274 #
1275 # Revision 1.49 2002/01/16 07:02:57 richard
1276 # . lots of date/interval related changes:
1277 # - more relaxed date format for input
1278 #
1279 # Revision 1.48 2002/01/14 06:32:34 richard
1280 # . #502951 ] adding new properties to old database
1281 #
1282 # Revision 1.47 2002/01/14 02:20:15 richard
1283 # . changed all config accesses so they access either the instance or the
1284 # config attriubute on the db. This means that all config is obtained from
1285 # instance_config instead of the mish-mash of classes. This will make
1286 # switching to a ConfigParser setup easier too, I hope.
1287 #
1288 # At a minimum, this makes migration a _little_ easier (a lot easier in the
1289 # 0.5.0 switch, I hope!)
1290 #
1291 # Revision 1.46 2002/01/07 10:42:23 richard
1292 # oops
1293 #
1294 # Revision 1.45 2002/01/02 04:18:17 richard
1295 # hyperdb docstrings
1296 #
1297 # Revision 1.44 2002/01/02 02:31:38 richard
1298 # Sorry for the huge checkin message - I was only intending to implement #496356
1299 # but I found a number of places where things had been broken by transactions:
1300 # . modified ROUNDUPDBSENDMAILDEBUG to be SENDMAILDEBUG and hold a filename
1301 # for _all_ roundup-generated smtp messages to be sent to.
1302 # . the transaction cache had broken the roundupdb.Class set() reactors
1303 # . newly-created author users in the mailgw weren't being committed to the db
1304 #
1305 # Stuff that made it into CHANGES.txt (ie. the stuff I was actually working
1306 # on when I found that stuff :):
1307 # . #496356 ] Use threading in messages
1308 # . detectors were being registered multiple times
1309 # . added tests for mailgw
1310 # . much better attaching of erroneous messages in the mail gateway
1311 #
1312 # Revision 1.43 2001/12/20 06:13:24 rochecompaan
1313 # Bugs fixed:
1314 # . Exception handling in hyperdb for strings-that-look-like numbers got
1315 # lost somewhere
1316 # . Internet Explorer submits full path for filename - we now strip away
1317 # the path
1318 # Features added:
1319 # . Link and multilink properties are now displayed sorted in the cgi
1320 # interface
1321 #
1322 # Revision 1.42 2001/12/16 10:53:37 richard
1323 # take a copy of the node dict so that the subsequent set
1324 # operation doesn't modify the oldvalues structure
1325 #
1326 # Revision 1.41 2001/12/15 23:47:47 richard
1327 # Cleaned up some bare except statements
1328 #
1329 # Revision 1.40 2001/12/14 23:42:57 richard
1330 # yuck, a gdbm instance tests false :(
1331 # I've left the debugging code in - it should be removed one day if we're ever
1332 # _really_ anal about performace :)
1333 #
1334 # Revision 1.39 2001/12/02 05:06:16 richard
1335 # . We now use weakrefs in the Classes to keep the database reference, so
1336 # the close() method on the database is no longer needed.
1337 # I bumped the minimum python requirement up to 2.1 accordingly.
1338 # . #487480 ] roundup-server
1339 # . #487476 ] INSTALL.txt
1340 #
1341 # I also cleaned up the change message / post-edit stuff in the cgi client.
1342 # There's now a clearly marked "TODO: append the change note" where I believe
1343 # the change note should be added there. The "changes" list will obviously
1344 # have to be modified to be a dict of the changes, or somesuch.
1345 #
1346 # More testing needed.
1347 #
1348 # Revision 1.38 2001/12/01 07:17:50 richard
1349 # . We now have basic transaction support! Information is only written to
1350 # the database when the commit() method is called. Only the anydbm
1351 # backend is modified in this way - neither of the bsddb backends have been.
1352 # The mail, admin and cgi interfaces all use commit (except the admin tool
1353 # doesn't have a commit command, so interactive users can't commit...)
1354 # . Fixed login/registration forwarding the user to the right page (or not,
1355 # on a failure)
1356 #
1357 # Revision 1.37 2001/11/28 21:55:35 richard
1358 # . login_action and newuser_action return values were being ignored
1359 # . Woohoo! Found that bloody re-login bug that was killing the mail
1360 # gateway.
1361 # (also a minor cleanup in hyperdb)
1362 #
1363 # Revision 1.36 2001/11/27 03:16:09 richard
1364 # Another place that wasn't handling missing properties.
1365 #
1366 # Revision 1.35 2001/11/22 15:46:42 jhermann
1367 # Added module docstrings to all modules.
1368 #
1369 # Revision 1.34 2001/11/21 04:04:43 richard
1370 # *sigh* more missing value handling
1371 #
1372 # Revision 1.33 2001/11/21 03:40:54 richard
1373 # more new property handling
1374 #
1375 # Revision 1.32 2001/11/21 03:11:28 richard
1376 # Better handling of new properties.
1377 #
1378 # Revision 1.31 2001/11/12 22:01:06 richard
1379 # Fixed issues with nosy reaction and author copies.
1380 #
1381 # Revision 1.30 2001/11/09 10:11:08 richard
1382 # . roundup-admin now handles all hyperdb exceptions
1383 #
1384 # Revision 1.29 2001/10/27 00:17:41 richard
1385 # Made Class.stringFind() do caseless matching.
1386 #
1387 # Revision 1.28 2001/10/21 04:44:50 richard
1388 # bug #473124: UI inconsistency with Link fields.
1389 # This also prompted me to fix a fairly long-standing usability issue -
1390 # that of being able to turn off certain filters.
1391 #
1392 # Revision 1.27 2001/10/20 23:44:27 richard
1393 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
1394 #
1395 # Revision 1.26 2001/10/16 03:48:01 richard
1396 # admin tool now complains if a "find" is attempted with a non-link property.
1397 #
1398 # Revision 1.25 2001/10/11 00:17:51 richard
1399 # Reverted a change in hyperdb so the default value for missing property
1400 # values in a create() is None and not '' (the empty string.) This obviously
1401 # breaks CSV import/export - the string 'None' will be created in an
1402 # export/import operation.
1403 #
1404 # Revision 1.24 2001/10/10 03:54:57 richard
1405 # Added database importing and exporting through CSV files.
1406 # Uses the csv module from object-craft for exporting if it's available.
1407 # Requires the csv module for importing.
1408 #
1409 # Revision 1.23 2001/10/09 23:58:10 richard
1410 # Moved the data stringification up into the hyperdb.Class class' get, set
1411 # and create methods. This means that the data is also stringified for the
1412 # journal call, and removes duplication of code from the backends. The
1413 # backend code now only sees strings.
1414 #
1415 # Revision 1.22 2001/10/09 07:25:59 richard
1416 # Added the Password property type. See "pydoc roundup.password" for
1417 # implementation details. Have updated some of the documentation too.
1418 #
1419 # Revision 1.21 2001/10/05 02:23:24 richard
1420 # . roundup-admin create now prompts for property info if none is supplied
1421 # on the command-line.
1422 # . hyperdb Class getprops() method may now return only the mutable
1423 # properties.
1424 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1425 # now support anonymous user access (read-only, unless there's an
1426 # "anonymous" user, in which case write access is permitted). Login
1427 # handling has been moved into cgi_client.Client.main()
1428 # . The "extended" schema is now the default in roundup init.
1429 # . The schemas have had their page headings modified to cope with the new
1430 # login handling. Existing installations should copy the interfaces.py
1431 # file from the roundup lib directory to their instance home.
1432 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1433 # Ping - has been removed.
1434 # . Fixed a whole bunch of places in the CGI interface where we should have
1435 # been returning Not Found instead of throwing an exception.
1436 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1437 # an item now throws an exception.
1438 #
1439 # Revision 1.20 2001/10/04 02:12:42 richard
1440 # Added nicer command-line item adding: passing no arguments will enter an
1441 # interactive more which asks for each property in turn. While I was at it, I
1442 # fixed an implementation problem WRT the spec - I wasn't raising a
1443 # ValueError if the key property was missing from a create(). Also added a
1444 # protected=boolean argument to getprops() so we can list only the mutable
1445 # properties (defaults to yes, which lists the immutables).
1446 #
1447 # Revision 1.19 2001/08/29 04:47:18 richard
1448 # Fixed CGI client change messages so they actually include the properties
1449 # changed (again).
1450 #
1451 # Revision 1.18 2001/08/16 07:34:59 richard
1452 # better CGI text searching - but hidden filter fields are disappearing...
1453 #
1454 # Revision 1.17 2001/08/16 06:59:58 richard
1455 # all searches use re now - and they're all case insensitive
1456 #
1457 # Revision 1.16 2001/08/15 23:43:18 richard
1458 # Fixed some isFooTypes that I missed.
1459 # Refactored some code in the CGI code.
1460 #
1461 # Revision 1.15 2001/08/12 06:32:36 richard
1462 # using isinstance(blah, Foo) now instead of isFooType
1463 #
1464 # Revision 1.14 2001/08/07 00:24:42 richard
1465 # stupid typo
1466 #
1467 # Revision 1.13 2001/08/07 00:15:51 richard
1468 # Added the copyright/license notice to (nearly) all files at request of
1469 # Bizar Software.
1470 #
1471 # Revision 1.12 2001/08/02 06:38:17 richard
1472 # Roundupdb now appends "mailing list" information to its messages which
1473 # include the e-mail address and web interface address. Templates may
1474 # override this in their db classes to include specific information (support
1475 # instructions, etc).
1476 #
1477 # Revision 1.11 2001/08/01 04:24:21 richard
1478 # mailgw was assuming certain properties existed on the issues being created.
1479 #
1480 # Revision 1.10 2001/07/30 02:38:31 richard
1481 # get() now has a default arg - for migration only.
1482 #
1483 # Revision 1.9 2001/07/29 09:28:23 richard
1484 # Fixed sorting by clicking on column headings.
1485 #
1486 # Revision 1.8 2001/07/29 08:27:40 richard
1487 # Fixed handling of passed-in values in form elements (ie. during a
1488 # drill-down)
1489 #
1490 # Revision 1.7 2001/07/29 07:01:39 richard
1491 # Added vim command to all source so that we don't get no steenkin' tabs :)
1492 #
1493 # Revision 1.6 2001/07/29 05:36:14 richard
1494 # Cleanup of the link label generation.
1495 #
1496 # Revision 1.5 2001/07/29 04:05:37 richard
1497 # Added the fabricated property "id".
1498 #
1499 # Revision 1.4 2001/07/27 06:25:35 richard
1500 # Fixed some of the exceptions so they're the right type.
1501 # Removed the str()-ification of node ids so we don't mask oopsy errors any
1502 # more.
1503 #
1504 # Revision 1.3 2001/07/27 05:17:14 richard
1505 # just some comments
1506 #
1507 # Revision 1.2 2001/07/22 12:09:32 richard
1508 # Final commit of Grande Splite
1509 #
1510 # Revision 1.1 2001/07/22 11:58:35 richard
1511 # More Grande Splite
1512 #
1513 #
1514 # vim: set filetype=python ts=4 sw=4 et si