Code

mailgw was assuming certain properties existed on the issues being created.
[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             # try to handle this property
123             try:
124                 prop = self.properties[key]
125             except KeyError:
126                 raise KeyError, '"%s" has no property "%s"'%(self.classname,
127                     key)
129             if prop.isLinkType:
130                 if type(value) != type(''):
131                     raise ValueError, 'link value must be String'
132 #                value = str(value)
133                 link_class = self.properties[key].classname
134                 # if it isn't a number, it's a key
135                 if not num_re.match(value):
136                     try:
137                         value = self.db.classes[link_class].lookup(value)
138                     except:
139                         raise IndexError, 'new property "%s": %s not a %s'%(
140                             key, value, self.properties[key].classname)
141                 propvalues[key] = value
142                 if not self.db.hasnode(link_class, value):
143                     raise IndexError, '%s has no node %s'%(link_class, value)
145                 # register the link with the newly linked node
146                 self.db.addjournal(link_class, value, 'link',
147                     (self.classname, newid, key))
149             elif prop.isMultilinkType:
150                 if type(value) != type([]):
151                     raise TypeError, 'new property "%s" not a list of ids'%key
152                 link_class = self.properties[key].classname
153                 l = []
154                 for entry in value:
155                     if type(entry) != type(''):
156                         raise ValueError, 'link value must be String'
157                     # if it isn't a number, it's a key
158                     if not num_re.match(entry):
159                         try:
160                             entry = self.db.classes[link_class].lookup(entry)
161                         except:
162                             raise IndexError, 'new property "%s": %s not a %s'%(
163                                 key, entry, self.properties[key].classname)
164                     l.append(entry)
165                 value = l
166                 propvalues[key] = value
168                 # handle additions
169                 for id in value:
170                     if not self.db.hasnode(link_class, id):
171                         raise IndexError, '%s has no node %s'%(link_class, id)
172                     # register the link with the newly linked node
173                     self.db.addjournal(link_class, id, 'link',
174                         (self.classname, newid, key))
176             elif prop.isStringType:
177                 if type(value) != type(''):
178                     raise TypeError, 'new property "%s" not a string'%key
180             elif prop.isDateType:
181                 if not hasattr(value, 'isDate'):
182                     raise TypeError, 'new property "%s" not a Date'% key
184             elif prop.isIntervalType:
185                 if not hasattr(value, 'isInterval'):
186                     raise TypeError, 'new property "%s" not an Interval'% key
188         for key, prop in self.properties.items():
189             if propvalues.has_key(key):
190                 continue
191             if prop.isMultilinkType:
192                 propvalues[key] = []
193             else:
194                 propvalues[key] = None
196         # done
197         self.db.addnode(self.classname, newid, propvalues)
198         self.db.addjournal(self.classname, newid, 'create', propvalues)
199         return newid
201     def get(self, nodeid, propname, default=_marker):
202         """Get the value of a property on an existing node of this class.
204         'nodeid' must be the id of an existing node of this class or an
205         IndexError is raised.  'propname' must be the name of a property
206         of this class or a KeyError is raised.
207         """
208         if propname == 'id':
209             return nodeid
210 #        nodeid = str(nodeid)
211         d = self.db.getnode(self.classname, nodeid)
212         if not d.has_key(propname) and default is not _marker:
213             return default
214         return d[propname]
216     # XXX not in spec
217     def getnode(self, nodeid):
218         ''' Return a convenience wrapper for the node
219         '''
220         return Node(self, nodeid)
222     def set(self, nodeid, **propvalues):
223         """Modify a property on an existing node of this class.
224         
225         'nodeid' must be the id of an existing node of this class or an
226         IndexError is raised.
228         Each key in 'propvalues' must be the name of a property of this
229         class or a KeyError is raised.
231         All values in 'propvalues' must be acceptable types for their
232         corresponding properties or a TypeError is raised.
234         If the value of the key property is set, it must not collide with
235         other key strings or a ValueError is raised.
237         If the value of a Link or Multilink property contains an invalid
238         node id, a ValueError is raised.
239         """
240         if not propvalues:
241             return
243         if propvalues.has_key('id'):
244             raise KeyError, '"id" is reserved'
246         if self.db.journaltag is None:
247             raise DatabaseError, 'Database open read-only'
249 #        nodeid = str(nodeid)
250         node = self.db.getnode(self.classname, nodeid)
251         if node.has_key(self.db.RETIRED_FLAG):
252             raise IndexError
253         num_re = re.compile('^\d+$')
254         for key, value in propvalues.items():
255             if not node.has_key(key):
256                 raise KeyError, key
258             if key == self.key:
259                 try:
260                     self.lookup(value)
261                 except KeyError:
262                     pass
263                 else:
264                     raise ValueError, 'node with key "%s" exists'%value
266             prop = self.properties[key]
268             if prop.isLinkType:
269 #                value = str(value)
270                 link_class = self.properties[key].classname
271                 # if it isn't a number, it's a key
272                 if type(value) != type(''):
273                     raise ValueError, 'link value must be String'
274                 if not num_re.match(value):
275                     try:
276                         value = self.db.classes[link_class].lookup(value)
277                     except:
278                         raise IndexError, 'new property "%s": %s not a %s'%(
279                             key, value, self.properties[key].classname)
281                 if not self.db.hasnode(link_class, value):
282                     raise IndexError, '%s has no node %s'%(link_class, value)
284                 # register the unlink with the old linked node
285                 if node[key] is not None:
286                     self.db.addjournal(link_class, node[key], 'unlink',
287                         (self.classname, nodeid, key))
289                 # register the link with the newly linked node
290                 if value is not None:
291                     self.db.addjournal(link_class, value, 'link',
292                         (self.classname, nodeid, key))
294             elif prop.isMultilinkType:
295                 if type(value) != type([]):
296                     raise TypeError, 'new property "%s" not a list of ids'%key
297                 link_class = self.properties[key].classname
298                 l = []
299                 for entry in value:
300                     # if it isn't a number, it's a key
301                     if type(entry) != type(''):
302                         raise ValueError, 'link value must be String'
303                     if not num_re.match(entry):
304                         try:
305                             entry = self.db.classes[link_class].lookup(entry)
306                         except:
307                             raise IndexError, 'new property "%s": %s not a %s'%(
308                                 key, entry, self.properties[key].classname)
309                     l.append(entry)
310                 value = l
311                 propvalues[key] = value
313                 #handle removals
314                 l = node[key]
315                 for id in l[:]:
316                     if id in value:
317                         continue
318                     # register the unlink with the old linked node
319                     self.db.addjournal(link_class, id, 'unlink',
320                         (self.classname, nodeid, key))
321                     l.remove(id)
323                 # handle additions
324                 for id in value:
325                     if not self.db.hasnode(link_class, id):
326                         raise IndexError, '%s has no node %s'%(link_class, id)
327                     if id in l:
328                         continue
329                     # register the link with the newly linked node
330                     self.db.addjournal(link_class, id, 'link',
331                         (self.classname, nodeid, key))
332                     l.append(id)
334             elif prop.isStringType:
335                 if value is not None and type(value) != type(''):
336                     raise TypeError, 'new property "%s" not a string'%key
338             elif prop.isDateType:
339                 if not hasattr(value, 'isDate'):
340                     raise TypeError, 'new property "%s" not a Date'% key
342             elif prop.isIntervalType:
343                 if not hasattr(value, 'isInterval'):
344                     raise TypeError, 'new property "%s" not an Interval'% key
346             node[key] = value
348         self.db.setnode(self.classname, nodeid, node)
349         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
351     def retire(self, nodeid):
352         """Retire a node.
353         
354         The properties on the node remain available from the get() method,
355         and the node's id is never reused.
356         
357         Retired nodes are not returned by the find(), list(), or lookup()
358         methods, and other nodes may reuse the values of their key properties.
359         """
360 #        nodeid = str(nodeid)
361         if self.db.journaltag is None:
362             raise DatabaseError, 'Database open read-only'
363         node = self.db.getnode(self.classname, nodeid)
364         node[self.db.RETIRED_FLAG] = 1
365         self.db.setnode(self.classname, nodeid, node)
366         self.db.addjournal(self.classname, nodeid, 'retired', None)
368     def history(self, nodeid):
369         """Retrieve the journal of edits on a particular node.
371         'nodeid' must be the id of an existing node of this class or an
372         IndexError is raised.
374         The returned list contains tuples of the form
376             (date, tag, action, params)
378         'date' is a Timestamp object specifying the time of the change and
379         'tag' is the journaltag specified when the database was opened.
380         """
381         return self.db.getjournal(self.classname, nodeid)
383     # Locating nodes:
385     def setkey(self, propname):
386         """Select a String property of this class to be the key property.
388         'propname' must be the name of a String property of this class or
389         None, or a TypeError is raised.  The values of the key property on
390         all existing nodes must be unique or a ValueError is raised.
391         """
392         self.key = propname
394     def getkey(self):
395         """Return the name of the key property for this class or None."""
396         return self.key
398     def labelprop(self):
399         ''' Return the property name for a label for the given node.
401         This method attempts to generate a consistent label for the node.
402         It tries the following in order:
403             1. key property
404             2. "name" property
405             3. "title" property
406             4. first property from the sorted property name list
407         '''
408         k = self.getkey()
409         if  k:
410             return k
411         props = self.getprops()
412         if props.has_key('name'):
413             return 'name'
414         elif props.has_key('title'):
415             return 'title'
416         props = props.keys()
417         props.sort()
418         return props[0]
420     # TODO: set up a separate index db file for this? profile?
421     def lookup(self, keyvalue):
422         """Locate a particular node by its key property and return its id.
424         If this class has no key property, a TypeError is raised.  If the
425         'keyvalue' matches one of the values for the key property among
426         the nodes in this class, the matching node's id is returned;
427         otherwise a KeyError is raised.
428         """
429         cldb = self.db.getclassdb(self.classname)
430         for nodeid in self.db.getnodeids(self.classname, cldb):
431             node = self.db.getnode(self.classname, nodeid, cldb)
432             if node.has_key(self.db.RETIRED_FLAG):
433                 continue
434             if node[self.key] == keyvalue:
435                 return nodeid
436         cldb.close()
437         raise KeyError, keyvalue
439     # XXX: change from spec - allows multiple props to match
440     def find(self, **propspec):
441         """Get the ids of nodes in this class which link to a given node.
443         'propspec' consists of keyword args propname=nodeid   
444           'propname' must be the name of a property in this class, or a
445             KeyError is raised.  That property must be a Link or Multilink
446             property, or a TypeError is raised.
448           'nodeid' must be the id of an existing node in the class linked
449             to by the given property, or an IndexError is raised.
450         """
451         propspec = propspec.items()
452         for propname, nodeid in propspec:
453 #            nodeid = str(nodeid)
454             # check the prop is OK
455             prop = self.properties[propname]
456             if not prop.isLinkType and not prop.isMultilinkType:
457                 raise TypeError, "'%s' not a Link/Multilink property"%propname
458             if not self.db.hasnode(prop.classname, nodeid):
459                 raise ValueError, '%s has no node %s'%(link_class, nodeid)
461         # ok, now do the find
462         cldb = self.db.getclassdb(self.classname)
463         l = []
464         for id in self.db.getnodeids(self.classname, cldb):
465             node = self.db.getnode(self.classname, id, cldb)
466             if node.has_key(self.db.RETIRED_FLAG):
467                 continue
468             for propname, nodeid in propspec:
469 #                nodeid = str(nodeid)
470                 property = node[propname]
471                 if prop.isLinkType and nodeid == property:
472                     l.append(id)
473                 elif prop.isMultilinkType and nodeid in property:
474                     l.append(id)
475         cldb.close()
476         return l
478     def stringFind(self, **requirements):
479         """Locate a particular node by matching a set of its String properties.
481         If the property is not a String property, a TypeError is raised.
482         
483         The return is a list of the id of all nodes that match.
484         """
485         for propname in requirements.keys():
486             prop = self.properties[propname]
487             if not prop.isStringType:
488                 raise TypeError, "'%s' not a String property"%propname
489         l = []
490         cldb = self.db.getclassdb(self.classname)
491         for nodeid in self.db.getnodeids(self.classname, cldb):
492             node = self.db.getnode(self.classname, nodeid, cldb)
493             if node.has_key(self.db.RETIRED_FLAG):
494                 continue
495             for key, value in requirements.items():
496                 if node[key] != value:
497                     break
498             else:
499                 l.append(nodeid)
500         cldb.close()
501         return l
503     def list(self):
504         """Return a list of the ids of the active nodes in this class."""
505         l = []
506         cn = self.classname
507         cldb = self.db.getclassdb(cn)
508         for nodeid in self.db.getnodeids(cn, cldb):
509             node = self.db.getnode(cn, nodeid, cldb)
510             if node.has_key(self.db.RETIRED_FLAG):
511                 continue
512             l.append(nodeid)
513         l.sort()
514         cldb.close()
515         return l
517     # XXX not in spec
518     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
519         ''' Return a list of the ids of the active nodes in this class that
520             match the 'filter' spec, sorted by the group spec and then the
521             sort spec
522         '''
523         cn = self.classname
525         # optimise filterspec
526         l = []
527         props = self.getprops()
528         for k, v in filterspec.items():
529             propclass = props[k]
530             if propclass.isLinkType:
531                 if type(v) is not type([]):
532                     v = [v]
533                 # replace key values with node ids
534                 u = []
535                 link_class =  self.db.classes[propclass.classname]
536                 for entry in v:
537                     if not num_re.match(entry):
538                         try:
539                             entry = link_class.lookup(entry)
540                         except:
541                             raise ValueError, 'new property "%s": %s not a %s'%(
542                                 k, entry, self.properties[k].classname)
543                     u.append(entry)
545                 l.append((0, k, u))
546             elif propclass.isMultilinkType:
547                 if type(v) is not type([]):
548                     v = [v]
549                 # replace key values with node ids
550                 u = []
551                 link_class =  self.db.classes[propclass.classname]
552                 for entry in v:
553                     if not num_re.match(entry):
554                         try:
555                             entry = link_class.lookup(entry)
556                         except:
557                             raise ValueError, 'new property "%s": %s not a %s'%(
558                                 k, entry, self.properties[k].classname)
559                     u.append(entry)
560                 l.append((1, k, u))
561             elif propclass.isStringType:
562                 if '*' in v or '?' in v:
563                     # simple glob searching
564                     v = v.replace('?', '.')
565                     v = v.replace('*', '.*?')
566                     v = re.compile(v)
567                     l.append((2, k, v))
568                 elif v[0] == '^':
569                     # start-anchored
570                     if v[-1] == '$':
571                         # _and_ end-anchored
572                         l.append((6, k, v[1:-1]))
573                     l.append((3, k, v[1:]))
574                 elif v[-1] == '$':
575                     # end-anchored
576                     l.append((4, k, v[:-1]))
577                 else:
578                     # substring
579                     l.append((5, k, v))
580             else:
581                 l.append((6, k, v))
582         filterspec = l
584         # now, find all the nodes that are active and pass filtering
585         l = []
586         cldb = self.db.getclassdb(cn)
587         for nodeid in self.db.getnodeids(cn, cldb):
588             node = self.db.getnode(cn, nodeid, cldb)
589             if node.has_key(self.db.RETIRED_FLAG):
590                 continue
591             # apply filter
592             for t, k, v in filterspec:
593                 if t == 0 and node[k] not in v:
594                     # link - if this node'd property doesn't appear in the
595                     # filterspec's nodeid list, skip it
596                     break
597                 elif t == 1:
598                     # multilink - if any of the nodeids required by the
599                     # filterspec aren't in this node's property, then skip
600                     # it
601                     for value in v:
602                         if value not in node[k]:
603                             break
604                     else:
605                         continue
606                     break
607                 elif t == 2 and not v.search(node[k]):
608                     # RE search
609                     break
610                 elif t == 3 and node[k][:len(v)] != v:
611                     # start anchored
612                     break
613                 elif t == 4 and node[k][-len(v):] != v:
614                     # end anchored
615                     break
616                 elif t == 5 and node[k].find(v) == -1:
617                     # substring search
618                     break
619                 elif t == 6 and node[k] != v:
620                     # straight value comparison for the other types
621                     break
622             else:
623                 l.append((nodeid, node))
624         l.sort()
625         cldb.close()
627         # optimise sort
628         m = []
629         for entry in sort:
630             if entry[0] != '-':
631                 m.append(('+', entry))
632             else:
633                 m.append((entry[0], entry[1:]))
634         sort = m
636         # optimise group
637         m = []
638         for entry in group:
639             if entry[0] != '-':
640                 m.append(('+', entry))
641             else:
642                 m.append((entry[0], entry[1:]))
643         group = m
644         # now, sort the result
645         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
646                 db = self.db, cl=self):
647             a_id, an = a
648             b_id, bn = b
649             # sort by group and then sort
650             for list in group, sort:
651                 for dir, prop in list:
652                     # handle the properties that might be "faked"
653                     if not an.has_key(prop):
654                         an[prop] = cl.get(a_id, prop)
655                     av = an[prop]
656                     if not bn.has_key(prop):
657                         bn[prop] = cl.get(b_id, prop)
658                     bv = bn[prop]
660                     # sorting is class-specific
661                     propclass = properties[prop]
663                     # String and Date values are sorted in the natural way
664                     if propclass.isStringType:
665                         # clean up the strings
666                         if av and av[0] in string.uppercase:
667                             av = an[prop] = av.lower()
668                         if bv and bv[0] in string.uppercase:
669                             bv = bn[prop] = bv.lower()
670                     if propclass.isStringType or propclass.isDateType:
671                         if dir == '+':
672                             r = cmp(av, bv)
673                             if r != 0: return r
674                         elif dir == '-':
675                             r = cmp(bv, av)
676                             if r != 0: return r
678                     # Link properties are sorted according to the value of
679                     # the "order" property on the linked nodes if it is
680                     # present; or otherwise on the key string of the linked
681                     # nodes; or finally on  the node ids.
682                     elif propclass.isLinkType:
683                         link = db.classes[propclass.classname]
684                         if av is None and bv is not None: return -1
685                         if av is not None and bv is None: return 1
686                         if av is None and bv is None: return 0
687                         if link.getprops().has_key('order'):
688                             if dir == '+':
689                                 r = cmp(link.get(av, 'order'),
690                                     link.get(bv, 'order'))
691                                 if r != 0: return r
692                             elif dir == '-':
693                                 r = cmp(link.get(bv, 'order'),
694                                     link.get(av, 'order'))
695                                 if r != 0: return r
696                         elif link.getkey():
697                             key = link.getkey()
698                             if dir == '+':
699                                 r = cmp(link.get(av, key), link.get(bv, key))
700                                 if r != 0: return r
701                             elif dir == '-':
702                                 r = cmp(link.get(bv, key), link.get(av, key))
703                                 if r != 0: return r
704                         else:
705                             if dir == '+':
706                                 r = cmp(av, bv)
707                                 if r != 0: return r
708                             elif dir == '-':
709                                 r = cmp(bv, av)
710                                 if r != 0: return r
712                     # Multilink properties are sorted according to how many
713                     # links are present.
714                     elif propclass.isMultilinkType:
715                         if dir == '+':
716                             r = cmp(len(av), len(bv))
717                             if r != 0: return r
718                         elif dir == '-':
719                             r = cmp(len(bv), len(av))
720                             if r != 0: return r
721                 # end for dir, prop in list:
722             # end for list in sort, group:
723             # if all else fails, compare the ids
724             return cmp(a[0], b[0])
726         l.sort(sortfun)
727         return [i[0] for i in l]
729     def count(self):
730         """Get the number of nodes in this class.
732         If the returned integer is 'numnodes', the ids of all the nodes
733         in this class run from 1 to numnodes, and numnodes+1 will be the
734         id of the next node to be created in this class.
735         """
736         return self.db.countnodes(self.classname)
738     # Manipulating properties:
740     def getprops(self):
741         """Return a dictionary mapping property names to property objects."""
742         d = self.properties.copy()
743         d['id'] = String()
744         return d
746     def addprop(self, **properties):
747         """Add properties to this class.
749         The keyword arguments in 'properties' must map names to property
750         objects, or a TypeError is raised.  None of the keys in 'properties'
751         may collide with the names of existing properties, or a ValueError
752         is raised before any properties have been added.
753         """
754         for key in properties.keys():
755             if self.properties.has_key(key):
756                 raise ValueError, key
757         self.properties.update(properties)
760 # XXX not in spec
761 class Node:
762     ''' A convenience wrapper for the given node
763     '''
764     def __init__(self, cl, nodeid):
765         self.__dict__['cl'] = cl
766         self.__dict__['nodeid'] = nodeid
767     def keys(self):
768         return self.cl.getprops().keys()
769     def has_key(self, name):
770         return self.cl.getprops().has_key(name)
771     def __getattr__(self, name):
772         if self.__dict__.has_key(name):
773             return self.__dict__['name']
774         try:
775             return self.cl.get(self.nodeid, name)
776         except KeyError, value:
777             raise AttributeError, str(value)
778     def __getitem__(self, name):
779         return self.cl.get(self.nodeid, name)
780     def __setattr__(self, name, value):
781         try:
782             return self.cl.set(self.nodeid, **{name: value})
783         except KeyError, value:
784             raise AttributeError, str(value)
785     def __setitem__(self, name, value):
786         self.cl.set(self.nodeid, **{name: value})
787     def history(self):
788         return self.cl.history(self.nodeid)
789     def retire(self):
790         return self.cl.retire(self.nodeid)
793 def Choice(name, *options):
794     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
795     for i in range(len(options)):
796         cl.create(name=option[i], order=i)
797     return hyperdb.Link(name)
800 # $Log: not supported by cvs2svn $
801 # Revision 1.10  2001/07/30 02:38:31  richard
802 # get() now has a default arg - for migration only.
804 # Revision 1.9  2001/07/29 09:28:23  richard
805 # Fixed sorting by clicking on column headings.
807 # Revision 1.8  2001/07/29 08:27:40  richard
808 # Fixed handling of passed-in values in form elements (ie. during a
809 # drill-down)
811 # Revision 1.7  2001/07/29 07:01:39  richard
812 # Added vim command to all source so that we don't get no steenkin' tabs :)
814 # Revision 1.6  2001/07/29 05:36:14  richard
815 # Cleanup of the link label generation.
817 # Revision 1.5  2001/07/29 04:05:37  richard
818 # Added the fabricated property "id".
820 # Revision 1.4  2001/07/27 06:25:35  richard
821 # Fixed some of the exceptions so they're the right type.
822 # Removed the str()-ification of node ids so we don't mask oopsy errors any
823 # more.
825 # Revision 1.3  2001/07/27 05:17:14  richard
826 # just some comments
828 # Revision 1.2  2001/07/22 12:09:32  richard
829 # Final commit of Grande Splite
831 # Revision 1.1  2001/07/22 11:58:35  richard
832 # More Grande Splite
835 # vim: set filetype=python ts=4 sw=4 et si