Code

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