32729f19d2e9d91fc662c7f6bd3ffddbcc2a7176
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.21 2001-10-05 02:23:24 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 key == self.key:
204 raise ValueError, 'key property "%s" is required'%key
205 if isinstance(prop, Multilink):
206 propvalues[key] = []
207 else:
208 propvalues[key] = None
210 # done
211 self.db.addnode(self.classname, newid, propvalues)
212 self.db.addjournal(self.classname, newid, 'create', propvalues)
213 return newid
215 def get(self, nodeid, propname, default=_marker):
216 """Get the value of a property on an existing node of this class.
218 'nodeid' must be the id of an existing node of this class or an
219 IndexError is raised. 'propname' must be the name of a property
220 of this class or a KeyError is raised.
221 """
222 d = self.db.getnode(self.classname, nodeid)
223 if propname == 'id':
224 return nodeid
225 if not d.has_key(propname) and default is not _marker:
226 return default
227 return d[propname]
229 # XXX not in spec
230 def getnode(self, nodeid):
231 ''' Return a convenience wrapper for the node
232 '''
233 return Node(self, nodeid)
235 def set(self, nodeid, **propvalues):
236 """Modify a property on an existing node of this class.
238 'nodeid' must be the id of an existing node of this class or an
239 IndexError is raised.
241 Each key in 'propvalues' must be the name of a property of this
242 class or a KeyError is raised.
244 All values in 'propvalues' must be acceptable types for their
245 corresponding properties or a TypeError is raised.
247 If the value of the key property is set, it must not collide with
248 other key strings or a ValueError is raised.
250 If the value of a Link or Multilink property contains an invalid
251 node id, a ValueError is raised.
252 """
253 if not propvalues:
254 return
256 if propvalues.has_key('id'):
257 raise KeyError, '"id" is reserved'
259 if self.db.journaltag is None:
260 raise DatabaseError, 'Database open read-only'
262 node = self.db.getnode(self.classname, nodeid)
263 if node.has_key(self.db.RETIRED_FLAG):
264 raise IndexError
265 num_re = re.compile('^\d+$')
266 for key, value in propvalues.items():
267 if not node.has_key(key):
268 raise KeyError, key
270 # check to make sure we're not duplicating an existing key
271 if key == self.key and node[key] != value:
272 try:
273 self.lookup(value)
274 except KeyError:
275 pass
276 else:
277 raise ValueError, 'node with key "%s" exists'%value
279 prop = self.properties[key]
281 if isinstance(prop, Link):
282 link_class = self.properties[key].classname
283 # if it isn't a number, it's a key
284 if type(value) != type(''):
285 raise ValueError, 'link value must be String'
286 if not num_re.match(value):
287 try:
288 value = self.db.classes[link_class].lookup(value)
289 except:
290 raise IndexError, 'new property "%s": %s not a %s'%(
291 key, value, self.properties[key].classname)
293 if not self.db.hasnode(link_class, value):
294 raise IndexError, '%s has no node %s'%(link_class, value)
296 # register the unlink with the old linked node
297 if node[key] is not None:
298 self.db.addjournal(link_class, node[key], 'unlink',
299 (self.classname, nodeid, key))
301 # register the link with the newly linked node
302 if value is not None:
303 self.db.addjournal(link_class, value, 'link',
304 (self.classname, nodeid, key))
306 elif isinstance(prop, Multilink):
307 if type(value) != type([]):
308 raise TypeError, 'new property "%s" not a list of ids'%key
309 link_class = self.properties[key].classname
310 l = []
311 for entry in value:
312 # if it isn't a number, it's a key
313 if type(entry) != type(''):
314 raise ValueError, 'link value must be String'
315 if not num_re.match(entry):
316 try:
317 entry = self.db.classes[link_class].lookup(entry)
318 except:
319 raise IndexError, 'new property "%s": %s not a %s'%(
320 key, entry, self.properties[key].classname)
321 l.append(entry)
322 value = l
323 propvalues[key] = value
325 #handle removals
326 l = node[key]
327 for id in l[:]:
328 if id in value:
329 continue
330 # register the unlink with the old linked node
331 self.db.addjournal(link_class, id, 'unlink',
332 (self.classname, nodeid, key))
333 l.remove(id)
335 # handle additions
336 for id in value:
337 if not self.db.hasnode(link_class, id):
338 raise IndexError, '%s has no node %s'%(link_class, id)
339 if id in l:
340 continue
341 # register the link with the newly linked node
342 self.db.addjournal(link_class, id, 'link',
343 (self.classname, nodeid, key))
344 l.append(id)
346 elif isinstance(prop, String):
347 if value is not None and type(value) != type(''):
348 raise TypeError, 'new property "%s" not a string'%key
350 elif isinstance(prop, Date):
351 if not isinstance(value, date.Date):
352 raise TypeError, 'new property "%s" not a Date'% key
354 elif isinstance(prop, Interval):
355 if not isinstance(value, date.Interval):
356 raise TypeError, 'new property "%s" not an Interval'% key
358 node[key] = value
360 self.db.setnode(self.classname, nodeid, node)
361 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
363 def retire(self, nodeid):
364 """Retire a node.
366 The properties on the node remain available from the get() method,
367 and the node's id is never reused.
369 Retired nodes are not returned by the find(), list(), or lookup()
370 methods, and other nodes may reuse the values of their key properties.
371 """
372 if self.db.journaltag is None:
373 raise DatabaseError, 'Database open read-only'
374 node = self.db.getnode(self.classname, nodeid)
375 node[self.db.RETIRED_FLAG] = 1
376 self.db.setnode(self.classname, nodeid, node)
377 self.db.addjournal(self.classname, nodeid, 'retired', None)
379 def history(self, nodeid):
380 """Retrieve the journal of edits on a particular node.
382 'nodeid' must be the id of an existing node of this class or an
383 IndexError is raised.
385 The returned list contains tuples of the form
387 (date, tag, action, params)
389 'date' is a Timestamp object specifying the time of the change and
390 'tag' is the journaltag specified when the database was opened.
391 """
392 return self.db.getjournal(self.classname, nodeid)
394 # Locating nodes:
396 def setkey(self, propname):
397 """Select a String property of this class to be the key property.
399 'propname' must be the name of a String property of this class or
400 None, or a TypeError is raised. The values of the key property on
401 all existing nodes must be unique or a ValueError is raised.
402 """
403 self.key = propname
405 def getkey(self):
406 """Return the name of the key property for this class or None."""
407 return self.key
409 def labelprop(self, default_to_id=0):
410 ''' Return the property name for a label for the given node.
412 This method attempts to generate a consistent label for the node.
413 It tries the following in order:
414 1. key property
415 2. "name" property
416 3. "title" property
417 4. first property from the sorted property name list
418 '''
419 k = self.getkey()
420 if k:
421 return k
422 props = self.getprops()
423 if props.has_key('name'):
424 return 'name'
425 elif props.has_key('title'):
426 return 'title'
427 if default_to_id:
428 return 'id'
429 props = props.keys()
430 props.sort()
431 return props[0]
433 # TODO: set up a separate index db file for this? profile?
434 def lookup(self, keyvalue):
435 """Locate a particular node by its key property and return its id.
437 If this class has no key property, a TypeError is raised. If the
438 'keyvalue' matches one of the values for the key property among
439 the nodes in this class, the matching node's id is returned;
440 otherwise a KeyError is raised.
441 """
442 cldb = self.db.getclassdb(self.classname)
443 for nodeid in self.db.getnodeids(self.classname, cldb):
444 node = self.db.getnode(self.classname, nodeid, cldb)
445 if node.has_key(self.db.RETIRED_FLAG):
446 continue
447 if node[self.key] == keyvalue:
448 return nodeid
449 cldb.close()
450 raise KeyError, keyvalue
452 # XXX: change from spec - allows multiple props to match
453 def find(self, **propspec):
454 """Get the ids of nodes in this class which link to a given node.
456 'propspec' consists of keyword args propname=nodeid
457 'propname' must be the name of a property in this class, or a
458 KeyError is raised. That property must be a Link or Multilink
459 property, or a TypeError is raised.
461 'nodeid' must be the id of an existing node in the class linked
462 to by the given property, or an IndexError is raised.
463 """
464 propspec = propspec.items()
465 for propname, nodeid in propspec:
466 # check the prop is OK
467 prop = self.properties[propname]
468 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
469 raise TypeError, "'%s' not a Link/Multilink property"%propname
470 if not self.db.hasnode(prop.classname, nodeid):
471 raise ValueError, '%s has no node %s'%(link_class, nodeid)
473 # ok, now do the find
474 cldb = self.db.getclassdb(self.classname)
475 l = []
476 for id in self.db.getnodeids(self.classname, cldb):
477 node = self.db.getnode(self.classname, id, cldb)
478 if node.has_key(self.db.RETIRED_FLAG):
479 continue
480 for propname, nodeid in propspec:
481 property = node[propname]
482 if isinstance(prop, Link) and nodeid == property:
483 l.append(id)
484 elif isinstance(prop, Multilink) and nodeid in property:
485 l.append(id)
486 cldb.close()
487 return l
489 def stringFind(self, **requirements):
490 """Locate a particular node by matching a set of its String properties.
492 If the property is not a String property, a TypeError is raised.
494 The return is a list of the id of all nodes that match.
495 """
496 for propname in requirements.keys():
497 prop = self.properties[propname]
498 if isinstance(not prop, String):
499 raise TypeError, "'%s' not a String property"%propname
500 l = []
501 cldb = self.db.getclassdb(self.classname)
502 for nodeid in self.db.getnodeids(self.classname, cldb):
503 node = self.db.getnode(self.classname, nodeid, cldb)
504 if node.has_key(self.db.RETIRED_FLAG):
505 continue
506 for key, value in requirements.items():
507 if node[key] != value:
508 break
509 else:
510 l.append(nodeid)
511 cldb.close()
512 return l
514 def list(self):
515 """Return a list of the ids of the active nodes in this class."""
516 l = []
517 cn = self.classname
518 cldb = self.db.getclassdb(cn)
519 for nodeid in self.db.getnodeids(cn, cldb):
520 node = self.db.getnode(cn, nodeid, cldb)
521 if node.has_key(self.db.RETIRED_FLAG):
522 continue
523 l.append(nodeid)
524 l.sort()
525 cldb.close()
526 return l
528 # XXX not in spec
529 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
530 ''' Return a list of the ids of the active nodes in this class that
531 match the 'filter' spec, sorted by the group spec and then the
532 sort spec
533 '''
534 cn = self.classname
536 # optimise filterspec
537 l = []
538 props = self.getprops()
539 for k, v in filterspec.items():
540 propclass = props[k]
541 if isinstance(propclass, Link):
542 if type(v) is not type([]):
543 v = [v]
544 # replace key values with node ids
545 u = []
546 link_class = self.db.classes[propclass.classname]
547 for entry in v:
548 if not num_re.match(entry):
549 try:
550 entry = link_class.lookup(entry)
551 except:
552 raise ValueError, 'new property "%s": %s not a %s'%(
553 k, entry, self.properties[k].classname)
554 u.append(entry)
556 l.append((0, k, u))
557 elif isinstance(propclass, Multilink):
558 if type(v) is not type([]):
559 v = [v]
560 # replace key values with node ids
561 u = []
562 link_class = self.db.classes[propclass.classname]
563 for entry in v:
564 if not num_re.match(entry):
565 try:
566 entry = link_class.lookup(entry)
567 except:
568 raise ValueError, 'new property "%s": %s not a %s'%(
569 k, entry, self.properties[k].classname)
570 u.append(entry)
571 l.append((1, k, u))
572 elif isinstance(propclass, String):
573 # simple glob searching
574 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
575 v = v.replace('?', '.')
576 v = v.replace('*', '.*?')
577 l.append((2, k, re.compile(v, re.I)))
578 else:
579 l.append((6, k, v))
580 filterspec = l
582 # now, find all the nodes that are active and pass filtering
583 l = []
584 cldb = self.db.getclassdb(cn)
585 for nodeid in self.db.getnodeids(cn, cldb):
586 node = self.db.getnode(cn, nodeid, cldb)
587 if node.has_key(self.db.RETIRED_FLAG):
588 continue
589 # apply filter
590 for t, k, v in filterspec:
591 if t == 0 and node[k] not in v:
592 # link - if this node'd property doesn't appear in the
593 # filterspec's nodeid list, skip it
594 break
595 elif t == 1:
596 # multilink - if any of the nodeids required by the
597 # filterspec aren't in this node's property, then skip
598 # it
599 for value in v:
600 if value not in node[k]:
601 break
602 else:
603 continue
604 break
605 elif t == 2 and not v.search(node[k]):
606 # RE search
607 break
608 # elif t == 3 and node[k][:len(v)] != v:
609 # # start anchored
610 # break
611 # elif t == 4 and node[k][-len(v):] != v:
612 # # end anchored
613 # break
614 # elif t == 5 and node[k].find(v) == -1:
615 # # substring search
616 # break
617 elif t == 6 and node[k] != v:
618 # straight value comparison for the other types
619 break
620 else:
621 l.append((nodeid, node))
622 l.sort()
623 cldb.close()
625 # optimise sort
626 m = []
627 for entry in sort:
628 if entry[0] != '-':
629 m.append(('+', entry))
630 else:
631 m.append((entry[0], entry[1:]))
632 sort = m
634 # optimise group
635 m = []
636 for entry in group:
637 if entry[0] != '-':
638 m.append(('+', entry))
639 else:
640 m.append((entry[0], entry[1:]))
641 group = m
642 # now, sort the result
643 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
644 db = self.db, cl=self):
645 a_id, an = a
646 b_id, bn = b
647 # sort by group and then sort
648 for list in group, sort:
649 for dir, prop in list:
650 # handle the properties that might be "faked"
651 if not an.has_key(prop):
652 an[prop] = cl.get(a_id, prop)
653 av = an[prop]
654 if not bn.has_key(prop):
655 bn[prop] = cl.get(b_id, prop)
656 bv = bn[prop]
658 # sorting is class-specific
659 propclass = properties[prop]
661 # String and Date values are sorted in the natural way
662 if isinstance(propclass, String):
663 # clean up the strings
664 if av and av[0] in string.uppercase:
665 av = an[prop] = av.lower()
666 if bv and bv[0] in string.uppercase:
667 bv = bn[prop] = bv.lower()
668 if (isinstance(propclass, String) or
669 isinstance(propclass, Date)):
670 if dir == '+':
671 r = cmp(av, bv)
672 if r != 0: return r
673 elif dir == '-':
674 r = cmp(bv, av)
675 if r != 0: return r
677 # Link properties are sorted according to the value of
678 # the "order" property on the linked nodes if it is
679 # present; or otherwise on the key string of the linked
680 # nodes; or finally on the node ids.
681 elif isinstance(propclass, Link):
682 link = db.classes[propclass.classname]
683 if av is None and bv is not None: return -1
684 if av is not None and bv is None: return 1
685 if av is None and bv is None: return 0
686 if link.getprops().has_key('order'):
687 if dir == '+':
688 r = cmp(link.get(av, 'order'),
689 link.get(bv, 'order'))
690 if r != 0: return r
691 elif dir == '-':
692 r = cmp(link.get(bv, 'order'),
693 link.get(av, 'order'))
694 if r != 0: return r
695 elif link.getkey():
696 key = link.getkey()
697 if dir == '+':
698 r = cmp(link.get(av, key), link.get(bv, key))
699 if r != 0: return r
700 elif dir == '-':
701 r = cmp(link.get(bv, key), link.get(av, key))
702 if r != 0: return r
703 else:
704 if dir == '+':
705 r = cmp(av, bv)
706 if r != 0: return r
707 elif dir == '-':
708 r = cmp(bv, av)
709 if r != 0: return r
711 # Multilink properties are sorted according to how many
712 # links are present.
713 elif isinstance(propclass, Multilink):
714 if dir == '+':
715 r = cmp(len(av), len(bv))
716 if r != 0: return r
717 elif dir == '-':
718 r = cmp(len(bv), len(av))
719 if r != 0: return r
720 # end for dir, prop in list:
721 # end for list in sort, group:
722 # if all else fails, compare the ids
723 return cmp(a[0], b[0])
725 l.sort(sortfun)
726 return [i[0] for i in l]
728 def count(self):
729 """Get the number of nodes in this class.
731 If the returned integer is 'numnodes', the ids of all the nodes
732 in this class run from 1 to numnodes, and numnodes+1 will be the
733 id of the next node to be created in this class.
734 """
735 return self.db.countnodes(self.classname)
737 # Manipulating properties:
739 def getprops(self, protected=1):
740 """Return a dictionary mapping property names to property objects.
741 If the "protected" flag is true, we include protected properties -
742 those which may not be modified."""
743 d = self.properties.copy()
744 if protected:
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)
801 #
802 # $Log: not supported by cvs2svn $
803 # Revision 1.20 2001/10/04 02:12:42 richard
804 # Added nicer command-line item adding: passing no arguments will enter an
805 # interactive more which asks for each property in turn. While I was at it, I
806 # fixed an implementation problem WRT the spec - I wasn't raising a
807 # ValueError if the key property was missing from a create(). Also added a
808 # protected=boolean argument to getprops() so we can list only the mutable
809 # properties (defaults to yes, which lists the immutables).
810 #
811 # Revision 1.19 2001/08/29 04:47:18 richard
812 # Fixed CGI client change messages so they actually include the properties
813 # changed (again).
814 #
815 # Revision 1.18 2001/08/16 07:34:59 richard
816 # better CGI text searching - but hidden filter fields are disappearing...
817 #
818 # Revision 1.17 2001/08/16 06:59:58 richard
819 # all searches use re now - and they're all case insensitive
820 #
821 # Revision 1.16 2001/08/15 23:43:18 richard
822 # Fixed some isFooTypes that I missed.
823 # Refactored some code in the CGI code.
824 #
825 # Revision 1.15 2001/08/12 06:32:36 richard
826 # using isinstance(blah, Foo) now instead of isFooType
827 #
828 # Revision 1.14 2001/08/07 00:24:42 richard
829 # stupid typo
830 #
831 # Revision 1.13 2001/08/07 00:15:51 richard
832 # Added the copyright/license notice to (nearly) all files at request of
833 # Bizar Software.
834 #
835 # Revision 1.12 2001/08/02 06:38:17 richard
836 # Roundupdb now appends "mailing list" information to its messages which
837 # include the e-mail address and web interface address. Templates may
838 # override this in their db classes to include specific information (support
839 # instructions, etc).
840 #
841 # Revision 1.11 2001/08/01 04:24:21 richard
842 # mailgw was assuming certain properties existed on the issues being created.
843 #
844 # Revision 1.10 2001/07/30 02:38:31 richard
845 # get() now has a default arg - for migration only.
846 #
847 # Revision 1.9 2001/07/29 09:28:23 richard
848 # Fixed sorting by clicking on column headings.
849 #
850 # Revision 1.8 2001/07/29 08:27:40 richard
851 # Fixed handling of passed-in values in form elements (ie. during a
852 # drill-down)
853 #
854 # Revision 1.7 2001/07/29 07:01:39 richard
855 # Added vim command to all source so that we don't get no steenkin' tabs :)
856 #
857 # Revision 1.6 2001/07/29 05:36:14 richard
858 # Cleanup of the link label generation.
859 #
860 # Revision 1.5 2001/07/29 04:05:37 richard
861 # Added the fabricated property "id".
862 #
863 # Revision 1.4 2001/07/27 06:25:35 richard
864 # Fixed some of the exceptions so they're the right type.
865 # Removed the str()-ification of node ids so we don't mask oopsy errors any
866 # more.
867 #
868 # Revision 1.3 2001/07/27 05:17:14 richard
869 # just some comments
870 #
871 # Revision 1.2 2001/07/22 12:09:32 richard
872 # Final commit of Grande Splite
873 #
874 # Revision 1.1 2001/07/22 11:58:35 richard
875 # More Grande Splite
876 #
877 #
878 # vim: set filetype=python ts=4 sw=4 et si