Code

all searches use re now - and they're all case insensitive
[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.17 2001-08-16 06:59:58 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             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 isinstance(value, date.Date):
349                     raise TypeError, 'new property "%s" not a Date'% key
351             elif isinstance(prop, Interval):
352                 if not isinstance(value, date.Interval):
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                     l.append((2, k, re.compile(v, re.I)))
575             else:
576                 l.append((6, k, v))
577         filterspec = l
579         # now, find all the nodes that are active and pass filtering
580         l = []
581         cldb = self.db.getclassdb(cn)
582         for nodeid in self.db.getnodeids(cn, cldb):
583             node = self.db.getnode(cn, nodeid, cldb)
584             if node.has_key(self.db.RETIRED_FLAG):
585                 continue
586             # apply filter
587             for t, k, v in filterspec:
588                 if t == 0 and node[k] not in v:
589                     # link - if this node'd property doesn't appear in the
590                     # filterspec's nodeid list, skip it
591                     break
592                 elif t == 1:
593                     # multilink - if any of the nodeids required by the
594                     # filterspec aren't in this node's property, then skip
595                     # it
596                     for value in v:
597                         if value not in node[k]:
598                             break
599                     else:
600                         continue
601                     break
602                 elif t == 2 and not v.search(node[k]):
603                     # RE search
604                     break
605 #                elif t == 3 and node[k][:len(v)] != v:
606 #                    # start anchored
607 #                    break
608 #                elif t == 4 and node[k][-len(v):] != v:
609 #                    # end anchored
610 #                    break
611 #                elif t == 5 and node[k].find(v) == -1:
612 #                    # substring search
613 #                    break
614                 elif t == 6 and node[k] != v:
615                     # straight value comparison for the other types
616                     break
617             else:
618                 l.append((nodeid, node))
619         l.sort()
620         cldb.close()
622         # optimise sort
623         m = []
624         for entry in sort:
625             if entry[0] != '-':
626                 m.append(('+', entry))
627             else:
628                 m.append((entry[0], entry[1:]))
629         sort = m
631         # optimise group
632         m = []
633         for entry in group:
634             if entry[0] != '-':
635                 m.append(('+', entry))
636             else:
637                 m.append((entry[0], entry[1:]))
638         group = m
639         # now, sort the result
640         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
641                 db = self.db, cl=self):
642             a_id, an = a
643             b_id, bn = b
644             # sort by group and then sort
645             for list in group, sort:
646                 for dir, prop in list:
647                     # handle the properties that might be "faked"
648                     if not an.has_key(prop):
649                         an[prop] = cl.get(a_id, prop)
650                     av = an[prop]
651                     if not bn.has_key(prop):
652                         bn[prop] = cl.get(b_id, prop)
653                     bv = bn[prop]
655                     # sorting is class-specific
656                     propclass = properties[prop]
658                     # String and Date values are sorted in the natural way
659                     if isinstance(propclass, String):
660                         # clean up the strings
661                         if av and av[0] in string.uppercase:
662                             av = an[prop] = av.lower()
663                         if bv and bv[0] in string.uppercase:
664                             bv = bn[prop] = bv.lower()
665                     if (isinstance(propclass, String) or
666                             isinstance(propclass, Date)):
667                         if dir == '+':
668                             r = cmp(av, bv)
669                             if r != 0: return r
670                         elif dir == '-':
671                             r = cmp(bv, av)
672                             if r != 0: return r
674                     # Link properties are sorted according to the value of
675                     # the "order" property on the linked nodes if it is
676                     # present; or otherwise on the key string of the linked
677                     # nodes; or finally on  the node ids.
678                     elif isinstance(propclass, Link):
679                         link = db.classes[propclass.classname]
680                         if av is None and bv is not None: return -1
681                         if av is not None and bv is None: return 1
682                         if av is None and bv is None: return 0
683                         if link.getprops().has_key('order'):
684                             if dir == '+':
685                                 r = cmp(link.get(av, 'order'),
686                                     link.get(bv, 'order'))
687                                 if r != 0: return r
688                             elif dir == '-':
689                                 r = cmp(link.get(bv, 'order'),
690                                     link.get(av, 'order'))
691                                 if r != 0: return r
692                         elif link.getkey():
693                             key = link.getkey()
694                             if dir == '+':
695                                 r = cmp(link.get(av, key), link.get(bv, key))
696                                 if r != 0: return r
697                             elif dir == '-':
698                                 r = cmp(link.get(bv, key), link.get(av, key))
699                                 if r != 0: return r
700                         else:
701                             if dir == '+':
702                                 r = cmp(av, bv)
703                                 if r != 0: return r
704                             elif dir == '-':
705                                 r = cmp(bv, av)
706                                 if r != 0: return r
708                     # Multilink properties are sorted according to how many
709                     # links are present.
710                     elif isinstance(propclass, Multilink):
711                         if dir == '+':
712                             r = cmp(len(av), len(bv))
713                             if r != 0: return r
714                         elif dir == '-':
715                             r = cmp(len(bv), len(av))
716                             if r != 0: return r
717                 # end for dir, prop in list:
718             # end for list in sort, group:
719             # if all else fails, compare the ids
720             return cmp(a[0], b[0])
722         l.sort(sortfun)
723         return [i[0] for i in l]
725     def count(self):
726         """Get the number of nodes in this class.
728         If the returned integer is 'numnodes', the ids of all the nodes
729         in this class run from 1 to numnodes, and numnodes+1 will be the
730         id of the next node to be created in this class.
731         """
732         return self.db.countnodes(self.classname)
734     # Manipulating properties:
736     def getprops(self):
737         """Return a dictionary mapping property names to property objects."""
738         d = self.properties.copy()
739         d['id'] = String()
740         return d
742     def addprop(self, **properties):
743         """Add properties to this class.
745         The keyword arguments in 'properties' must map names to property
746         objects, or a TypeError is raised.  None of the keys in 'properties'
747         may collide with the names of existing properties, or a ValueError
748         is raised before any properties have been added.
749         """
750         for key in properties.keys():
751             if self.properties.has_key(key):
752                 raise ValueError, key
753         self.properties.update(properties)
756 # XXX not in spec
757 class Node:
758     ''' A convenience wrapper for the given node
759     '''
760     def __init__(self, cl, nodeid):
761         self.__dict__['cl'] = cl
762         self.__dict__['nodeid'] = nodeid
763     def keys(self):
764         return self.cl.getprops().keys()
765     def has_key(self, name):
766         return self.cl.getprops().has_key(name)
767     def __getattr__(self, name):
768         if self.__dict__.has_key(name):
769             return self.__dict__['name']
770         try:
771             return self.cl.get(self.nodeid, name)
772         except KeyError, value:
773             raise AttributeError, str(value)
774     def __getitem__(self, name):
775         return self.cl.get(self.nodeid, name)
776     def __setattr__(self, name, value):
777         try:
778             return self.cl.set(self.nodeid, **{name: value})
779         except KeyError, value:
780             raise AttributeError, str(value)
781     def __setitem__(self, name, value):
782         self.cl.set(self.nodeid, **{name: value})
783     def history(self):
784         return self.cl.history(self.nodeid)
785     def retire(self):
786         return self.cl.retire(self.nodeid)
789 def Choice(name, *options):
790     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
791     for i in range(len(options)):
792         cl.create(name=option[i], order=i)
793     return hyperdb.Link(name)
796 # $Log: not supported by cvs2svn $
797 # Revision 1.16  2001/08/15 23:43:18  richard
798 # Fixed some isFooTypes that I missed.
799 # Refactored some code in the CGI code.
801 # Revision 1.15  2001/08/12 06:32:36  richard
802 # using isinstance(blah, Foo) now instead of isFooType
804 # Revision 1.14  2001/08/07 00:24:42  richard
805 # stupid typo
807 # Revision 1.13  2001/08/07 00:15:51  richard
808 # Added the copyright/license notice to (nearly) all files at request of
809 # Bizar Software.
811 # Revision 1.12  2001/08/02 06:38:17  richard
812 # Roundupdb now appends "mailing list" information to its messages which
813 # include the e-mail address and web interface address. Templates may
814 # override this in their db classes to include specific information (support
815 # instructions, etc).
817 # Revision 1.11  2001/08/01 04:24:21  richard
818 # mailgw was assuming certain properties existed on the issues being created.
820 # Revision 1.10  2001/07/30 02:38:31  richard
821 # get() now has a default arg - for migration only.
823 # Revision 1.9  2001/07/29 09:28:23  richard
824 # Fixed sorting by clicking on column headings.
826 # Revision 1.8  2001/07/29 08:27:40  richard
827 # Fixed handling of passed-in values in form elements (ie. during a
828 # drill-down)
830 # Revision 1.7  2001/07/29 07:01:39  richard
831 # Added vim command to all source so that we don't get no steenkin' tabs :)
833 # Revision 1.6  2001/07/29 05:36:14  richard
834 # Cleanup of the link label generation.
836 # Revision 1.5  2001/07/29 04:05:37  richard
837 # Added the fabricated property "id".
839 # Revision 1.4  2001/07/27 06:25:35  richard
840 # Fixed some of the exceptions so they're the right type.
841 # Removed the str()-ification of node ids so we don't mask oopsy errors any
842 # more.
844 # Revision 1.3  2001/07/27 05:17:14  richard
845 # just some comments
847 # Revision 1.2  2001/07/22 12:09:32  richard
848 # Final commit of Grande Splite
850 # Revision 1.1  2001/07/22 11:58:35  richard
851 # More Grande Splite
854 # vim: set filetype=python ts=4 sw=4 et si