Code

get() now has a default arg - for migration only.
[roundup.git] / roundup / hyperdb.py
1 # $Id: hyperdb.py,v 1.10 2001-07-30 02:38:31 richard Exp $
3 # standard python modules
4 import cPickle, re, string
6 # roundup modules
7 import date
10 #
11 # Types
12 #
13 class BaseType:
14     isStringType = 0
15     isDateType = 0
16     isIntervalType = 0
17     isLinkType = 0
18     isMultilinkType = 0
20 class String(BaseType):
21     def __init__(self):
22         """An object designating a String property."""
23         pass
24     def __repr__(self):
25         return '<%s>'%self.__class__
26     isStringType = 1
28 class Date(BaseType, String):
29     isDateType = 1
31 class Interval(BaseType, String):
32     isIntervalType = 1
34 class Link(BaseType):
35     def __init__(self, classname):
36         """An object designating a Link property that links to
37         nodes in a specified class."""
38         self.classname = classname
39     def __repr__(self):
40         return '<%s to "%s">'%(self.__class__, self.classname)
41     isLinkType = 1
43 class Multilink(BaseType, Link):
44     """An object designating a Multilink property that links
45        to nodes in a specified class.
46     """
47     isMultilinkType = 1
49 class DatabaseError(ValueError):
50     pass
53 #
54 # the base Database class
55 #
56 class Database:
57     # flag to set on retired entries
58     RETIRED_FLAG = '__hyperdb_retired'
61 _marker = []
62 #
63 # The base Class class
64 #
65 class Class:
66     """The handle to a particular class of nodes in a hyperdatabase."""
68     def __init__(self, db, classname, **properties):
69         """Create a new class with a given name and property specification.
71         'classname' must not collide with the name of an existing class,
72         or a ValueError is raised.  The keyword arguments in 'properties'
73         must map names to property objects, or a TypeError is raised.
74         """
75         self.classname = classname
76         self.properties = properties
77         self.db = db
78         self.key = ''
80         # do the db-related init stuff
81         db.addclass(self)
83     # Editing nodes:
85     def create(self, **propvalues):
86         """Create a new node of this class and return its id.
88         The keyword arguments in 'propvalues' map property names to values.
90         The values of arguments must be acceptable for the types of their
91         corresponding properties or a TypeError is raised.
92         
93         If this class has a key property, it must be present and its value
94         must not collide with other key strings or a ValueError is raised.
95         
96         Any other properties on this class that are missing from the
97         'propvalues' dictionary are set to None.
98         
99         If an id in a link or multilink property does not refer to a valid
100         node, an IndexError is raised.
101         """
102         if propvalues.has_key('id'):
103             raise KeyError, '"id" is reserved'
105         if self.db.journaltag is None:
106             raise DatabaseError, 'Database open read-only'
108         # new node's id
109         newid = str(self.count() + 1)
111         # validate propvalues
112         num_re = re.compile('^\d+$')
113         for key, value in propvalues.items():
114             if key == self.key:
115                 try:
116                     self.lookup(value)
117                 except KeyError:
118                     pass
119                 else:
120                     raise ValueError, 'node with key "%s" exists'%value
122             prop = self.properties[key]
124             if prop.isLinkType:
125                 if type(value) != type(''):
126                     raise ValueError, 'link value must be String'
127 #                value = str(value)
128                 link_class = self.properties[key].classname
129                 # if it isn't a number, it's a key
130                 if not num_re.match(value):
131                     try:
132                         value = self.db.classes[link_class].lookup(value)
133                     except:
134                         raise IndexError, 'new property "%s": %s not a %s'%(
135                             key, value, self.properties[key].classname)
136                 propvalues[key] = value
137                 if not self.db.hasnode(link_class, value):
138                     raise IndexError, '%s has no node %s'%(link_class, value)
140                 # register the link with the newly linked node
141                 self.db.addjournal(link_class, value, 'link',
142                     (self.classname, newid, key))
144             elif prop.isMultilinkType:
145                 if type(value) != type([]):
146                     raise TypeError, 'new property "%s" not a list of ids'%key
147                 link_class = self.properties[key].classname
148                 l = []
149                 for entry in value:
150                     if type(entry) != type(''):
151                         raise ValueError, 'link value must be String'
152                     # if it isn't a number, it's a key
153                     if not num_re.match(entry):
154                         try:
155                             entry = self.db.classes[link_class].lookup(entry)
156                         except:
157                             raise IndexError, 'new property "%s": %s not a %s'%(
158                                 key, entry, self.properties[key].classname)
159                     l.append(entry)
160                 value = l
161                 propvalues[key] = value
163                 # handle additions
164                 for id in value:
165                     if not self.db.hasnode(link_class, id):
166                         raise IndexError, '%s has no node %s'%(link_class, id)
167                     # register the link with the newly linked node
168                     self.db.addjournal(link_class, id, 'link',
169                         (self.classname, newid, key))
171             elif prop.isStringType:
172                 if type(value) != type(''):
173                     raise TypeError, 'new property "%s" not a string'%key
175             elif prop.isDateType:
176                 if not hasattr(value, 'isDate'):
177                     raise TypeError, 'new property "%s" not a Date'% key
179             elif prop.isIntervalType:
180                 if not hasattr(value, 'isInterval'):
181                     raise TypeError, 'new property "%s" not an Interval'% key
183         for key, prop in self.properties.items():
184             if propvalues.has_key(key):
185                 continue
186             if prop.isMultilinkType:
187                 propvalues[key] = []
188             else:
189                 propvalues[key] = None
191         # done
192         self.db.addnode(self.classname, newid, propvalues)
193         self.db.addjournal(self.classname, newid, 'create', propvalues)
194         return newid
196     def get(self, nodeid, propname, default=_marker):
197         """Get the value of a property on an existing node of this class.
199         'nodeid' must be the id of an existing node of this class or an
200         IndexError is raised.  'propname' must be the name of a property
201         of this class or a KeyError is raised.
202         """
203         if propname == 'id':
204             return nodeid
205 #        nodeid = str(nodeid)
206         d = self.db.getnode(self.classname, nodeid)
207         if not d.has_key(propname) and default is not _marker:
208             return default
209         return d[propname]
211     # XXX not in spec
212     def getnode(self, nodeid):
213         ''' Return a convenience wrapper for the node
214         '''
215         return Node(self, nodeid)
217     def set(self, nodeid, **propvalues):
218         """Modify a property on an existing node of this class.
219         
220         'nodeid' must be the id of an existing node of this class or an
221         IndexError is raised.
223         Each key in 'propvalues' must be the name of a property of this
224         class or a KeyError is raised.
226         All values in 'propvalues' must be acceptable types for their
227         corresponding properties or a TypeError is raised.
229         If the value of the key property is set, it must not collide with
230         other key strings or a ValueError is raised.
232         If the value of a Link or Multilink property contains an invalid
233         node id, a ValueError is raised.
234         """
235         if not propvalues:
236             return
238         if propvalues.has_key('id'):
239             raise KeyError, '"id" is reserved'
241         if self.db.journaltag is None:
242             raise DatabaseError, 'Database open read-only'
244 #        nodeid = str(nodeid)
245         node = self.db.getnode(self.classname, nodeid)
246         if node.has_key(self.db.RETIRED_FLAG):
247             raise IndexError
248         num_re = re.compile('^\d+$')
249         for key, value in propvalues.items():
250             if not node.has_key(key):
251                 raise KeyError, key
253             if key == self.key:
254                 try:
255                     self.lookup(value)
256                 except KeyError:
257                     pass
258                 else:
259                     raise ValueError, 'node with key "%s" exists'%value
261             prop = self.properties[key]
263             if prop.isLinkType:
264 #                value = str(value)
265                 link_class = self.properties[key].classname
266                 # if it isn't a number, it's a key
267                 if type(value) != type(''):
268                     raise ValueError, 'link value must be String'
269                 if not num_re.match(value):
270                     try:
271                         value = self.db.classes[link_class].lookup(value)
272                     except:
273                         raise IndexError, 'new property "%s": %s not a %s'%(
274                             key, value, self.properties[key].classname)
276                 if not self.db.hasnode(link_class, value):
277                     raise IndexError, '%s has no node %s'%(link_class, value)
279                 # register the unlink with the old linked node
280                 if node[key] is not None:
281                     self.db.addjournal(link_class, node[key], 'unlink',
282                         (self.classname, nodeid, key))
284                 # register the link with the newly linked node
285                 if value is not None:
286                     self.db.addjournal(link_class, value, 'link',
287                         (self.classname, nodeid, key))
289             elif prop.isMultilinkType:
290                 if type(value) != type([]):
291                     raise TypeError, 'new property "%s" not a list of ids'%key
292                 link_class = self.properties[key].classname
293                 l = []
294                 for entry in value:
295                     # if it isn't a number, it's a key
296                     if type(entry) != type(''):
297                         raise ValueError, 'link value must be String'
298                     if not num_re.match(entry):
299                         try:
300                             entry = self.db.classes[link_class].lookup(entry)
301                         except:
302                             raise IndexError, 'new property "%s": %s not a %s'%(
303                                 key, entry, self.properties[key].classname)
304                     l.append(entry)
305                 value = l
306                 propvalues[key] = value
308                 #handle removals
309                 l = node[key]
310                 for id in l[:]:
311                     if id in value:
312                         continue
313                     # register the unlink with the old linked node
314                     self.db.addjournal(link_class, id, 'unlink',
315                         (self.classname, nodeid, key))
316                     l.remove(id)
318                 # handle additions
319                 for id in value:
320                     if not self.db.hasnode(link_class, id):
321                         raise IndexError, '%s has no node %s'%(link_class, id)
322                     if id in l:
323                         continue
324                     # register the link with the newly linked node
325                     self.db.addjournal(link_class, id, 'link',
326                         (self.classname, nodeid, key))
327                     l.append(id)
329             elif prop.isStringType:
330                 if value is not None and type(value) != type(''):
331                     raise TypeError, 'new property "%s" not a string'%key
333             elif prop.isDateType:
334                 if not hasattr(value, 'isDate'):
335                     raise TypeError, 'new property "%s" not a Date'% key
337             elif prop.isIntervalType:
338                 if not hasattr(value, 'isInterval'):
339                     raise TypeError, 'new property "%s" not an Interval'% key
341             node[key] = value
343         self.db.setnode(self.classname, nodeid, node)
344         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
346     def retire(self, nodeid):
347         """Retire a node.
348         
349         The properties on the node remain available from the get() method,
350         and the node's id is never reused.
351         
352         Retired nodes are not returned by the find(), list(), or lookup()
353         methods, and other nodes may reuse the values of their key properties.
354         """
355 #        nodeid = str(nodeid)
356         if self.db.journaltag is None:
357             raise DatabaseError, 'Database open read-only'
358         node = self.db.getnode(self.classname, nodeid)
359         node[self.db.RETIRED_FLAG] = 1
360         self.db.setnode(self.classname, nodeid, node)
361         self.db.addjournal(self.classname, nodeid, 'retired', None)
363     def history(self, nodeid):
364         """Retrieve the journal of edits on a particular node.
366         'nodeid' must be the id of an existing node of this class or an
367         IndexError is raised.
369         The returned list contains tuples of the form
371             (date, tag, action, params)
373         'date' is a Timestamp object specifying the time of the change and
374         'tag' is the journaltag specified when the database was opened.
375         """
376         return self.db.getjournal(self.classname, nodeid)
378     # Locating nodes:
380     def setkey(self, propname):
381         """Select a String property of this class to be the key property.
383         'propname' must be the name of a String property of this class or
384         None, or a TypeError is raised.  The values of the key property on
385         all existing nodes must be unique or a ValueError is raised.
386         """
387         self.key = propname
389     def getkey(self):
390         """Return the name of the key property for this class or None."""
391         return self.key
393     def labelprop(self):
394         ''' Return the property name for a label for the given node.
396         This method attempts to generate a consistent label for the node.
397         It tries the following in order:
398             1. key property
399             2. "name" property
400             3. "title" property
401             4. first property from the sorted property name list
402         '''
403         k = self.getkey()
404         if  k:
405             return k
406         props = self.getprops()
407         if props.has_key('name'):
408             return 'name'
409         elif props.has_key('title'):
410             return 'title'
411         props = props.keys()
412         props.sort()
413         return props[0]
415     # TODO: set up a separate index db file for this? profile?
416     def lookup(self, keyvalue):
417         """Locate a particular node by its key property and return its id.
419         If this class has no key property, a TypeError is raised.  If the
420         'keyvalue' matches one of the values for the key property among
421         the nodes in this class, the matching node's id is returned;
422         otherwise a KeyError is raised.
423         """
424         cldb = self.db.getclassdb(self.classname)
425         for nodeid in self.db.getnodeids(self.classname, cldb):
426             node = self.db.getnode(self.classname, nodeid, cldb)
427             if node.has_key(self.db.RETIRED_FLAG):
428                 continue
429             if node[self.key] == keyvalue:
430                 return nodeid
431         cldb.close()
432         raise KeyError, keyvalue
434     # XXX: change from spec - allows multiple props to match
435     def find(self, **propspec):
436         """Get the ids of nodes in this class which link to a given node.
438         'propspec' consists of keyword args propname=nodeid   
439           'propname' must be the name of a property in this class, or a
440             KeyError is raised.  That property must be a Link or Multilink
441             property, or a TypeError is raised.
443           'nodeid' must be the id of an existing node in the class linked
444             to by the given property, or an IndexError is raised.
445         """
446         propspec = propspec.items()
447         for propname, nodeid in propspec:
448 #            nodeid = str(nodeid)
449             # check the prop is OK
450             prop = self.properties[propname]
451             if not prop.isLinkType and not prop.isMultilinkType:
452                 raise TypeError, "'%s' not a Link/Multilink property"%propname
453             if not self.db.hasnode(prop.classname, nodeid):
454                 raise ValueError, '%s has no node %s'%(link_class, nodeid)
456         # ok, now do the find
457         cldb = self.db.getclassdb(self.classname)
458         l = []
459         for id in self.db.getnodeids(self.classname, cldb):
460             node = self.db.getnode(self.classname, id, cldb)
461             if node.has_key(self.db.RETIRED_FLAG):
462                 continue
463             for propname, nodeid in propspec:
464 #                nodeid = str(nodeid)
465                 property = node[propname]
466                 if prop.isLinkType and nodeid == property:
467                     l.append(id)
468                 elif prop.isMultilinkType and nodeid in property:
469                     l.append(id)
470         cldb.close()
471         return l
473     def stringFind(self, **requirements):
474         """Locate a particular node by matching a set of its String properties.
476         If the property is not a String property, a TypeError is raised.
477         
478         The return is a list of the id of all nodes that match.
479         """
480         for propname in requirements.keys():
481             prop = self.properties[propname]
482             if not prop.isStringType:
483                 raise TypeError, "'%s' not a String property"%propname
484         l = []
485         cldb = self.db.getclassdb(self.classname)
486         for nodeid in self.db.getnodeids(self.classname, cldb):
487             node = self.db.getnode(self.classname, nodeid, cldb)
488             if node.has_key(self.db.RETIRED_FLAG):
489                 continue
490             for key, value in requirements.items():
491                 if node[key] != value:
492                     break
493             else:
494                 l.append(nodeid)
495         cldb.close()
496         return l
498     def list(self):
499         """Return a list of the ids of the active nodes in this class."""
500         l = []
501         cn = self.classname
502         cldb = self.db.getclassdb(cn)
503         for nodeid in self.db.getnodeids(cn, cldb):
504             node = self.db.getnode(cn, nodeid, cldb)
505             if node.has_key(self.db.RETIRED_FLAG):
506                 continue
507             l.append(nodeid)
508         l.sort()
509         cldb.close()
510         return l
512     # XXX not in spec
513     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
514         ''' Return a list of the ids of the active nodes in this class that
515             match the 'filter' spec, sorted by the group spec and then the
516             sort spec
517         '''
518         cn = self.classname
520         # optimise filterspec
521         l = []
522         props = self.getprops()
523         for k, v in filterspec.items():
524             propclass = props[k]
525             if propclass.isLinkType:
526                 if type(v) is not type([]):
527                     v = [v]
528                 # replace key values with node ids
529                 u = []
530                 link_class =  self.db.classes[propclass.classname]
531                 for entry in v:
532                     if not num_re.match(entry):
533                         try:
534                             entry = link_class.lookup(entry)
535                         except:
536                             raise ValueError, 'new property "%s": %s not a %s'%(
537                                 k, entry, self.properties[k].classname)
538                     u.append(entry)
540                 l.append((0, k, u))
541             elif propclass.isMultilinkType:
542                 if type(v) is not type([]):
543                     v = [v]
544                 # replace key values with node ids
545                 u = []
546                 link_class =  self.db.classes[propclass.classname]
547                 for entry in v:
548                     if not num_re.match(entry):
549                         try:
550                             entry = link_class.lookup(entry)
551                         except:
552                             raise ValueError, 'new property "%s": %s not a %s'%(
553                                 k, entry, self.properties[k].classname)
554                     u.append(entry)
555                 l.append((1, k, u))
556             elif propclass.isStringType:
557                 if '*' in v or '?' in v:
558                     # simple glob searching
559                     v = v.replace('?', '.')
560                     v = v.replace('*', '.*?')
561                     v = re.compile(v)
562                     l.append((2, k, v))
563                 elif v[0] == '^':
564                     # start-anchored
565                     if v[-1] == '$':
566                         # _and_ end-anchored
567                         l.append((6, k, v[1:-1]))
568                     l.append((3, k, v[1:]))
569                 elif v[-1] == '$':
570                     # end-anchored
571                     l.append((4, k, v[:-1]))
572                 else:
573                     # substring
574                     l.append((5, k, v))
575             else:
576                 l.append((6, k, v))
577         filterspec = l
579         # now, find all the nodes that are active and pass filtering
580         l = []
581         cldb = self.db.getclassdb(cn)
582         for nodeid in self.db.getnodeids(cn, cldb):
583             node = self.db.getnode(cn, nodeid, cldb)
584             if node.has_key(self.db.RETIRED_FLAG):
585                 continue
586             # apply filter
587             for t, k, v in filterspec:
588                 if t == 0 and node[k] not in v:
589                     # link - if this node'd property doesn't appear in the
590                     # filterspec's nodeid list, skip it
591                     break
592                 elif t == 1:
593                     # multilink - if any of the nodeids required by the
594                     # filterspec aren't in this node's property, then skip
595                     # it
596                     for value in v:
597                         if value not in node[k]:
598                             break
599                     else:
600                         continue
601                     break
602                 elif t == 2 and not v.search(node[k]):
603                     # RE search
604                     break
605                 elif t == 3 and node[k][:len(v)] != v:
606                     # start anchored
607                     break
608                 elif t == 4 and node[k][-len(v):] != v:
609                     # end anchored
610                     break
611                 elif t == 5 and node[k].find(v) == -1:
612                     # substring search
613                     break
614                 elif t == 6 and node[k] != v:
615                     # straight value comparison for the other types
616                     break
617             else:
618                 l.append((nodeid, node))
619         l.sort()
620         cldb.close()
622         # optimise sort
623         m = []
624         for entry in sort:
625             if entry[0] != '-':
626                 m.append(('+', entry))
627             else:
628                 m.append((entry[0], entry[1:]))
629         sort = m
631         # optimise group
632         m = []
633         for entry in group:
634             if entry[0] != '-':
635                 m.append(('+', entry))
636             else:
637                 m.append((entry[0], entry[1:]))
638         group = m
639         # now, sort the result
640         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
641                 db = self.db, cl=self):
642             a_id, an = a
643             b_id, bn = b
644             # sort by group and then sort
645             for list in group, sort:
646                 for dir, prop in list:
647                     # handle the properties that might be "faked"
648                     if not an.has_key(prop):
649                         an[prop] = cl.get(a_id, prop)
650                     av = an[prop]
651                     if not bn.has_key(prop):
652                         bn[prop] = cl.get(b_id, prop)
653                     bv = bn[prop]
655                     # sorting is class-specific
656                     propclass = properties[prop]
658                     # String and Date values are sorted in the natural way
659                     if propclass.isStringType:
660                         # clean up the strings
661                         if av and av[0] in string.uppercase:
662                             av = an[prop] = av.lower()
663                         if bv and bv[0] in string.uppercase:
664                             bv = bn[prop] = bv.lower()
665                     if propclass.isStringType or propclass.isDateType:
666                         if dir == '+':
667                             r = cmp(av, bv)
668                             if r != 0: return r
669                         elif dir == '-':
670                             r = cmp(bv, av)
671                             if r != 0: return r
673                     # Link properties are sorted according to the value of
674                     # the "order" property on the linked nodes if it is
675                     # present; or otherwise on the key string of the linked
676                     # nodes; or finally on  the node ids.
677                     elif propclass.isLinkType:
678                         link = db.classes[propclass.classname]
679                         if av is None and bv is not None: return -1
680                         if av is not None and bv is None: return 1
681                         if av is None and bv is None: return 0
682                         if link.getprops().has_key('order'):
683                             if dir == '+':
684                                 r = cmp(link.get(av, 'order'),
685                                     link.get(bv, 'order'))
686                                 if r != 0: return r
687                             elif dir == '-':
688                                 r = cmp(link.get(bv, 'order'),
689                                     link.get(av, 'order'))
690                                 if r != 0: return r
691                         elif link.getkey():
692                             key = link.getkey()
693                             if dir == '+':
694                                 r = cmp(link.get(av, key), link.get(bv, key))
695                                 if r != 0: return r
696                             elif dir == '-':
697                                 r = cmp(link.get(bv, key), link.get(av, key))
698                                 if r != 0: return r
699                         else:
700                             if dir == '+':
701                                 r = cmp(av, bv)
702                                 if r != 0: return r
703                             elif dir == '-':
704                                 r = cmp(bv, av)
705                                 if r != 0: return r
707                     # Multilink properties are sorted according to how many
708                     # links are present.
709                     elif propclass.isMultilinkType:
710                         if dir == '+':
711                             r = cmp(len(av), len(bv))
712                             if r != 0: return r
713                         elif dir == '-':
714                             r = cmp(len(bv), len(av))
715                             if r != 0: return r
716                 # end for dir, prop in list:
717             # end for list in sort, group:
718             # if all else fails, compare the ids
719             return cmp(a[0], b[0])
721         l.sort(sortfun)
722         return [i[0] for i in l]
724     def count(self):
725         """Get the number of nodes in this class.
727         If the returned integer is 'numnodes', the ids of all the nodes
728         in this class run from 1 to numnodes, and numnodes+1 will be the
729         id of the next node to be created in this class.
730         """
731         return self.db.countnodes(self.classname)
733     # Manipulating properties:
735     def getprops(self):
736         """Return a dictionary mapping property names to property objects."""
737         d = self.properties.copy()
738         d['id'] = String()
739         return d
741     def addprop(self, **properties):
742         """Add properties to this class.
744         The keyword arguments in 'properties' must map names to property
745         objects, or a TypeError is raised.  None of the keys in 'properties'
746         may collide with the names of existing properties, or a ValueError
747         is raised before any properties have been added.
748         """
749         for key in properties.keys():
750             if self.properties.has_key(key):
751                 raise ValueError, key
752         self.properties.update(properties)
755 # XXX not in spec
756 class Node:
757     ''' A convenience wrapper for the given node
758     '''
759     def __init__(self, cl, nodeid):
760         self.__dict__['cl'] = cl
761         self.__dict__['nodeid'] = nodeid
762     def keys(self):
763         return self.cl.getprops().keys()
764     def has_key(self, name):
765         return self.cl.getprops().has_key(name)
766     def __getattr__(self, name):
767         if self.__dict__.has_key(name):
768             return self.__dict__['name']
769         try:
770             return self.cl.get(self.nodeid, name)
771         except KeyError, value:
772             raise AttributeError, str(value)
773     def __getitem__(self, name):
774         return self.cl.get(self.nodeid, name)
775     def __setattr__(self, name, value):
776         try:
777             return self.cl.set(self.nodeid, **{name: value})
778         except KeyError, value:
779             raise AttributeError, str(value)
780     def __setitem__(self, name, value):
781         self.cl.set(self.nodeid, **{name: value})
782     def history(self):
783         return self.cl.history(self.nodeid)
784     def retire(self):
785         return self.cl.retire(self.nodeid)
788 def Choice(name, *options):
789     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
790     for i in range(len(options)):
791         cl.create(name=option[i], order=i)
792     return hyperdb.Link(name)
795 # $Log: not supported by cvs2svn $
796 # Revision 1.9  2001/07/29 09:28:23  richard
797 # Fixed sorting by clicking on column headings.
799 # Revision 1.8  2001/07/29 08:27:40  richard
800 # Fixed handling of passed-in values in form elements (ie. during a
801 # drill-down)
803 # Revision 1.7  2001/07/29 07:01:39  richard
804 # Added vim command to all source so that we don't get no steenkin' tabs :)
806 # Revision 1.6  2001/07/29 05:36:14  richard
807 # Cleanup of the link label generation.
809 # Revision 1.5  2001/07/29 04:05:37  richard
810 # Added the fabricated property "id".
812 # Revision 1.4  2001/07/27 06:25:35  richard
813 # Fixed some of the exceptions so they're the right type.
814 # Removed the str()-ification of node ids so we don't mask oopsy errors any
815 # more.
817 # Revision 1.3  2001/07/27 05:17:14  richard
818 # just some comments
820 # Revision 1.2  2001/07/22 12:09:32  richard
821 # Final commit of Grande Splite
823 # Revision 1.1  2001/07/22 11:58:35  richard
824 # More Grande Splite
827 # vim: set filetype=python ts=4 sw=4 et si