Code

using isinstance(blah, Foo) now instead of isFooType
[roundup.git] / roundup / hyperdb.py
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.
105         
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.
108         
109         Any other properties on this class that are missing from the
110         'propvalues' dictionary are set to None.
111         
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.
235         
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.
362         
363         The properties on the node remain available from the get() method,
364         and the node's id is never reused.
365         
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.
490         
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)
808 # $Log: not supported by cvs2svn $
809 # Revision 1.14  2001/08/07 00:24:42  richard
810 # stupid typo
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.
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).
822 # Revision 1.11  2001/08/01 04:24:21  richard
823 # mailgw was assuming certain properties existed on the issues being created.
825 # Revision 1.10  2001/07/30 02:38:31  richard
826 # get() now has a default arg - for migration only.
828 # Revision 1.9  2001/07/29 09:28:23  richard
829 # Fixed sorting by clicking on column headings.
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)
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 :)
838 # Revision 1.6  2001/07/29 05:36:14  richard
839 # Cleanup of the link label generation.
841 # Revision 1.5  2001/07/29 04:05:37  richard
842 # Added the fabricated property "id".
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.
849 # Revision 1.3  2001/07/27 05:17:14  richard
850 # just some comments
852 # Revision 1.2  2001/07/22 12:09:32  richard
853 # Final commit of Grande Splite
855 # Revision 1.1  2001/07/22 11:58:35  richard
856 # More Grande Splite
859 # vim: set filetype=python ts=4 sw=4 et si