Code

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