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 THE 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.13 2001-08-07 00:15:51 richard Exp $
20 # standard python modules
21 import cPickle, re, string
23 # roundup modules
24 import date
27 #
28 # Types
29 #
30 class BaseType:
31 isStringType = 0
32 isDateType = 0
33 isIntervalType = 0
34 isLinkType = 0
35 isMultilinkType = 0
37 class String(BaseType):
38 def __init__(self):
39 """An object designating a String property."""
40 pass
41 def __repr__(self):
42 return '<%s>'%self.__class__
43 isStringType = 1
45 class Date(BaseType, String):
46 isDateType = 1
48 class Interval(BaseType, String):
49 isIntervalType = 1
51 class Link(BaseType):
52 def __init__(self, classname):
53 """An object designating a Link property that links to
54 nodes in a specified class."""
55 self.classname = classname
56 def __repr__(self):
57 return '<%s to "%s">'%(self.__class__, self.classname)
58 isLinkType = 1
60 class Multilink(BaseType, Link):
61 """An object designating a Multilink property that links
62 to nodes in a specified class.
63 """
64 isMultilinkType = 1
66 class DatabaseError(ValueError):
67 pass
70 #
71 # the base Database class
72 #
73 class Database:
74 # flag to set on retired entries
75 RETIRED_FLAG = '__hyperdb_retired'
78 _marker = []
79 #
80 # The base Class class
81 #
82 class Class:
83 """The handle to a particular class of nodes in a hyperdatabase."""
85 def __init__(self, db, classname, **properties):
86 """Create a new class with a given name and property specification.
88 'classname' must not collide with the name of an existing class,
89 or a ValueError is raised. The keyword arguments in 'properties'
90 must map names to property objects, or a TypeError is raised.
91 """
92 self.classname = classname
93 self.properties = properties
94 self.db = db
95 self.key = ''
97 # do the db-related init stuff
98 db.addclass(self)
100 # Editing nodes:
102 def create(self, **propvalues):
103 """Create a new node of this class and return its id.
105 The keyword arguments in 'propvalues' map property names to values.
107 The values of arguments must be acceptable for the types of their
108 corresponding properties or a TypeError is raised.
110 If this class has a key property, it must be present and its value
111 must not collide with other key strings or a ValueError is raised.
113 Any other properties on this class that are missing from the
114 'propvalues' dictionary are set to None.
116 If an id in a link or multilink property does not refer to a valid
117 node, an IndexError is raised.
118 """
119 if propvalues.has_key('id'):
120 raise KeyError, '"id" is reserved'
122 if self.db.journaltag is None:
123 raise DatabaseError, 'Database open read-only'
125 # new node's id
126 newid = str(self.count() + 1)
128 # validate propvalues
129 num_re = re.compile('^\d+$')
130 for key, value in propvalues.items():
131 if key == self.key:
132 try:
133 self.lookup(value)
134 except KeyError:
135 pass
136 else:
137 raise ValueError, 'node with key "%s" exists'%value
139 # try to handle this property
140 try:
141 prop = self.properties[key]
142 except KeyError:
143 raise KeyError, '"%s" has no property "%s"'%(self.classname,
144 key)
146 if prop.isLinkType:
147 if type(value) != type(''):
148 raise ValueError, 'link value must be String'
149 # value = str(value)
150 link_class = self.properties[key].classname
151 # if it isn't a number, it's a key
152 if not num_re.match(value):
153 try:
154 value = self.db.classes[link_class].lookup(value)
155 except:
156 raise IndexError, 'new property "%s": %s not a %s'%(
157 key, value, self.properties[key].classname)
158 propvalues[key] = value
159 if not self.db.hasnode(link_class, value):
160 raise IndexError, '%s has no node %s'%(link_class, value)
162 # register the link with the newly linked node
163 self.db.addjournal(link_class, value, 'link',
164 (self.classname, newid, key))
166 elif prop.isMultilinkType:
167 if type(value) != type([]):
168 raise TypeError, 'new property "%s" not a list of ids'%key
169 link_class = self.properties[key].classname
170 l = []
171 for entry in value:
172 if type(entry) != type(''):
173 raise ValueError, 'link value must be String'
174 # if it isn't a number, it's a key
175 if not num_re.match(entry):
176 try:
177 entry = self.db.classes[link_class].lookup(entry)
178 except:
179 raise IndexError, 'new property "%s": %s not a %s'%(
180 key, entry, self.properties[key].classname)
181 l.append(entry)
182 value = l
183 propvalues[key] = value
185 # handle additions
186 for id in value:
187 if not self.db.hasnode(link_class, id):
188 raise IndexError, '%s has no node %s'%(link_class, id)
189 # register the link with the newly linked node
190 self.db.addjournal(link_class, id, 'link',
191 (self.classname, newid, key))
193 elif prop.isStringType:
194 if type(value) != type(''):
195 raise TypeError, 'new property "%s" not a string'%key
197 elif prop.isDateType:
198 if not hasattr(value, 'isDate'):
199 raise TypeError, 'new property "%s" not a Date'% key
201 elif prop.isIntervalType:
202 if not hasattr(value, 'isInterval'):
203 raise TypeError, 'new property "%s" not an Interval'% key
205 for key, prop in self.properties.items():
206 if propvalues.has_key(key):
207 continue
208 if prop.isMultilinkType:
209 propvalues[key] = []
210 else:
211 propvalues[key] = None
213 # done
214 self.db.addnode(self.classname, newid, propvalues)
215 self.db.addjournal(self.classname, newid, 'create', propvalues)
216 return newid
218 def get(self, nodeid, propname, default=_marker):
219 """Get the value of a property on an existing node of this class.
221 'nodeid' must be the id of an existing node of this class or an
222 IndexError is raised. 'propname' must be the name of a property
223 of this class or a KeyError is raised.
224 """
225 if propname == 'id':
226 return nodeid
227 # nodeid = str(nodeid)
228 d = self.db.getnode(self.classname, nodeid)
229 if not d.has_key(propname) and default is not _marker:
230 return default
231 return d[propname]
233 # XXX not in spec
234 def getnode(self, nodeid):
235 ''' Return a convenience wrapper for the node
236 '''
237 return Node(self, nodeid)
239 def set(self, nodeid, **propvalues):
240 """Modify a property on an existing node of this class.
242 'nodeid' must be the id of an existing node of this class or an
243 IndexError is raised.
245 Each key in 'propvalues' must be the name of a property of this
246 class or a KeyError is raised.
248 All values in 'propvalues' must be acceptable types for their
249 corresponding properties or a TypeError is raised.
251 If the value of the key property is set, it must not collide with
252 other key strings or a ValueError is raised.
254 If the value of a Link or Multilink property contains an invalid
255 node id, a ValueError is raised.
256 """
257 if not propvalues:
258 return
260 if propvalues.has_key('id'):
261 raise KeyError, '"id" is reserved'
263 if self.db.journaltag is None:
264 raise DatabaseError, 'Database open read-only'
266 # nodeid = str(nodeid)
267 node = self.db.getnode(self.classname, nodeid)
268 if node.has_key(self.db.RETIRED_FLAG):
269 raise IndexError
270 num_re = re.compile('^\d+$')
271 for key, value in propvalues.items():
272 if not node.has_key(key):
273 raise KeyError, key
275 if key == self.key:
276 try:
277 self.lookup(value)
278 except KeyError:
279 pass
280 else:
281 raise ValueError, 'node with key "%s" exists'%value
283 prop = self.properties[key]
285 if prop.isLinkType:
286 # value = str(value)
287 link_class = self.properties[key].classname
288 # if it isn't a number, it's a key
289 if type(value) != type(''):
290 raise ValueError, 'link value must be String'
291 if not num_re.match(value):
292 try:
293 value = self.db.classes[link_class].lookup(value)
294 except:
295 raise IndexError, 'new property "%s": %s not a %s'%(
296 key, value, self.properties[key].classname)
298 if not self.db.hasnode(link_class, value):
299 raise IndexError, '%s has no node %s'%(link_class, value)
301 # register the unlink with the old linked node
302 if node[key] is not None:
303 self.db.addjournal(link_class, node[key], 'unlink',
304 (self.classname, nodeid, key))
306 # register the link with the newly linked node
307 if value is not None:
308 self.db.addjournal(link_class, value, 'link',
309 (self.classname, nodeid, key))
311 elif prop.isMultilinkType:
312 if type(value) != type([]):
313 raise TypeError, 'new property "%s" not a list of ids'%key
314 link_class = self.properties[key].classname
315 l = []
316 for entry in value:
317 # if it isn't a number, it's a key
318 if type(entry) != type(''):
319 raise ValueError, 'link value must be String'
320 if not num_re.match(entry):
321 try:
322 entry = self.db.classes[link_class].lookup(entry)
323 except:
324 raise IndexError, 'new property "%s": %s not a %s'%(
325 key, entry, self.properties[key].classname)
326 l.append(entry)
327 value = l
328 propvalues[key] = value
330 #handle removals
331 l = node[key]
332 for id in l[:]:
333 if id in value:
334 continue
335 # register the unlink with the old linked node
336 self.db.addjournal(link_class, id, 'unlink',
337 (self.classname, nodeid, key))
338 l.remove(id)
340 # handle additions
341 for id in value:
342 if not self.db.hasnode(link_class, id):
343 raise IndexError, '%s has no node %s'%(link_class, id)
344 if id in l:
345 continue
346 # register the link with the newly linked node
347 self.db.addjournal(link_class, id, 'link',
348 (self.classname, nodeid, key))
349 l.append(id)
351 elif prop.isStringType:
352 if value is not None and type(value) != type(''):
353 raise TypeError, 'new property "%s" not a string'%key
355 elif prop.isDateType:
356 if not hasattr(value, 'isDate'):
357 raise TypeError, 'new property "%s" not a Date'% key
359 elif prop.isIntervalType:
360 if not hasattr(value, 'isInterval'):
361 raise TypeError, 'new property "%s" not an Interval'% key
363 node[key] = value
365 self.db.setnode(self.classname, nodeid, node)
366 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
368 def retire(self, nodeid):
369 """Retire a node.
371 The properties on the node remain available from the get() method,
372 and the node's id is never reused.
374 Retired nodes are not returned by the find(), list(), or lookup()
375 methods, and other nodes may reuse the values of their key properties.
376 """
377 # nodeid = str(nodeid)
378 if self.db.journaltag is None:
379 raise DatabaseError, 'Database open read-only'
380 node = self.db.getnode(self.classname, nodeid)
381 node[self.db.RETIRED_FLAG] = 1
382 self.db.setnode(self.classname, nodeid, node)
383 self.db.addjournal(self.classname, nodeid, 'retired', None)
385 def history(self, nodeid):
386 """Retrieve the journal of edits on a particular node.
388 'nodeid' must be the id of an existing node of this class or an
389 IndexError is raised.
391 The returned list contains tuples of the form
393 (date, tag, action, params)
395 'date' is a Timestamp object specifying the time of the change and
396 'tag' is the journaltag specified when the database was opened.
397 """
398 return self.db.getjournal(self.classname, nodeid)
400 # Locating nodes:
402 def setkey(self, propname):
403 """Select a String property of this class to be the key property.
405 'propname' must be the name of a String property of this class or
406 None, or a TypeError is raised. The values of the key property on
407 all existing nodes must be unique or a ValueError is raised.
408 """
409 self.key = propname
411 def getkey(self):
412 """Return the name of the key property for this class or None."""
413 return self.key
415 def labelprop(self, default_to_id=0):
416 ''' Return the property name for a label for the given node.
418 This method attempts to generate a consistent label for the node.
419 It tries the following in order:
420 1. key property
421 2. "name" property
422 3. "title" property
423 4. first property from the sorted property name list
424 '''
425 k = self.getkey()
426 if k:
427 return k
428 props = self.getprops()
429 if props.has_key('name'):
430 return 'name'
431 elif props.has_key('title'):
432 return 'title'
433 if default_to_id:
434 return 'id'
435 props = props.keys()
436 props.sort()
437 return props[0]
439 # TODO: set up a separate index db file for this? profile?
440 def lookup(self, keyvalue):
441 """Locate a particular node by its key property and return its id.
443 If this class has no key property, a TypeError is raised. If the
444 'keyvalue' matches one of the values for the key property among
445 the nodes in this class, the matching node's id is returned;
446 otherwise a KeyError is raised.
447 """
448 cldb = self.db.getclassdb(self.classname)
449 for nodeid in self.db.getnodeids(self.classname, cldb):
450 node = self.db.getnode(self.classname, nodeid, cldb)
451 if node.has_key(self.db.RETIRED_FLAG):
452 continue
453 if node[self.key] == keyvalue:
454 return nodeid
455 cldb.close()
456 raise KeyError, keyvalue
458 # XXX: change from spec - allows multiple props to match
459 def find(self, **propspec):
460 """Get the ids of nodes in this class which link to a given node.
462 'propspec' consists of keyword args propname=nodeid
463 'propname' must be the name of a property in this class, or a
464 KeyError is raised. That property must be a Link or Multilink
465 property, or a TypeError is raised.
467 'nodeid' must be the id of an existing node in the class linked
468 to by the given property, or an IndexError is raised.
469 """
470 propspec = propspec.items()
471 for propname, nodeid in propspec:
472 # nodeid = str(nodeid)
473 # check the prop is OK
474 prop = self.properties[propname]
475 if not prop.isLinkType and not prop.isMultilinkType:
476 raise TypeError, "'%s' not a Link/Multilink property"%propname
477 if not self.db.hasnode(prop.classname, nodeid):
478 raise ValueError, '%s has no node %s'%(link_class, nodeid)
480 # ok, now do the find
481 cldb = self.db.getclassdb(self.classname)
482 l = []
483 for id in self.db.getnodeids(self.classname, cldb):
484 node = self.db.getnode(self.classname, id, cldb)
485 if node.has_key(self.db.RETIRED_FLAG):
486 continue
487 for propname, nodeid in propspec:
488 # nodeid = str(nodeid)
489 property = node[propname]
490 if prop.isLinkType and nodeid == property:
491 l.append(id)
492 elif prop.isMultilinkType and nodeid in property:
493 l.append(id)
494 cldb.close()
495 return l
497 def stringFind(self, **requirements):
498 """Locate a particular node by matching a set of its String properties.
500 If the property is not a String property, a TypeError is raised.
502 The return is a list of the id of all nodes that match.
503 """
504 for propname in requirements.keys():
505 prop = self.properties[propname]
506 if not prop.isStringType:
507 raise TypeError, "'%s' not a String property"%propname
508 l = []
509 cldb = self.db.getclassdb(self.classname)
510 for nodeid in self.db.getnodeids(self.classname, cldb):
511 node = self.db.getnode(self.classname, nodeid, cldb)
512 if node.has_key(self.db.RETIRED_FLAG):
513 continue
514 for key, value in requirements.items():
515 if node[key] != value:
516 break
517 else:
518 l.append(nodeid)
519 cldb.close()
520 return l
522 def list(self):
523 """Return a list of the ids of the active nodes in this class."""
524 l = []
525 cn = self.classname
526 cldb = self.db.getclassdb(cn)
527 for nodeid in self.db.getnodeids(cn, cldb):
528 node = self.db.getnode(cn, nodeid, cldb)
529 if node.has_key(self.db.RETIRED_FLAG):
530 continue
531 l.append(nodeid)
532 l.sort()
533 cldb.close()
534 return l
536 # XXX not in spec
537 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
538 ''' Return a list of the ids of the active nodes in this class that
539 match the 'filter' spec, sorted by the group spec and then the
540 sort spec
541 '''
542 cn = self.classname
544 # optimise filterspec
545 l = []
546 props = self.getprops()
547 for k, v in filterspec.items():
548 propclass = props[k]
549 if propclass.isLinkType:
550 if type(v) is not type([]):
551 v = [v]
552 # replace key values with node ids
553 u = []
554 link_class = self.db.classes[propclass.classname]
555 for entry in v:
556 if not num_re.match(entry):
557 try:
558 entry = link_class.lookup(entry)
559 except:
560 raise ValueError, 'new property "%s": %s not a %s'%(
561 k, entry, self.properties[k].classname)
562 u.append(entry)
564 l.append((0, k, u))
565 elif propclass.isMultilinkType:
566 if type(v) is not type([]):
567 v = [v]
568 # replace key values with node ids
569 u = []
570 link_class = self.db.classes[propclass.classname]
571 for entry in v:
572 if not num_re.match(entry):
573 try:
574 entry = link_class.lookup(entry)
575 except:
576 raise ValueError, 'new property "%s": %s not a %s'%(
577 k, entry, self.properties[k].classname)
578 u.append(entry)
579 l.append((1, k, u))
580 elif propclass.isStringType:
581 if '*' in v or '?' in v:
582 # simple glob searching
583 v = v.replace('?', '.')
584 v = v.replace('*', '.*?')
585 v = re.compile(v)
586 l.append((2, k, v))
587 elif v[0] == '^':
588 # start-anchored
589 if v[-1] == '$':
590 # _and_ end-anchored
591 l.append((6, k, v[1:-1]))
592 l.append((3, k, v[1:]))
593 elif v[-1] == '$':
594 # end-anchored
595 l.append((4, k, v[:-1]))
596 else:
597 # substring
598 l.append((5, k, v))
599 else:
600 l.append((6, k, v))
601 filterspec = l
603 # now, find all the nodes that are active and pass filtering
604 l = []
605 cldb = self.db.getclassdb(cn)
606 for nodeid in self.db.getnodeids(cn, cldb):
607 node = self.db.getnode(cn, nodeid, cldb)
608 if node.has_key(self.db.RETIRED_FLAG):
609 continue
610 # apply filter
611 for t, k, v in filterspec:
612 if t == 0 and node[k] not in v:
613 # link - if this node'd property doesn't appear in the
614 # filterspec's nodeid list, skip it
615 break
616 elif t == 1:
617 # multilink - if any of the nodeids required by the
618 # filterspec aren't in this node's property, then skip
619 # it
620 for value in v:
621 if value not in node[k]:
622 break
623 else:
624 continue
625 break
626 elif t == 2 and not v.search(node[k]):
627 # RE search
628 break
629 elif t == 3 and node[k][:len(v)] != v:
630 # start anchored
631 break
632 elif t == 4 and node[k][-len(v):] != v:
633 # end anchored
634 break
635 elif t == 5 and node[k].find(v) == -1:
636 # substring search
637 break
638 elif t == 6 and node[k] != v:
639 # straight value comparison for the other types
640 break
641 else:
642 l.append((nodeid, node))
643 l.sort()
644 cldb.close()
646 # optimise sort
647 m = []
648 for entry in sort:
649 if entry[0] != '-':
650 m.append(('+', entry))
651 else:
652 m.append((entry[0], entry[1:]))
653 sort = m
655 # optimise group
656 m = []
657 for entry in group:
658 if entry[0] != '-':
659 m.append(('+', entry))
660 else:
661 m.append((entry[0], entry[1:]))
662 group = m
663 # now, sort the result
664 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
665 db = self.db, cl=self):
666 a_id, an = a
667 b_id, bn = b
668 # sort by group and then sort
669 for list in group, sort:
670 for dir, prop in list:
671 # handle the properties that might be "faked"
672 if not an.has_key(prop):
673 an[prop] = cl.get(a_id, prop)
674 av = an[prop]
675 if not bn.has_key(prop):
676 bn[prop] = cl.get(b_id, prop)
677 bv = bn[prop]
679 # sorting is class-specific
680 propclass = properties[prop]
682 # String and Date values are sorted in the natural way
683 if propclass.isStringType:
684 # clean up the strings
685 if av and av[0] in string.uppercase:
686 av = an[prop] = av.lower()
687 if bv and bv[0] in string.uppercase:
688 bv = bn[prop] = bv.lower()
689 if propclass.isStringType or propclass.isDateType:
690 if dir == '+':
691 r = cmp(av, bv)
692 if r != 0: return r
693 elif dir == '-':
694 r = cmp(bv, av)
695 if r != 0: return r
697 # Link properties are sorted according to the value of
698 # the "order" property on the linked nodes if it is
699 # present; or otherwise on the key string of the linked
700 # nodes; or finally on the node ids.
701 elif propclass.isLinkType:
702 link = db.classes[propclass.classname]
703 if av is None and bv is not None: return -1
704 if av is not None and bv is None: return 1
705 if av is None and bv is None: return 0
706 if link.getprops().has_key('order'):
707 if dir == '+':
708 r = cmp(link.get(av, 'order'),
709 link.get(bv, 'order'))
710 if r != 0: return r
711 elif dir == '-':
712 r = cmp(link.get(bv, 'order'),
713 link.get(av, 'order'))
714 if r != 0: return r
715 elif link.getkey():
716 key = link.getkey()
717 if dir == '+':
718 r = cmp(link.get(av, key), link.get(bv, key))
719 if r != 0: return r
720 elif dir == '-':
721 r = cmp(link.get(bv, key), link.get(av, key))
722 if r != 0: return r
723 else:
724 if dir == '+':
725 r = cmp(av, bv)
726 if r != 0: return r
727 elif dir == '-':
728 r = cmp(bv, av)
729 if r != 0: return r
731 # Multilink properties are sorted according to how many
732 # links are present.
733 elif propclass.isMultilinkType:
734 if dir == '+':
735 r = cmp(len(av), len(bv))
736 if r != 0: return r
737 elif dir == '-':
738 r = cmp(len(bv), len(av))
739 if r != 0: return r
740 # end for dir, prop in list:
741 # end for list in sort, group:
742 # if all else fails, compare the ids
743 return cmp(a[0], b[0])
745 l.sort(sortfun)
746 return [i[0] for i in l]
748 def count(self):
749 """Get the number of nodes in this class.
751 If the returned integer is 'numnodes', the ids of all the nodes
752 in this class run from 1 to numnodes, and numnodes+1 will be the
753 id of the next node to be created in this class.
754 """
755 return self.db.countnodes(self.classname)
757 # Manipulating properties:
759 def getprops(self):
760 """Return a dictionary mapping property names to property objects."""
761 d = self.properties.copy()
762 d['id'] = String()
763 return d
765 def addprop(self, **properties):
766 """Add properties to this class.
768 The keyword arguments in 'properties' must map names to property
769 objects, or a TypeError is raised. None of the keys in 'properties'
770 may collide with the names of existing properties, or a ValueError
771 is raised before any properties have been added.
772 """
773 for key in properties.keys():
774 if self.properties.has_key(key):
775 raise ValueError, key
776 self.properties.update(properties)
779 # XXX not in spec
780 class Node:
781 ''' A convenience wrapper for the given node
782 '''
783 def __init__(self, cl, nodeid):
784 self.__dict__['cl'] = cl
785 self.__dict__['nodeid'] = nodeid
786 def keys(self):
787 return self.cl.getprops().keys()
788 def has_key(self, name):
789 return self.cl.getprops().has_key(name)
790 def __getattr__(self, name):
791 if self.__dict__.has_key(name):
792 return self.__dict__['name']
793 try:
794 return self.cl.get(self.nodeid, name)
795 except KeyError, value:
796 raise AttributeError, str(value)
797 def __getitem__(self, name):
798 return self.cl.get(self.nodeid, name)
799 def __setattr__(self, name, value):
800 try:
801 return self.cl.set(self.nodeid, **{name: value})
802 except KeyError, value:
803 raise AttributeError, str(value)
804 def __setitem__(self, name, value):
805 self.cl.set(self.nodeid, **{name: value})
806 def history(self):
807 return self.cl.history(self.nodeid)
808 def retire(self):
809 return self.cl.retire(self.nodeid)
812 def Choice(name, *options):
813 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
814 for i in range(len(options)):
815 cl.create(name=option[i], order=i)
816 return hyperdb.Link(name)
818 #
819 # $Log: not supported by cvs2svn $
820 # Revision 1.12 2001/08/02 06:38:17 richard
821 # Roundupdb now appends "mailing list" information to its messages which
822 # include the e-mail address and web interface address. Templates may
823 # override this in their db classes to include specific information (support
824 # instructions, etc).
825 #
826 # Revision 1.11 2001/08/01 04:24:21 richard
827 # mailgw was assuming certain properties existed on the issues being created.
828 #
829 # Revision 1.10 2001/07/30 02:38:31 richard
830 # get() now has a default arg - for migration only.
831 #
832 # Revision 1.9 2001/07/29 09:28:23 richard
833 # Fixed sorting by clicking on column headings.
834 #
835 # Revision 1.8 2001/07/29 08:27:40 richard
836 # Fixed handling of passed-in values in form elements (ie. during a
837 # drill-down)
838 #
839 # Revision 1.7 2001/07/29 07:01:39 richard
840 # Added vim command to all source so that we don't get no steenkin' tabs :)
841 #
842 # Revision 1.6 2001/07/29 05:36:14 richard
843 # Cleanup of the link label generation.
844 #
845 # Revision 1.5 2001/07/29 04:05:37 richard
846 # Added the fabricated property "id".
847 #
848 # Revision 1.4 2001/07/27 06:25:35 richard
849 # Fixed some of the exceptions so they're the right type.
850 # Removed the str()-ification of node ids so we don't mask oopsy errors any
851 # more.
852 #
853 # Revision 1.3 2001/07/27 05:17:14 richard
854 # just some comments
855 #
856 # Revision 1.2 2001/07/22 12:09:32 richard
857 # Final commit of Grande Splite
858 #
859 # Revision 1.1 2001/07/22 11:58:35 richard
860 # More Grande Splite
861 #
862 #
863 # vim: set filetype=python ts=4 sw=4 et si