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.15 2001-08-12 06:32:36 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 hasattr(value, 'isDate'):
194 raise TypeError, 'new property "%s" not a Date'% key
196 elif isinstance(prop, Interval):
197 if not hasattr(value, 'isInterval'):
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 if key == self.key:
269 try:
270 self.lookup(value)
271 except KeyError:
272 pass
273 else:
274 raise ValueError, 'node with key "%s" exists'%value
276 prop = self.properties[key]
278 if isinstance(prop, Link):
279 link_class = self.properties[key].classname
280 # if it isn't a number, it's a key
281 if type(value) != type(''):
282 raise ValueError, 'link value must be String'
283 if not num_re.match(value):
284 try:
285 value = self.db.classes[link_class].lookup(value)
286 except:
287 raise IndexError, 'new property "%s": %s not a %s'%(
288 key, value, self.properties[key].classname)
290 if not self.db.hasnode(link_class, value):
291 raise IndexError, '%s has no node %s'%(link_class, value)
293 # register the unlink with the old linked node
294 if node[key] is not None:
295 self.db.addjournal(link_class, node[key], 'unlink',
296 (self.classname, nodeid, key))
298 # register the link with the newly linked node
299 if value is not None:
300 self.db.addjournal(link_class, value, 'link',
301 (self.classname, nodeid, key))
303 elif isinstance(prop, Multilink):
304 if type(value) != type([]):
305 raise TypeError, 'new property "%s" not a list of ids'%key
306 link_class = self.properties[key].classname
307 l = []
308 for entry in value:
309 # if it isn't a number, it's a key
310 if type(entry) != type(''):
311 raise ValueError, 'link value must be String'
312 if not num_re.match(entry):
313 try:
314 entry = self.db.classes[link_class].lookup(entry)
315 except:
316 raise IndexError, 'new property "%s": %s not a %s'%(
317 key, entry, self.properties[key].classname)
318 l.append(entry)
319 value = l
320 propvalues[key] = value
322 #handle removals
323 l = node[key]
324 for id in l[:]:
325 if id in value:
326 continue
327 # register the unlink with the old linked node
328 self.db.addjournal(link_class, id, 'unlink',
329 (self.classname, nodeid, key))
330 l.remove(id)
332 # handle additions
333 for id in value:
334 if not self.db.hasnode(link_class, id):
335 raise IndexError, '%s has no node %s'%(link_class, id)
336 if id in l:
337 continue
338 # register the link with the newly linked node
339 self.db.addjournal(link_class, id, 'link',
340 (self.classname, nodeid, key))
341 l.append(id)
343 elif isinstance(prop, String):
344 if value is not None and type(value) != type(''):
345 raise TypeError, 'new property "%s" not a string'%key
347 elif isinstance(prop, Date):
348 if not hasattr(value, 'isDate'):
349 raise TypeError, 'new property "%s" not a Date'% key
351 elif isinstance(prop, Interval):
352 if not hasattr(value, 'isInterval'):
353 raise TypeError, 'new property "%s" not an Interval'% key
355 node[key] = value
357 self.db.setnode(self.classname, nodeid, node)
358 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
360 def retire(self, nodeid):
361 """Retire a node.
363 The properties on the node remain available from the get() method,
364 and the node's id is never reused.
366 Retired nodes are not returned by the find(), list(), or lookup()
367 methods, and other nodes may reuse the values of their key properties.
368 """
369 if self.db.journaltag is None:
370 raise DatabaseError, 'Database open read-only'
371 node = self.db.getnode(self.classname, nodeid)
372 node[self.db.RETIRED_FLAG] = 1
373 self.db.setnode(self.classname, nodeid, node)
374 self.db.addjournal(self.classname, nodeid, 'retired', None)
376 def history(self, nodeid):
377 """Retrieve the journal of edits on a particular node.
379 'nodeid' must be the id of an existing node of this class or an
380 IndexError is raised.
382 The returned list contains tuples of the form
384 (date, tag, action, params)
386 'date' is a Timestamp object specifying the time of the change and
387 'tag' is the journaltag specified when the database was opened.
388 """
389 return self.db.getjournal(self.classname, nodeid)
391 # Locating nodes:
393 def setkey(self, propname):
394 """Select a String property of this class to be the key property.
396 'propname' must be the name of a String property of this class or
397 None, or a TypeError is raised. The values of the key property on
398 all existing nodes must be unique or a ValueError is raised.
399 """
400 self.key = propname
402 def getkey(self):
403 """Return the name of the key property for this class or None."""
404 return self.key
406 def labelprop(self, default_to_id=0):
407 ''' Return the property name for a label for the given node.
409 This method attempts to generate a consistent label for the node.
410 It tries the following in order:
411 1. key property
412 2. "name" property
413 3. "title" property
414 4. first property from the sorted property name list
415 '''
416 k = self.getkey()
417 if k:
418 return k
419 props = self.getprops()
420 if props.has_key('name'):
421 return 'name'
422 elif props.has_key('title'):
423 return 'title'
424 if default_to_id:
425 return 'id'
426 props = props.keys()
427 props.sort()
428 return props[0]
430 # TODO: set up a separate index db file for this? profile?
431 def lookup(self, keyvalue):
432 """Locate a particular node by its key property and return its id.
434 If this class has no key property, a TypeError is raised. If the
435 'keyvalue' matches one of the values for the key property among
436 the nodes in this class, the matching node's id is returned;
437 otherwise a KeyError is raised.
438 """
439 cldb = self.db.getclassdb(self.classname)
440 for nodeid in self.db.getnodeids(self.classname, cldb):
441 node = self.db.getnode(self.classname, nodeid, cldb)
442 if node.has_key(self.db.RETIRED_FLAG):
443 continue
444 if node[self.key] == keyvalue:
445 return nodeid
446 cldb.close()
447 raise KeyError, keyvalue
449 # XXX: change from spec - allows multiple props to match
450 def find(self, **propspec):
451 """Get the ids of nodes in this class which link to a given node.
453 'propspec' consists of keyword args propname=nodeid
454 'propname' must be the name of a property in this class, or a
455 KeyError is raised. That property must be a Link or Multilink
456 property, or a TypeError is raised.
458 'nodeid' must be the id of an existing node in the class linked
459 to by the given property, or an IndexError is raised.
460 """
461 propspec = propspec.items()
462 for propname, nodeid in propspec:
463 # check the prop is OK
464 prop = self.properties[propname]
465 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
466 raise TypeError, "'%s' not a Link/Multilink property"%propname
467 if not self.db.hasnode(prop.classname, nodeid):
468 raise ValueError, '%s has no node %s'%(link_class, nodeid)
470 # ok, now do the find
471 cldb = self.db.getclassdb(self.classname)
472 l = []
473 for id in self.db.getnodeids(self.classname, cldb):
474 node = self.db.getnode(self.classname, id, cldb)
475 if node.has_key(self.db.RETIRED_FLAG):
476 continue
477 for propname, nodeid in propspec:
478 property = node[propname]
479 if isinstance(prop, Link) and nodeid == property:
480 l.append(id)
481 elif isinstance(prop, Multilink) and nodeid in property:
482 l.append(id)
483 cldb.close()
484 return l
486 def stringFind(self, **requirements):
487 """Locate a particular node by matching a set of its String properties.
489 If the property is not a String property, a TypeError is raised.
491 The return is a list of the id of all nodes that match.
492 """
493 for propname in requirements.keys():
494 prop = self.properties[propname]
495 if isinstance(not prop, String):
496 raise TypeError, "'%s' not a String property"%propname
497 l = []
498 cldb = self.db.getclassdb(self.classname)
499 for nodeid in self.db.getnodeids(self.classname, cldb):
500 node = self.db.getnode(self.classname, nodeid, cldb)
501 if node.has_key(self.db.RETIRED_FLAG):
502 continue
503 for key, value in requirements.items():
504 if node[key] != value:
505 break
506 else:
507 l.append(nodeid)
508 cldb.close()
509 return l
511 def list(self):
512 """Return a list of the ids of the active nodes in this class."""
513 l = []
514 cn = self.classname
515 cldb = self.db.getclassdb(cn)
516 for nodeid in self.db.getnodeids(cn, cldb):
517 node = self.db.getnode(cn, nodeid, cldb)
518 if node.has_key(self.db.RETIRED_FLAG):
519 continue
520 l.append(nodeid)
521 l.sort()
522 cldb.close()
523 return l
525 # XXX not in spec
526 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
527 ''' Return a list of the ids of the active nodes in this class that
528 match the 'filter' spec, sorted by the group spec and then the
529 sort spec
530 '''
531 cn = self.classname
533 # optimise filterspec
534 l = []
535 props = self.getprops()
536 for k, v in filterspec.items():
537 propclass = props[k]
538 if isinstance(propclass, Link):
539 if type(v) is not type([]):
540 v = [v]
541 # replace key values with node ids
542 u = []
543 link_class = self.db.classes[propclass.classname]
544 for entry in v:
545 if not num_re.match(entry):
546 try:
547 entry = link_class.lookup(entry)
548 except:
549 raise ValueError, 'new property "%s": %s not a %s'%(
550 k, entry, self.properties[k].classname)
551 u.append(entry)
553 l.append((0, k, u))
554 elif isinstance(propclass, Multilink):
555 if type(v) is not type([]):
556 v = [v]
557 # replace key values with node ids
558 u = []
559 link_class = self.db.classes[propclass.classname]
560 for entry in v:
561 if not num_re.match(entry):
562 try:
563 entry = link_class.lookup(entry)
564 except:
565 raise ValueError, 'new property "%s": %s not a %s'%(
566 k, entry, self.properties[k].classname)
567 u.append(entry)
568 l.append((1, k, u))
569 elif isinstance(propclass, String):
570 if '*' in v or '?' in v:
571 # simple glob searching
572 v = v.replace('?', '.')
573 v = v.replace('*', '.*?')
574 v = re.compile(v)
575 l.append((2, k, v))
576 elif v[0] == '^':
577 # start-anchored
578 if v[-1] == '$':
579 # _and_ end-anchored
580 l.append((6, k, v[1:-1]))
581 l.append((3, k, v[1:]))
582 elif v[-1] == '$':
583 # end-anchored
584 l.append((4, k, v[:-1]))
585 else:
586 # substring
587 l.append((5, k, v))
588 else:
589 l.append((6, k, v))
590 filterspec = l
592 # now, find all the nodes that are active and pass filtering
593 l = []
594 cldb = self.db.getclassdb(cn)
595 for nodeid in self.db.getnodeids(cn, cldb):
596 node = self.db.getnode(cn, nodeid, cldb)
597 if node.has_key(self.db.RETIRED_FLAG):
598 continue
599 # apply filter
600 for t, k, v in filterspec:
601 if t == 0 and node[k] not in v:
602 # link - if this node'd property doesn't appear in the
603 # filterspec's nodeid list, skip it
604 break
605 elif t == 1:
606 # multilink - if any of the nodeids required by the
607 # filterspec aren't in this node's property, then skip
608 # it
609 for value in v:
610 if value not in node[k]:
611 break
612 else:
613 continue
614 break
615 elif t == 2 and not v.search(node[k]):
616 # RE search
617 break
618 elif t == 3 and node[k][:len(v)] != v:
619 # start anchored
620 break
621 elif t == 4 and node[k][-len(v):] != v:
622 # end anchored
623 break
624 elif t == 5 and node[k].find(v) == -1:
625 # substring search
626 break
627 elif t == 6 and node[k] != v:
628 # straight value comparison for the other types
629 break
630 else:
631 l.append((nodeid, node))
632 l.sort()
633 cldb.close()
635 # optimise sort
636 m = []
637 for entry in sort:
638 if entry[0] != '-':
639 m.append(('+', entry))
640 else:
641 m.append((entry[0], entry[1:]))
642 sort = m
644 # optimise group
645 m = []
646 for entry in group:
647 if entry[0] != '-':
648 m.append(('+', entry))
649 else:
650 m.append((entry[0], entry[1:]))
651 group = m
652 # now, sort the result
653 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
654 db = self.db, cl=self):
655 a_id, an = a
656 b_id, bn = b
657 # sort by group and then sort
658 for list in group, sort:
659 for dir, prop in list:
660 # handle the properties that might be "faked"
661 if not an.has_key(prop):
662 an[prop] = cl.get(a_id, prop)
663 av = an[prop]
664 if not bn.has_key(prop):
665 bn[prop] = cl.get(b_id, prop)
666 bv = bn[prop]
668 # sorting is class-specific
669 propclass = properties[prop]
671 # String and Date values are sorted in the natural way
672 if isinstance(propclass, String):
673 # clean up the strings
674 if av and av[0] in string.uppercase:
675 av = an[prop] = av.lower()
676 if bv and bv[0] in string.uppercase:
677 bv = bn[prop] = bv.lower()
678 if isinstance(propclass.isStringType or propclass, Date):
679 if dir == '+':
680 r = cmp(av, bv)
681 if r != 0: return r
682 elif dir == '-':
683 r = cmp(bv, av)
684 if r != 0: return r
686 # Link properties are sorted according to the value of
687 # the "order" property on the linked nodes if it is
688 # present; or otherwise on the key string of the linked
689 # nodes; or finally on the node ids.
690 elif isinstance(propclass, Link):
691 link = db.classes[propclass.classname]
692 if av is None and bv is not None: return -1
693 if av is not None and bv is None: return 1
694 if av is None and bv is None: return 0
695 if link.getprops().has_key('order'):
696 if dir == '+':
697 r = cmp(link.get(av, 'order'),
698 link.get(bv, 'order'))
699 if r != 0: return r
700 elif dir == '-':
701 r = cmp(link.get(bv, 'order'),
702 link.get(av, 'order'))
703 if r != 0: return r
704 elif link.getkey():
705 key = link.getkey()
706 if dir == '+':
707 r = cmp(link.get(av, key), link.get(bv, key))
708 if r != 0: return r
709 elif dir == '-':
710 r = cmp(link.get(bv, key), link.get(av, key))
711 if r != 0: return r
712 else:
713 if dir == '+':
714 r = cmp(av, bv)
715 if r != 0: return r
716 elif dir == '-':
717 r = cmp(bv, av)
718 if r != 0: return r
720 # Multilink properties are sorted according to how many
721 # links are present.
722 elif isinstance(propclass, Multilink):
723 if dir == '+':
724 r = cmp(len(av), len(bv))
725 if r != 0: return r
726 elif dir == '-':
727 r = cmp(len(bv), len(av))
728 if r != 0: return r
729 # end for dir, prop in list:
730 # end for list in sort, group:
731 # if all else fails, compare the ids
732 return cmp(a[0], b[0])
734 l.sort(sortfun)
735 return [i[0] for i in l]
737 def count(self):
738 """Get the number of nodes in this class.
740 If the returned integer is 'numnodes', the ids of all the nodes
741 in this class run from 1 to numnodes, and numnodes+1 will be the
742 id of the next node to be created in this class.
743 """
744 return self.db.countnodes(self.classname)
746 # Manipulating properties:
748 def getprops(self):
749 """Return a dictionary mapping property names to property objects."""
750 d = self.properties.copy()
751 d['id'] = String()
752 return d
754 def addprop(self, **properties):
755 """Add properties to this class.
757 The keyword arguments in 'properties' must map names to property
758 objects, or a TypeError is raised. None of the keys in 'properties'
759 may collide with the names of existing properties, or a ValueError
760 is raised before any properties have been added.
761 """
762 for key in properties.keys():
763 if self.properties.has_key(key):
764 raise ValueError, key
765 self.properties.update(properties)
768 # XXX not in spec
769 class Node:
770 ''' A convenience wrapper for the given node
771 '''
772 def __init__(self, cl, nodeid):
773 self.__dict__['cl'] = cl
774 self.__dict__['nodeid'] = nodeid
775 def keys(self):
776 return self.cl.getprops().keys()
777 def has_key(self, name):
778 return self.cl.getprops().has_key(name)
779 def __getattr__(self, name):
780 if self.__dict__.has_key(name):
781 return self.__dict__['name']
782 try:
783 return self.cl.get(self.nodeid, name)
784 except KeyError, value:
785 raise AttributeError, str(value)
786 def __getitem__(self, name):
787 return self.cl.get(self.nodeid, name)
788 def __setattr__(self, name, value):
789 try:
790 return self.cl.set(self.nodeid, **{name: value})
791 except KeyError, value:
792 raise AttributeError, str(value)
793 def __setitem__(self, name, value):
794 self.cl.set(self.nodeid, **{name: value})
795 def history(self):
796 return self.cl.history(self.nodeid)
797 def retire(self):
798 return self.cl.retire(self.nodeid)
801 def Choice(name, *options):
802 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
803 for i in range(len(options)):
804 cl.create(name=option[i], order=i)
805 return hyperdb.Link(name)
807 #
808 # $Log: not supported by cvs2svn $
809 # Revision 1.14 2001/08/07 00:24:42 richard
810 # stupid typo
811 #
812 # Revision 1.13 2001/08/07 00:15:51 richard
813 # Added the copyright/license notice to (nearly) all files at request of
814 # Bizar Software.
815 #
816 # Revision 1.12 2001/08/02 06:38:17 richard
817 # Roundupdb now appends "mailing list" information to its messages which
818 # include the e-mail address and web interface address. Templates may
819 # override this in their db classes to include specific information (support
820 # instructions, etc).
821 #
822 # Revision 1.11 2001/08/01 04:24:21 richard
823 # mailgw was assuming certain properties existed on the issues being created.
824 #
825 # Revision 1.10 2001/07/30 02:38:31 richard
826 # get() now has a default arg - for migration only.
827 #
828 # Revision 1.9 2001/07/29 09:28:23 richard
829 # Fixed sorting by clicking on column headings.
830 #
831 # Revision 1.8 2001/07/29 08:27:40 richard
832 # Fixed handling of passed-in values in form elements (ie. during a
833 # drill-down)
834 #
835 # Revision 1.7 2001/07/29 07:01:39 richard
836 # Added vim command to all source so that we don't get no steenkin' tabs :)
837 #
838 # Revision 1.6 2001/07/29 05:36:14 richard
839 # Cleanup of the link label generation.
840 #
841 # Revision 1.5 2001/07/29 04:05:37 richard
842 # Added the fabricated property "id".
843 #
844 # Revision 1.4 2001/07/27 06:25:35 richard
845 # Fixed some of the exceptions so they're the right type.
846 # Removed the str()-ification of node ids so we don't mask oopsy errors any
847 # more.
848 #
849 # Revision 1.3 2001/07/27 05:17:14 richard
850 # just some comments
851 #
852 # Revision 1.2 2001/07/22 12:09:32 richard
853 # Final commit of Grande Splite
854 #
855 # Revision 1.1 2001/07/22 11:58:35 richard
856 # More Grande Splite
857 #
858 #
859 # vim: set filetype=python ts=4 sw=4 et si