Code

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