1 #
2 # Copyright (c) 2001 Bizar Software Pty Ltd (http://www.bizarsoftware.com.au/)
3 # This module is free software, and you may redistribute it and/or modify
4 # under the same terms as Python, so long as this copyright message and
5 # disclaimer are retained in their original form.
6 #
7 # IN NO EVENT SHALL BIZAR SOFTWARE PTY LTD BE LIABLE TO ANY PARTY FOR
8 # DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
9 # OUT OF THE USE OF THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE
10 # POSSIBILITY OF SUCH DAMAGE.
11 #
12 # BIZAR SOFTWARE PTY LTD SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
14 # FOR A PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS"
15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 #
18 # $Id: hyperdb.py,v 1.19 2001-08-29 04:47:18 richard Exp $
20 # standard python modules
21 import cPickle, re, string
23 # roundup modules
24 import date
27 #
28 # Types
29 #
30 class String:
31 """An object designating a String property."""
32 def __repr__(self):
33 return '<%s>'%self.__class__
35 class Date:
36 """An object designating a Date property."""
37 def __repr__(self):
38 return '<%s>'%self.__class__
40 class Interval:
41 """An object designating an Interval property."""
42 def __repr__(self):
43 return '<%s>'%self.__class__
45 class Link:
46 """An object designating a Link property that links to a
47 node in a specified class."""
48 def __init__(self, classname):
49 self.classname = classname
50 def __repr__(self):
51 return '<%s to "%s">'%(self.__class__, self.classname)
53 class Multilink:
54 """An object designating a Multilink property that links
55 to nodes in a specified class.
56 """
57 def __init__(self, classname):
58 self.classname = classname
59 def __repr__(self):
60 return '<%s to "%s">'%(self.__class__, self.classname)
62 class DatabaseError(ValueError):
63 pass
66 #
67 # the base Database class
68 #
69 class Database:
70 # flag to set on retired entries
71 RETIRED_FLAG = '__hyperdb_retired'
74 _marker = []
75 #
76 # The base Class class
77 #
78 class Class:
79 """The handle to a particular class of nodes in a hyperdatabase."""
81 def __init__(self, db, classname, **properties):
82 """Create a new class with a given name and property specification.
84 'classname' must not collide with the name of an existing class,
85 or a ValueError is raised. The keyword arguments in 'properties'
86 must map names to property objects, or a TypeError is raised.
87 """
88 self.classname = classname
89 self.properties = properties
90 self.db = db
91 self.key = ''
93 # do the db-related init stuff
94 db.addclass(self)
96 # Editing nodes:
98 def create(self, **propvalues):
99 """Create a new node of this class and return its id.
101 The keyword arguments in 'propvalues' map property names to values.
103 The values of arguments must be acceptable for the types of their
104 corresponding properties or a TypeError is raised.
106 If this class has a key property, it must be present and its value
107 must not collide with other key strings or a ValueError is raised.
109 Any other properties on this class that are missing from the
110 'propvalues' dictionary are set to None.
112 If an id in a link or multilink property does not refer to a valid
113 node, an IndexError is raised.
114 """
115 if propvalues.has_key('id'):
116 raise KeyError, '"id" is reserved'
118 if self.db.journaltag is None:
119 raise DatabaseError, 'Database open read-only'
121 # new node's id
122 newid = str(self.count() + 1)
124 # validate propvalues
125 num_re = re.compile('^\d+$')
126 for key, value in propvalues.items():
127 if key == self.key:
128 try:
129 self.lookup(value)
130 except KeyError:
131 pass
132 else:
133 raise ValueError, 'node with key "%s" exists'%value
135 # try to handle this property
136 try:
137 prop = self.properties[key]
138 except KeyError:
139 raise KeyError, '"%s" has no property "%s"'%(self.classname,
140 key)
142 if isinstance(prop, Link):
143 if type(value) != type(''):
144 raise ValueError, 'link value must be String'
145 link_class = self.properties[key].classname
146 # if it isn't a number, it's a key
147 if not num_re.match(value):
148 try:
149 value = self.db.classes[link_class].lookup(value)
150 except:
151 raise IndexError, 'new property "%s": %s not a %s'%(
152 key, value, self.properties[key].classname)
153 propvalues[key] = value
154 if not self.db.hasnode(link_class, value):
155 raise IndexError, '%s has no node %s'%(link_class, value)
157 # register the link with the newly linked node
158 self.db.addjournal(link_class, value, 'link',
159 (self.classname, newid, key))
161 elif isinstance(prop, Multilink):
162 if type(value) != type([]):
163 raise TypeError, 'new property "%s" not a list of ids'%key
164 link_class = self.properties[key].classname
165 l = []
166 for entry in value:
167 if type(entry) != type(''):
168 raise ValueError, 'link value must be String'
169 # if it isn't a number, it's a key
170 if not num_re.match(entry):
171 try:
172 entry = self.db.classes[link_class].lookup(entry)
173 except:
174 raise IndexError, 'new property "%s": %s not a %s'%(
175 key, entry, self.properties[key].classname)
176 l.append(entry)
177 value = l
178 propvalues[key] = value
180 # handle additions
181 for id in value:
182 if not self.db.hasnode(link_class, id):
183 raise IndexError, '%s has no node %s'%(link_class, id)
184 # register the link with the newly linked node
185 self.db.addjournal(link_class, id, 'link',
186 (self.classname, newid, key))
188 elif isinstance(prop, String):
189 if type(value) != type(''):
190 raise TypeError, 'new property "%s" not a string'%key
192 elif isinstance(prop, Date):
193 if not isinstance(value, date.Date):
194 raise TypeError, 'new property "%s" not a Date'% key
196 elif isinstance(prop, Interval):
197 if not isinstance(value, date.Interval):
198 raise TypeError, 'new property "%s" not an Interval'% key
200 for key, prop in self.properties.items():
201 if propvalues.has_key(key):
202 continue
203 if isinstance(prop, Multilink):
204 propvalues[key] = []
205 else:
206 propvalues[key] = None
208 # done
209 self.db.addnode(self.classname, newid, propvalues)
210 self.db.addjournal(self.classname, newid, 'create', propvalues)
211 return newid
213 def get(self, nodeid, propname, default=_marker):
214 """Get the value of a property on an existing node of this class.
216 'nodeid' must be the id of an existing node of this class or an
217 IndexError is raised. 'propname' must be the name of a property
218 of this class or a KeyError is raised.
219 """
220 if propname == 'id':
221 return nodeid
222 d = self.db.getnode(self.classname, nodeid)
223 if not d.has_key(propname) and default is not _marker:
224 return default
225 return d[propname]
227 # XXX not in spec
228 def getnode(self, nodeid):
229 ''' Return a convenience wrapper for the node
230 '''
231 return Node(self, nodeid)
233 def set(self, nodeid, **propvalues):
234 """Modify a property on an existing node of this class.
236 'nodeid' must be the id of an existing node of this class or an
237 IndexError is raised.
239 Each key in 'propvalues' must be the name of a property of this
240 class or a KeyError is raised.
242 All values in 'propvalues' must be acceptable types for their
243 corresponding properties or a TypeError is raised.
245 If the value of the key property is set, it must not collide with
246 other key strings or a ValueError is raised.
248 If the value of a Link or Multilink property contains an invalid
249 node id, a ValueError is raised.
250 """
251 if not propvalues:
252 return
254 if propvalues.has_key('id'):
255 raise KeyError, '"id" is reserved'
257 if self.db.journaltag is None:
258 raise DatabaseError, 'Database open read-only'
260 node = self.db.getnode(self.classname, nodeid)
261 if node.has_key(self.db.RETIRED_FLAG):
262 raise IndexError
263 num_re = re.compile('^\d+$')
264 for key, value in propvalues.items():
265 if not node.has_key(key):
266 raise KeyError, key
268 # check to make sure we're not duplicating an existing key
269 if key == self.key and node[key] != value:
270 try:
271 self.lookup(value)
272 except KeyError:
273 pass
274 else:
275 raise ValueError, 'node with key "%s" exists'%value
277 prop = self.properties[key]
279 if isinstance(prop, Link):
280 link_class = self.properties[key].classname
281 # if it isn't a number, it's a key
282 if type(value) != type(''):
283 raise ValueError, 'link value must be String'
284 if not num_re.match(value):
285 try:
286 value = self.db.classes[link_class].lookup(value)
287 except:
288 raise IndexError, 'new property "%s": %s not a %s'%(
289 key, value, self.properties[key].classname)
291 if not self.db.hasnode(link_class, value):
292 raise IndexError, '%s has no node %s'%(link_class, value)
294 # register the unlink with the old linked node
295 if node[key] is not None:
296 self.db.addjournal(link_class, node[key], 'unlink',
297 (self.classname, nodeid, key))
299 # register the link with the newly linked node
300 if value is not None:
301 self.db.addjournal(link_class, value, 'link',
302 (self.classname, nodeid, key))
304 elif isinstance(prop, Multilink):
305 if type(value) != type([]):
306 raise TypeError, 'new property "%s" not a list of ids'%key
307 link_class = self.properties[key].classname
308 l = []
309 for entry in value:
310 # if it isn't a number, it's a key
311 if type(entry) != type(''):
312 raise ValueError, 'link value must be String'
313 if not num_re.match(entry):
314 try:
315 entry = self.db.classes[link_class].lookup(entry)
316 except:
317 raise IndexError, 'new property "%s": %s not a %s'%(
318 key, entry, self.properties[key].classname)
319 l.append(entry)
320 value = l
321 propvalues[key] = value
323 #handle removals
324 l = node[key]
325 for id in l[:]:
326 if id in value:
327 continue
328 # register the unlink with the old linked node
329 self.db.addjournal(link_class, id, 'unlink',
330 (self.classname, nodeid, key))
331 l.remove(id)
333 # handle additions
334 for id in value:
335 if not self.db.hasnode(link_class, id):
336 raise IndexError, '%s has no node %s'%(link_class, id)
337 if id in l:
338 continue
339 # register the link with the newly linked node
340 self.db.addjournal(link_class, id, 'link',
341 (self.classname, nodeid, key))
342 l.append(id)
344 elif isinstance(prop, String):
345 if value is not None and type(value) != type(''):
346 raise TypeError, 'new property "%s" not a string'%key
348 elif isinstance(prop, Date):
349 if not isinstance(value, date.Date):
350 raise TypeError, 'new property "%s" not a Date'% key
352 elif isinstance(prop, Interval):
353 if not isinstance(value, date.Interval):
354 raise TypeError, 'new property "%s" not an Interval'% key
356 node[key] = value
358 self.db.setnode(self.classname, nodeid, node)
359 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
361 def retire(self, nodeid):
362 """Retire a node.
364 The properties on the node remain available from the get() method,
365 and the node's id is never reused.
367 Retired nodes are not returned by the find(), list(), or lookup()
368 methods, and other nodes may reuse the values of their key properties.
369 """
370 if self.db.journaltag is None:
371 raise DatabaseError, 'Database open read-only'
372 node = self.db.getnode(self.classname, nodeid)
373 node[self.db.RETIRED_FLAG] = 1
374 self.db.setnode(self.classname, nodeid, node)
375 self.db.addjournal(self.classname, nodeid, 'retired', None)
377 def history(self, nodeid):
378 """Retrieve the journal of edits on a particular node.
380 'nodeid' must be the id of an existing node of this class or an
381 IndexError is raised.
383 The returned list contains tuples of the form
385 (date, tag, action, params)
387 'date' is a Timestamp object specifying the time of the change and
388 'tag' is the journaltag specified when the database was opened.
389 """
390 return self.db.getjournal(self.classname, nodeid)
392 # Locating nodes:
394 def setkey(self, propname):
395 """Select a String property of this class to be the key property.
397 'propname' must be the name of a String property of this class or
398 None, or a TypeError is raised. The values of the key property on
399 all existing nodes must be unique or a ValueError is raised.
400 """
401 self.key = propname
403 def getkey(self):
404 """Return the name of the key property for this class or None."""
405 return self.key
407 def labelprop(self, default_to_id=0):
408 ''' Return the property name for a label for the given node.
410 This method attempts to generate a consistent label for the node.
411 It tries the following in order:
412 1. key property
413 2. "name" property
414 3. "title" property
415 4. first property from the sorted property name list
416 '''
417 k = self.getkey()
418 if k:
419 return k
420 props = self.getprops()
421 if props.has_key('name'):
422 return 'name'
423 elif props.has_key('title'):
424 return 'title'
425 if default_to_id:
426 return 'id'
427 props = props.keys()
428 props.sort()
429 return props[0]
431 # TODO: set up a separate index db file for this? profile?
432 def lookup(self, keyvalue):
433 """Locate a particular node by its key property and return its id.
435 If this class has no key property, a TypeError is raised. If the
436 'keyvalue' matches one of the values for the key property among
437 the nodes in this class, the matching node's id is returned;
438 otherwise a KeyError is raised.
439 """
440 cldb = self.db.getclassdb(self.classname)
441 for nodeid in self.db.getnodeids(self.classname, cldb):
442 node = self.db.getnode(self.classname, nodeid, cldb)
443 if node.has_key(self.db.RETIRED_FLAG):
444 continue
445 if node[self.key] == keyvalue:
446 return nodeid
447 cldb.close()
448 raise KeyError, keyvalue
450 # XXX: change from spec - allows multiple props to match
451 def find(self, **propspec):
452 """Get the ids of nodes in this class which link to a given node.
454 'propspec' consists of keyword args propname=nodeid
455 'propname' must be the name of a property in this class, or a
456 KeyError is raised. That property must be a Link or Multilink
457 property, or a TypeError is raised.
459 'nodeid' must be the id of an existing node in the class linked
460 to by the given property, or an IndexError is raised.
461 """
462 propspec = propspec.items()
463 for propname, nodeid in propspec:
464 # check the prop is OK
465 prop = self.properties[propname]
466 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
467 raise TypeError, "'%s' not a Link/Multilink property"%propname
468 if not self.db.hasnode(prop.classname, nodeid):
469 raise ValueError, '%s has no node %s'%(link_class, nodeid)
471 # ok, now do the find
472 cldb = self.db.getclassdb(self.classname)
473 l = []
474 for id in self.db.getnodeids(self.classname, cldb):
475 node = self.db.getnode(self.classname, id, cldb)
476 if node.has_key(self.db.RETIRED_FLAG):
477 continue
478 for propname, nodeid in propspec:
479 property = node[propname]
480 if isinstance(prop, Link) and nodeid == property:
481 l.append(id)
482 elif isinstance(prop, Multilink) and nodeid in property:
483 l.append(id)
484 cldb.close()
485 return l
487 def stringFind(self, **requirements):
488 """Locate a particular node by matching a set of its String properties.
490 If the property is not a String property, a TypeError is raised.
492 The return is a list of the id of all nodes that match.
493 """
494 for propname in requirements.keys():
495 prop = self.properties[propname]
496 if isinstance(not prop, String):
497 raise TypeError, "'%s' not a String property"%propname
498 l = []
499 cldb = self.db.getclassdb(self.classname)
500 for nodeid in self.db.getnodeids(self.classname, cldb):
501 node = self.db.getnode(self.classname, nodeid, cldb)
502 if node.has_key(self.db.RETIRED_FLAG):
503 continue
504 for key, value in requirements.items():
505 if node[key] != value:
506 break
507 else:
508 l.append(nodeid)
509 cldb.close()
510 return l
512 def list(self):
513 """Return a list of the ids of the active nodes in this class."""
514 l = []
515 cn = self.classname
516 cldb = self.db.getclassdb(cn)
517 for nodeid in self.db.getnodeids(cn, cldb):
518 node = self.db.getnode(cn, nodeid, cldb)
519 if node.has_key(self.db.RETIRED_FLAG):
520 continue
521 l.append(nodeid)
522 l.sort()
523 cldb.close()
524 return l
526 # XXX not in spec
527 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
528 ''' Return a list of the ids of the active nodes in this class that
529 match the 'filter' spec, sorted by the group spec and then the
530 sort spec
531 '''
532 cn = self.classname
534 # optimise filterspec
535 l = []
536 props = self.getprops()
537 for k, v in filterspec.items():
538 propclass = props[k]
539 if isinstance(propclass, Link):
540 if type(v) is not type([]):
541 v = [v]
542 # replace key values with node ids
543 u = []
544 link_class = self.db.classes[propclass.classname]
545 for entry in v:
546 if not num_re.match(entry):
547 try:
548 entry = link_class.lookup(entry)
549 except:
550 raise ValueError, 'new property "%s": %s not a %s'%(
551 k, entry, self.properties[k].classname)
552 u.append(entry)
554 l.append((0, k, u))
555 elif isinstance(propclass, Multilink):
556 if type(v) is not type([]):
557 v = [v]
558 # replace key values with node ids
559 u = []
560 link_class = self.db.classes[propclass.classname]
561 for entry in v:
562 if not num_re.match(entry):
563 try:
564 entry = link_class.lookup(entry)
565 except:
566 raise ValueError, 'new property "%s": %s not a %s'%(
567 k, entry, self.properties[k].classname)
568 u.append(entry)
569 l.append((1, k, u))
570 elif isinstance(propclass, String):
571 # simple glob searching
572 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
573 v = v.replace('?', '.')
574 v = v.replace('*', '.*?')
575 l.append((2, k, re.compile(v, re.I)))
576 else:
577 l.append((6, k, v))
578 filterspec = l
580 # now, find all the nodes that are active and pass filtering
581 l = []
582 cldb = self.db.getclassdb(cn)
583 for nodeid in self.db.getnodeids(cn, cldb):
584 node = self.db.getnode(cn, nodeid, cldb)
585 if node.has_key(self.db.RETIRED_FLAG):
586 continue
587 # apply filter
588 for t, k, v in filterspec:
589 if t == 0 and node[k] not in v:
590 # link - if this node'd property doesn't appear in the
591 # filterspec's nodeid list, skip it
592 break
593 elif t == 1:
594 # multilink - if any of the nodeids required by the
595 # filterspec aren't in this node's property, then skip
596 # it
597 for value in v:
598 if value not in node[k]:
599 break
600 else:
601 continue
602 break
603 elif t == 2 and not v.search(node[k]):
604 # RE search
605 break
606 # elif t == 3 and node[k][:len(v)] != v:
607 # # start anchored
608 # break
609 # elif t == 4 and node[k][-len(v):] != v:
610 # # end anchored
611 # break
612 # elif t == 5 and node[k].find(v) == -1:
613 # # substring search
614 # break
615 elif t == 6 and node[k] != v:
616 # straight value comparison for the other types
617 break
618 else:
619 l.append((nodeid, node))
620 l.sort()
621 cldb.close()
623 # optimise sort
624 m = []
625 for entry in sort:
626 if entry[0] != '-':
627 m.append(('+', entry))
628 else:
629 m.append((entry[0], entry[1:]))
630 sort = m
632 # optimise group
633 m = []
634 for entry in group:
635 if entry[0] != '-':
636 m.append(('+', entry))
637 else:
638 m.append((entry[0], entry[1:]))
639 group = m
640 # now, sort the result
641 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
642 db = self.db, cl=self):
643 a_id, an = a
644 b_id, bn = b
645 # sort by group and then sort
646 for list in group, sort:
647 for dir, prop in list:
648 # handle the properties that might be "faked"
649 if not an.has_key(prop):
650 an[prop] = cl.get(a_id, prop)
651 av = an[prop]
652 if not bn.has_key(prop):
653 bn[prop] = cl.get(b_id, prop)
654 bv = bn[prop]
656 # sorting is class-specific
657 propclass = properties[prop]
659 # String and Date values are sorted in the natural way
660 if isinstance(propclass, String):
661 # clean up the strings
662 if av and av[0] in string.uppercase:
663 av = an[prop] = av.lower()
664 if bv and bv[0] in string.uppercase:
665 bv = bn[prop] = bv.lower()
666 if (isinstance(propclass, String) or
667 isinstance(propclass, Date)):
668 if dir == '+':
669 r = cmp(av, bv)
670 if r != 0: return r
671 elif dir == '-':
672 r = cmp(bv, av)
673 if r != 0: return r
675 # Link properties are sorted according to the value of
676 # the "order" property on the linked nodes if it is
677 # present; or otherwise on the key string of the linked
678 # nodes; or finally on the node ids.
679 elif isinstance(propclass, Link):
680 link = db.classes[propclass.classname]
681 if av is None and bv is not None: return -1
682 if av is not None and bv is None: return 1
683 if av is None and bv is None: return 0
684 if link.getprops().has_key('order'):
685 if dir == '+':
686 r = cmp(link.get(av, 'order'),
687 link.get(bv, 'order'))
688 if r != 0: return r
689 elif dir == '-':
690 r = cmp(link.get(bv, 'order'),
691 link.get(av, 'order'))
692 if r != 0: return r
693 elif link.getkey():
694 key = link.getkey()
695 if dir == '+':
696 r = cmp(link.get(av, key), link.get(bv, key))
697 if r != 0: return r
698 elif dir == '-':
699 r = cmp(link.get(bv, key), link.get(av, key))
700 if r != 0: return r
701 else:
702 if dir == '+':
703 r = cmp(av, bv)
704 if r != 0: return r
705 elif dir == '-':
706 r = cmp(bv, av)
707 if r != 0: return r
709 # Multilink properties are sorted according to how many
710 # links are present.
711 elif isinstance(propclass, Multilink):
712 if dir == '+':
713 r = cmp(len(av), len(bv))
714 if r != 0: return r
715 elif dir == '-':
716 r = cmp(len(bv), len(av))
717 if r != 0: return r
718 # end for dir, prop in list:
719 # end for list in sort, group:
720 # if all else fails, compare the ids
721 return cmp(a[0], b[0])
723 l.sort(sortfun)
724 return [i[0] for i in l]
726 def count(self):
727 """Get the number of nodes in this class.
729 If the returned integer is 'numnodes', the ids of all the nodes
730 in this class run from 1 to numnodes, and numnodes+1 will be the
731 id of the next node to be created in this class.
732 """
733 return self.db.countnodes(self.classname)
735 # Manipulating properties:
737 def getprops(self):
738 """Return a dictionary mapping property names to property objects."""
739 d = self.properties.copy()
740 d['id'] = String()
741 return d
743 def addprop(self, **properties):
744 """Add properties to this class.
746 The keyword arguments in 'properties' must map names to property
747 objects, or a TypeError is raised. None of the keys in 'properties'
748 may collide with the names of existing properties, or a ValueError
749 is raised before any properties have been added.
750 """
751 for key in properties.keys():
752 if self.properties.has_key(key):
753 raise ValueError, key
754 self.properties.update(properties)
757 # XXX not in spec
758 class Node:
759 ''' A convenience wrapper for the given node
760 '''
761 def __init__(self, cl, nodeid):
762 self.__dict__['cl'] = cl
763 self.__dict__['nodeid'] = nodeid
764 def keys(self):
765 return self.cl.getprops().keys()
766 def has_key(self, name):
767 return self.cl.getprops().has_key(name)
768 def __getattr__(self, name):
769 if self.__dict__.has_key(name):
770 return self.__dict__['name']
771 try:
772 return self.cl.get(self.nodeid, name)
773 except KeyError, value:
774 raise AttributeError, str(value)
775 def __getitem__(self, name):
776 return self.cl.get(self.nodeid, name)
777 def __setattr__(self, name, value):
778 try:
779 return self.cl.set(self.nodeid, **{name: value})
780 except KeyError, value:
781 raise AttributeError, str(value)
782 def __setitem__(self, name, value):
783 self.cl.set(self.nodeid, **{name: value})
784 def history(self):
785 return self.cl.history(self.nodeid)
786 def retire(self):
787 return self.cl.retire(self.nodeid)
790 def Choice(name, *options):
791 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
792 for i in range(len(options)):
793 cl.create(name=option[i], order=i)
794 return hyperdb.Link(name)
796 #
797 # $Log: not supported by cvs2svn $
798 # Revision 1.18 2001/08/16 07:34:59 richard
799 # better CGI text searching - but hidden filter fields are disappearing...
800 #
801 # Revision 1.17 2001/08/16 06:59:58 richard
802 # all searches use re now - and they're all case insensitive
803 #
804 # Revision 1.16 2001/08/15 23:43:18 richard
805 # Fixed some isFooTypes that I missed.
806 # Refactored some code in the CGI code.
807 #
808 # Revision 1.15 2001/08/12 06:32:36 richard
809 # using isinstance(blah, Foo) now instead of isFooType
810 #
811 # Revision 1.14 2001/08/07 00:24:42 richard
812 # stupid typo
813 #
814 # Revision 1.13 2001/08/07 00:15:51 richard
815 # Added the copyright/license notice to (nearly) all files at request of
816 # Bizar Software.
817 #
818 # Revision 1.12 2001/08/02 06:38:17 richard
819 # Roundupdb now appends "mailing list" information to its messages which
820 # include the e-mail address and web interface address. Templates may
821 # override this in their db classes to include specific information (support
822 # instructions, etc).
823 #
824 # Revision 1.11 2001/08/01 04:24:21 richard
825 # mailgw was assuming certain properties existed on the issues being created.
826 #
827 # Revision 1.10 2001/07/30 02:38:31 richard
828 # get() now has a default arg - for migration only.
829 #
830 # Revision 1.9 2001/07/29 09:28:23 richard
831 # Fixed sorting by clicking on column headings.
832 #
833 # Revision 1.8 2001/07/29 08:27:40 richard
834 # Fixed handling of passed-in values in form elements (ie. during a
835 # drill-down)
836 #
837 # Revision 1.7 2001/07/29 07:01:39 richard
838 # Added vim command to all source so that we don't get no steenkin' tabs :)
839 #
840 # Revision 1.6 2001/07/29 05:36:14 richard
841 # Cleanup of the link label generation.
842 #
843 # Revision 1.5 2001/07/29 04:05:37 richard
844 # Added the fabricated property "id".
845 #
846 # Revision 1.4 2001/07/27 06:25:35 richard
847 # Fixed some of the exceptions so they're the right type.
848 # Removed the str()-ification of node ids so we don't mask oopsy errors any
849 # more.
850 #
851 # Revision 1.3 2001/07/27 05:17:14 richard
852 # just some comments
853 #
854 # Revision 1.2 2001/07/22 12:09:32 richard
855 # Final commit of Grande Splite
856 #
857 # Revision 1.1 2001/07/22 11:58:35 richard
858 # More Grande Splite
859 #
860 #
861 # vim: set filetype=python ts=4 sw=4 et si