Code

c225e6ac7f7ff7d34bfac7fb1d4862d4484c6ccf
[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, default_to_id=0):
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         if default_to_id:
417             return 'id'
418         props = props.keys()
419         props.sort()
420         return props[0]
422     # TODO: set up a separate index db file for this? profile?
423     def lookup(self, keyvalue):
424         """Locate a particular node by its key property and return its id.
426         If this class has no key property, a TypeError is raised.  If the
427         'keyvalue' matches one of the values for the key property among
428         the nodes in this class, the matching node's id is returned;
429         otherwise a KeyError is raised.
430         """
431         cldb = self.db.getclassdb(self.classname)
432         for nodeid in self.db.getnodeids(self.classname, cldb):
433             node = self.db.getnode(self.classname, nodeid, cldb)
434             if node.has_key(self.db.RETIRED_FLAG):
435                 continue
436             if node[self.key] == keyvalue:
437                 return nodeid
438         cldb.close()
439         raise KeyError, keyvalue
441     # XXX: change from spec - allows multiple props to match
442     def find(self, **propspec):
443         """Get the ids of nodes in this class which link to a given node.
445         'propspec' consists of keyword args propname=nodeid   
446           'propname' must be the name of a property in this class, or a
447             KeyError is raised.  That property must be a Link or Multilink
448             property, or a TypeError is raised.
450           'nodeid' must be the id of an existing node in the class linked
451             to by the given property, or an IndexError is raised.
452         """
453         propspec = propspec.items()
454         for propname, nodeid in propspec:
455 #            nodeid = str(nodeid)
456             # check the prop is OK
457             prop = self.properties[propname]
458             if not prop.isLinkType and not prop.isMultilinkType:
459                 raise TypeError, "'%s' not a Link/Multilink property"%propname
460             if not self.db.hasnode(prop.classname, nodeid):
461                 raise ValueError, '%s has no node %s'%(link_class, nodeid)
463         # ok, now do the find
464         cldb = self.db.getclassdb(self.classname)
465         l = []
466         for id in self.db.getnodeids(self.classname, cldb):
467             node = self.db.getnode(self.classname, id, cldb)
468             if node.has_key(self.db.RETIRED_FLAG):
469                 continue
470             for propname, nodeid in propspec:
471 #                nodeid = str(nodeid)
472                 property = node[propname]
473                 if prop.isLinkType and nodeid == property:
474                     l.append(id)
475                 elif prop.isMultilinkType and nodeid in property:
476                     l.append(id)
477         cldb.close()
478         return l
480     def stringFind(self, **requirements):
481         """Locate a particular node by matching a set of its String properties.
483         If the property is not a String property, a TypeError is raised.
484         
485         The return is a list of the id of all nodes that match.
486         """
487         for propname in requirements.keys():
488             prop = self.properties[propname]
489             if not prop.isStringType:
490                 raise TypeError, "'%s' not a String property"%propname
491         l = []
492         cldb = self.db.getclassdb(self.classname)
493         for nodeid in self.db.getnodeids(self.classname, cldb):
494             node = self.db.getnode(self.classname, nodeid, cldb)
495             if node.has_key(self.db.RETIRED_FLAG):
496                 continue
497             for key, value in requirements.items():
498                 if node[key] != value:
499                     break
500             else:
501                 l.append(nodeid)
502         cldb.close()
503         return l
505     def list(self):
506         """Return a list of the ids of the active nodes in this class."""
507         l = []
508         cn = self.classname
509         cldb = self.db.getclassdb(cn)
510         for nodeid in self.db.getnodeids(cn, cldb):
511             node = self.db.getnode(cn, nodeid, cldb)
512             if node.has_key(self.db.RETIRED_FLAG):
513                 continue
514             l.append(nodeid)
515         l.sort()
516         cldb.close()
517         return l
519     # XXX not in spec
520     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
521         ''' Return a list of the ids of the active nodes in this class that
522             match the 'filter' spec, sorted by the group spec and then the
523             sort spec
524         '''
525         cn = self.classname
527         # optimise filterspec
528         l = []
529         props = self.getprops()
530         for k, v in filterspec.items():
531             propclass = props[k]
532             if propclass.isLinkType:
533                 if type(v) is not type([]):
534                     v = [v]
535                 # replace key values with node ids
536                 u = []
537                 link_class =  self.db.classes[propclass.classname]
538                 for entry in v:
539                     if not num_re.match(entry):
540                         try:
541                             entry = link_class.lookup(entry)
542                         except:
543                             raise ValueError, 'new property "%s": %s not a %s'%(
544                                 k, entry, self.properties[k].classname)
545                     u.append(entry)
547                 l.append((0, k, u))
548             elif propclass.isMultilinkType:
549                 if type(v) is not type([]):
550                     v = [v]
551                 # replace key values with node ids
552                 u = []
553                 link_class =  self.db.classes[propclass.classname]
554                 for entry in v:
555                     if not num_re.match(entry):
556                         try:
557                             entry = link_class.lookup(entry)
558                         except:
559                             raise ValueError, 'new property "%s": %s not a %s'%(
560                                 k, entry, self.properties[k].classname)
561                     u.append(entry)
562                 l.append((1, k, u))
563             elif propclass.isStringType:
564                 if '*' in v or '?' in v:
565                     # simple glob searching
566                     v = v.replace('?', '.')
567                     v = v.replace('*', '.*?')
568                     v = re.compile(v)
569                     l.append((2, k, v))
570                 elif v[0] == '^':
571                     # start-anchored
572                     if v[-1] == '$':
573                         # _and_ end-anchored
574                         l.append((6, k, v[1:-1]))
575                     l.append((3, k, v[1:]))
576                 elif v[-1] == '$':
577                     # end-anchored
578                     l.append((4, k, v[:-1]))
579                 else:
580                     # substring
581                     l.append((5, k, v))
582             else:
583                 l.append((6, k, v))
584         filterspec = l
586         # now, find all the nodes that are active and pass filtering
587         l = []
588         cldb = self.db.getclassdb(cn)
589         for nodeid in self.db.getnodeids(cn, cldb):
590             node = self.db.getnode(cn, nodeid, cldb)
591             if node.has_key(self.db.RETIRED_FLAG):
592                 continue
593             # apply filter
594             for t, k, v in filterspec:
595                 if t == 0 and node[k] not in v:
596                     # link - if this node'd property doesn't appear in the
597                     # filterspec's nodeid list, skip it
598                     break
599                 elif t == 1:
600                     # multilink - if any of the nodeids required by the
601                     # filterspec aren't in this node's property, then skip
602                     # it
603                     for value in v:
604                         if value not in node[k]:
605                             break
606                     else:
607                         continue
608                     break
609                 elif t == 2 and not v.search(node[k]):
610                     # RE search
611                     break
612                 elif t == 3 and node[k][:len(v)] != v:
613                     # start anchored
614                     break
615                 elif t == 4 and node[k][-len(v):] != v:
616                     # end anchored
617                     break
618                 elif t == 5 and node[k].find(v) == -1:
619                     # substring search
620                     break
621                 elif t == 6 and node[k] != v:
622                     # straight value comparison for the other types
623                     break
624             else:
625                 l.append((nodeid, node))
626         l.sort()
627         cldb.close()
629         # optimise sort
630         m = []
631         for entry in sort:
632             if entry[0] != '-':
633                 m.append(('+', entry))
634             else:
635                 m.append((entry[0], entry[1:]))
636         sort = m
638         # optimise group
639         m = []
640         for entry in group:
641             if entry[0] != '-':
642                 m.append(('+', entry))
643             else:
644                 m.append((entry[0], entry[1:]))
645         group = m
646         # now, sort the result
647         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
648                 db = self.db, cl=self):
649             a_id, an = a
650             b_id, bn = b
651             # sort by group and then sort
652             for list in group, sort:
653                 for dir, prop in list:
654                     # handle the properties that might be "faked"
655                     if not an.has_key(prop):
656                         an[prop] = cl.get(a_id, prop)
657                     av = an[prop]
658                     if not bn.has_key(prop):
659                         bn[prop] = cl.get(b_id, prop)
660                     bv = bn[prop]
662                     # sorting is class-specific
663                     propclass = properties[prop]
665                     # String and Date values are sorted in the natural way
666                     if propclass.isStringType:
667                         # clean up the strings
668                         if av and av[0] in string.uppercase:
669                             av = an[prop] = av.lower()
670                         if bv and bv[0] in string.uppercase:
671                             bv = bn[prop] = bv.lower()
672                     if propclass.isStringType or propclass.isDateType:
673                         if dir == '+':
674                             r = cmp(av, bv)
675                             if r != 0: return r
676                         elif dir == '-':
677                             r = cmp(bv, av)
678                             if r != 0: return r
680                     # Link properties are sorted according to the value of
681                     # the "order" property on the linked nodes if it is
682                     # present; or otherwise on the key string of the linked
683                     # nodes; or finally on  the node ids.
684                     elif propclass.isLinkType:
685                         link = db.classes[propclass.classname]
686                         if av is None and bv is not None: return -1
687                         if av is not None and bv is None: return 1
688                         if av is None and bv is None: return 0
689                         if link.getprops().has_key('order'):
690                             if dir == '+':
691                                 r = cmp(link.get(av, 'order'),
692                                     link.get(bv, 'order'))
693                                 if r != 0: return r
694                             elif dir == '-':
695                                 r = cmp(link.get(bv, 'order'),
696                                     link.get(av, 'order'))
697                                 if r != 0: return r
698                         elif link.getkey():
699                             key = link.getkey()
700                             if dir == '+':
701                                 r = cmp(link.get(av, key), link.get(bv, key))
702                                 if r != 0: return r
703                             elif dir == '-':
704                                 r = cmp(link.get(bv, key), link.get(av, key))
705                                 if r != 0: return r
706                         else:
707                             if dir == '+':
708                                 r = cmp(av, bv)
709                                 if r != 0: return r
710                             elif dir == '-':
711                                 r = cmp(bv, av)
712                                 if r != 0: return r
714                     # Multilink properties are sorted according to how many
715                     # links are present.
716                     elif propclass.isMultilinkType:
717                         if dir == '+':
718                             r = cmp(len(av), len(bv))
719                             if r != 0: return r
720                         elif dir == '-':
721                             r = cmp(len(bv), len(av))
722                             if r != 0: return r
723                 # end for dir, prop in list:
724             # end for list in sort, group:
725             # if all else fails, compare the ids
726             return cmp(a[0], b[0])
728         l.sort(sortfun)
729         return [i[0] for i in l]
731     def count(self):
732         """Get the number of nodes in this class.
734         If the returned integer is 'numnodes', the ids of all the nodes
735         in this class run from 1 to numnodes, and numnodes+1 will be the
736         id of the next node to be created in this class.
737         """
738         return self.db.countnodes(self.classname)
740     # Manipulating properties:
742     def getprops(self):
743         """Return a dictionary mapping property names to property objects."""
744         d = self.properties.copy()
745         d['id'] = String()
746         return d
748     def addprop(self, **properties):
749         """Add properties to this class.
751         The keyword arguments in 'properties' must map names to property
752         objects, or a TypeError is raised.  None of the keys in 'properties'
753         may collide with the names of existing properties, or a ValueError
754         is raised before any properties have been added.
755         """
756         for key in properties.keys():
757             if self.properties.has_key(key):
758                 raise ValueError, key
759         self.properties.update(properties)
762 # XXX not in spec
763 class Node:
764     ''' A convenience wrapper for the given node
765     '''
766     def __init__(self, cl, nodeid):
767         self.__dict__['cl'] = cl
768         self.__dict__['nodeid'] = nodeid
769     def keys(self):
770         return self.cl.getprops().keys()
771     def has_key(self, name):
772         return self.cl.getprops().has_key(name)
773     def __getattr__(self, name):
774         if self.__dict__.has_key(name):
775             return self.__dict__['name']
776         try:
777             return self.cl.get(self.nodeid, name)
778         except KeyError, value:
779             raise AttributeError, str(value)
780     def __getitem__(self, name):
781         return self.cl.get(self.nodeid, name)
782     def __setattr__(self, name, value):
783         try:
784             return self.cl.set(self.nodeid, **{name: value})
785         except KeyError, value:
786             raise AttributeError, str(value)
787     def __setitem__(self, name, value):
788         self.cl.set(self.nodeid, **{name: value})
789     def history(self):
790         return self.cl.history(self.nodeid)
791     def retire(self):
792         return self.cl.retire(self.nodeid)
795 def Choice(name, *options):
796     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
797     for i in range(len(options)):
798         cl.create(name=option[i], order=i)
799     return hyperdb.Link(name)
802 # $Log: not supported by cvs2svn $
803 # Revision 1.11  2001/08/01 04:24:21  richard
804 # mailgw was assuming certain properties existed on the issues being created.
806 # Revision 1.10  2001/07/30 02:38:31  richard
807 # get() now has a default arg - for migration only.
809 # Revision 1.9  2001/07/29 09:28:23  richard
810 # Fixed sorting by clicking on column headings.
812 # Revision 1.8  2001/07/29 08:27:40  richard
813 # Fixed handling of passed-in values in form elements (ie. during a
814 # drill-down)
816 # Revision 1.7  2001/07/29 07:01:39  richard
817 # Added vim command to all source so that we don't get no steenkin' tabs :)
819 # Revision 1.6  2001/07/29 05:36:14  richard
820 # Cleanup of the link label generation.
822 # Revision 1.5  2001/07/29 04:05:37  richard
823 # Added the fabricated property "id".
825 # Revision 1.4  2001/07/27 06:25:35  richard
826 # Fixed some of the exceptions so they're the right type.
827 # Removed the str()-ification of node ids so we don't mask oopsy errors any
828 # more.
830 # Revision 1.3  2001/07/27 05:17:14  richard
831 # just some comments
833 # Revision 1.2  2001/07/22 12:09:32  richard
834 # Final commit of Grande Splite
836 # Revision 1.1  2001/07/22 11:58:35  richard
837 # More Grande Splite
840 # vim: set filetype=python ts=4 sw=4 et si