Code

Better handling of new properties.
[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.32 2001-11-21 03:11:28 richard Exp $
20 # standard python modules
21 import cPickle, re, string
23 # roundup modules
24 import date, password
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 Password:
36     """An object designating a Password property."""
37     def __repr__(self):
38         return '<%s>'%self.__class__
40 class Date:
41     """An object designating a Date property."""
42     def __repr__(self):
43         return '<%s>'%self.__class__
45 class Interval:
46     """An object designating an Interval property."""
47     def __repr__(self):
48         return '<%s>'%self.__class__
50 class Link:
51     """An object designating a Link property that links to a
52        node in a specified class."""
53     def __init__(self, classname):
54         self.classname = classname
55     def __repr__(self):
56         return '<%s to "%s">'%(self.__class__, self.classname)
58 class Multilink:
59     """An object designating a Multilink property that links
60        to nodes in a specified class.
61     """
62     def __init__(self, classname):
63         self.classname = classname
64     def __repr__(self):
65         return '<%s to "%s">'%(self.__class__, self.classname)
67 class DatabaseError(ValueError):
68     pass
71 #
72 # the base Database class
73 #
74 class Database:
75     # flag to set on retired entries
76     RETIRED_FLAG = '__hyperdb_retired'
79 _marker = []
80 #
81 # The base Class class
82 #
83 class Class:
84     """The handle to a particular class of nodes in a hyperdatabase."""
86     def __init__(self, db, classname, **properties):
87         """Create a new class with a given name and property specification.
89         'classname' must not collide with the name of an existing class,
90         or a ValueError is raised.  The keyword arguments in 'properties'
91         must map names to property objects, or a TypeError is raised.
92         """
93         self.classname = classname
94         self.properties = properties
95         self.db = db
96         self.key = ''
98         # do the db-related init stuff
99         db.addclass(self)
101     # Editing nodes:
103     def create(self, **propvalues):
104         """Create a new node of this class and return its id.
106         The keyword arguments in 'propvalues' map property names to values.
108         The values of arguments must be acceptable for the types of their
109         corresponding properties or a TypeError is raised.
110         
111         If this class has a key property, it must be present and its value
112         must not collide with other key strings or a ValueError is raised.
113         
114         Any other properties on this class that are missing from the
115         'propvalues' dictionary are set to None.
116         
117         If an id in a link or multilink property does not refer to a valid
118         node, an IndexError is raised.
119         """
120         if propvalues.has_key('id'):
121             raise KeyError, '"id" is reserved'
123         if self.db.journaltag is None:
124             raise DatabaseError, 'Database open read-only'
126         # new node's id
127         newid = str(self.count() + 1)
129         # validate propvalues
130         num_re = re.compile('^\d+$')
131         for key, value in propvalues.items():
132             if key == self.key:
133                 try:
134                     self.lookup(value)
135                 except KeyError:
136                     pass
137                 else:
138                     raise ValueError, 'node with key "%s" exists'%value
140             # try to handle this property
141             try:
142                 prop = self.properties[key]
143             except KeyError:
144                 raise KeyError, '"%s" has no property "%s"'%(self.classname,
145                     key)
147             if isinstance(prop, Link):
148                 if type(value) != type(''):
149                     raise ValueError, 'link value must be String'
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 isinstance(prop, Multilink):
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 isinstance(prop, String):
194                 if type(value) != type(''):
195                     raise TypeError, 'new property "%s" not a string'%key
197             elif isinstance(prop, Password):
198                 if not isinstance(value, password.Password):
199                     raise TypeError, 'new property "%s" not a Password'%key
201             elif isinstance(prop, Date):
202                 if not isinstance(value, date.Date):
203                     raise TypeError, 'new property "%s" not a Date'%key
205             elif isinstance(prop, Interval):
206                 if not isinstance(value, date.Interval):
207                     raise TypeError, 'new property "%s" not an Interval'%key
209         # make sure there's data where there needs to be
210         for key, prop in self.properties.items():
211             if propvalues.has_key(key):
212                 continue
213             if key == self.key:
214                 raise ValueError, 'key property "%s" is required'%key
215             if isinstance(prop, Multilink):
216                 propvalues[key] = []
217             else:
218                 propvalues[key] = None
220         # convert all data to strings
221         for key, prop in self.properties.items():
222             if isinstance(prop, Date):
223                 propvalues[key] = propvalues[key].get_tuple()
224             elif isinstance(prop, Interval):
225                 propvalues[key] = propvalues[key].get_tuple()
226             elif isinstance(prop, Password):
227                 propvalues[key] = str(propvalues[key])
229         # done
230         self.db.addnode(self.classname, newid, propvalues)
231         self.db.addjournal(self.classname, newid, 'create', propvalues)
232         return newid
234     def get(self, nodeid, propname, default=_marker):
235         """Get the value of a property on an existing node of this class.
237         'nodeid' must be the id of an existing node of this class or an
238         IndexError is raised.  'propname' must be the name of a property
239         of this class or a KeyError is raised.
240         """
241         d = self.db.getnode(self.classname, nodeid)
243         # convert the marshalled data to instances
244         for key, prop in self.properties.items():
245             if isinstance(prop, Date):
246                 d[key] = date.Date(d[key])
247             elif isinstance(prop, Interval):
248                 d[key] = date.Interval(d[key])
249             elif isinstance(prop, Password):
250                 p = password.Password()
251                 p.unpack(d[key])
252                 d[key] = p
254         if propname == 'id':
255             return nodeid
256         if not d.has_key(propname) and default is not _marker:
257             return default
258         return d[propname]
260     # XXX not in spec
261     def getnode(self, nodeid):
262         ''' Return a convenience wrapper for the node
263         '''
264         return Node(self, nodeid)
266     def set(self, nodeid, **propvalues):
267         """Modify a property on an existing node of this class.
268         
269         'nodeid' must be the id of an existing node of this class or an
270         IndexError is raised.
272         Each key in 'propvalues' must be the name of a property of this
273         class or a KeyError is raised.
275         All values in 'propvalues' must be acceptable types for their
276         corresponding properties or a TypeError is raised.
278         If the value of the key property is set, it must not collide with
279         other key strings or a ValueError is raised.
281         If the value of a Link or Multilink property contains an invalid
282         node id, a ValueError is raised.
283         """
284         if not propvalues:
285             return
287         if propvalues.has_key('id'):
288             raise KeyError, '"id" is reserved'
290         if self.db.journaltag is None:
291             raise DatabaseError, 'Database open read-only'
293         node = self.db.getnode(self.classname, nodeid)
294         if node.has_key(self.db.RETIRED_FLAG):
295             raise IndexError
296         num_re = re.compile('^\d+$')
297         for key, value in propvalues.items():
298             # check to make sure we're not duplicating an existing key
299             if key == self.key and node[key] != value:
300                 try:
301                     self.lookup(value)
302                 except KeyError:
303                     pass
304                 else:
305                     raise ValueError, 'node with key "%s" exists'%value
307             # this will raise the KeyError if the property isn't valid
308             prop = self.properties[key]
310             if isinstance(prop, Link):
311                 link_class = self.properties[key].classname
312                 # if it isn't a number, it's a key
313                 if type(value) != type(''):
314                     raise ValueError, 'link value must be String'
315                 if not num_re.match(value):
316                     try:
317                         value = self.db.classes[link_class].lookup(value)
318                     except:
319                         raise IndexError, 'new property "%s": %s not a %s'%(
320                             key, value, self.properties[key].classname)
322                 if not self.db.hasnode(link_class, value):
323                     raise IndexError, '%s has no node %s'%(link_class, value)
325                 # register the unlink with the old linked node
326                 if node[key] is not None:
327                     self.db.addjournal(link_class, node[key], 'unlink',
328                         (self.classname, nodeid, key))
330                 # register the link with the newly linked node
331                 if value is not None:
332                     self.db.addjournal(link_class, value, 'link',
333                         (self.classname, nodeid, key))
335             elif isinstance(prop, Multilink):
336                 if type(value) != type([]):
337                     raise TypeError, 'new property "%s" not a list of ids'%key
338                 link_class = self.properties[key].classname
339                 l = []
340                 for entry in value:
341                     # if it isn't a number, it's a key
342                     if type(entry) != type(''):
343                         raise ValueError, 'link value must be String'
344                     if not num_re.match(entry):
345                         try:
346                             entry = self.db.classes[link_class].lookup(entry)
347                         except:
348                             raise IndexError, 'new property "%s": %s not a %s'%(
349                                 key, entry, self.properties[key].classname)
350                     l.append(entry)
351                 value = l
352                 propvalues[key] = value
354                 #handle removals
355                 l = node[key]
356                 for id in l[:]:
357                     if id in value:
358                         continue
359                     # register the unlink with the old linked node
360                     self.db.addjournal(link_class, id, 'unlink',
361                         (self.classname, nodeid, key))
362                     l.remove(id)
364                 # handle additions
365                 for id in value:
366                     if not self.db.hasnode(link_class, id):
367                         raise IndexError, '%s has no node %s'%(link_class, id)
368                     if id in l:
369                         continue
370                     # register the link with the newly linked node
371                     self.db.addjournal(link_class, id, 'link',
372                         (self.classname, nodeid, key))
373                     l.append(id)
375             elif isinstance(prop, String):
376                 if value is not None and type(value) != type(''):
377                     raise TypeError, 'new property "%s" not a string'%key
379             elif isinstance(prop, Password):
380                 if not isinstance(value, password.Password):
381                     raise TypeError, 'new property "%s" not a Password'% key
382                 propvalues[key] = value = str(value)
384             elif isinstance(prop, Date):
385                 if not isinstance(value, date.Date):
386                     raise TypeError, 'new property "%s" not a Date'% key
387                 propvalues[key] = value = value.get_tuple()
389             elif isinstance(prop, Interval):
390                 if not isinstance(value, date.Interval):
391                     raise TypeError, 'new property "%s" not an Interval'% key
392                 propvalues[key] = value = value.get_tuple()
394             node[key] = value
396         self.db.setnode(self.classname, nodeid, node)
397         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
399     def retire(self, nodeid):
400         """Retire a node.
401         
402         The properties on the node remain available from the get() method,
403         and the node's id is never reused.
404         
405         Retired nodes are not returned by the find(), list(), or lookup()
406         methods, and other nodes may reuse the values of their key properties.
407         """
408         if self.db.journaltag is None:
409             raise DatabaseError, 'Database open read-only'
410         node = self.db.getnode(self.classname, nodeid)
411         node[self.db.RETIRED_FLAG] = 1
412         self.db.setnode(self.classname, nodeid, node)
413         self.db.addjournal(self.classname, nodeid, 'retired', None)
415     def history(self, nodeid):
416         """Retrieve the journal of edits on a particular node.
418         'nodeid' must be the id of an existing node of this class or an
419         IndexError is raised.
421         The returned list contains tuples of the form
423             (date, tag, action, params)
425         'date' is a Timestamp object specifying the time of the change and
426         'tag' is the journaltag specified when the database was opened.
427         """
428         return self.db.getjournal(self.classname, nodeid)
430     # Locating nodes:
432     def setkey(self, propname):
433         """Select a String property of this class to be the key property.
435         'propname' must be the name of a String property of this class or
436         None, or a TypeError is raised.  The values of the key property on
437         all existing nodes must be unique or a ValueError is raised.
438         """
439         # TODO: validate that the property is a String!
440         self.key = propname
442     def getkey(self):
443         """Return the name of the key property for this class or None."""
444         return self.key
446     def labelprop(self, default_to_id=0):
447         ''' Return the property name for a label for the given node.
449         This method attempts to generate a consistent label for the node.
450         It tries the following in order:
451             1. key property
452             2. "name" property
453             3. "title" property
454             4. first property from the sorted property name list
455         '''
456         k = self.getkey()
457         if  k:
458             return k
459         props = self.getprops()
460         if props.has_key('name'):
461             return 'name'
462         elif props.has_key('title'):
463             return 'title'
464         if default_to_id:
465             return 'id'
466         props = props.keys()
467         props.sort()
468         return props[0]
470     # TODO: set up a separate index db file for this? profile?
471     def lookup(self, keyvalue):
472         """Locate a particular node by its key property and return its id.
474         If this class has no key property, a TypeError is raised.  If the
475         'keyvalue' matches one of the values for the key property among
476         the nodes in this class, the matching node's id is returned;
477         otherwise a KeyError is raised.
478         """
479         cldb = self.db.getclassdb(self.classname)
480         for nodeid in self.db.getnodeids(self.classname, cldb):
481             node = self.db.getnode(self.classname, nodeid, cldb)
482             if node.has_key(self.db.RETIRED_FLAG):
483                 continue
484             if node[self.key] == keyvalue:
485                 return nodeid
486         cldb.close()
487         raise KeyError, keyvalue
489     # XXX: change from spec - allows multiple props to match
490     def find(self, **propspec):
491         """Get the ids of nodes in this class which link to a given node.
493         'propspec' consists of keyword args propname=nodeid   
494           'propname' must be the name of a property in this class, or a
495             KeyError is raised.  That property must be a Link or Multilink
496             property, or a TypeError is raised.
498           'nodeid' must be the id of an existing node in the class linked
499             to by the given property, or an IndexError is raised.
500         """
501         propspec = propspec.items()
502         for propname, nodeid in propspec:
503             # check the prop is OK
504             prop = self.properties[propname]
505             if not isinstance(prop, Link) and not isinstance(prop, Multilink):
506                 raise TypeError, "'%s' not a Link/Multilink property"%propname
507             if not self.db.hasnode(prop.classname, nodeid):
508                 raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
510         # ok, now do the find
511         cldb = self.db.getclassdb(self.classname)
512         l = []
513         for id in self.db.getnodeids(self.classname, cldb):
514             node = self.db.getnode(self.classname, id, cldb)
515             if node.has_key(self.db.RETIRED_FLAG):
516                 continue
517             for propname, nodeid in propspec:
518                 property = node[propname]
519                 if isinstance(prop, Link) and nodeid == property:
520                     l.append(id)
521                 elif isinstance(prop, Multilink) and nodeid in property:
522                     l.append(id)
523         cldb.close()
524         return l
526     def stringFind(self, **requirements):
527         """Locate a particular node by matching a set of its String
528         properties in a caseless search.
530         If the property is not a String property, a TypeError is raised.
531         
532         The return is a list of the id of all nodes that match.
533         """
534         for propname in requirements.keys():
535             prop = self.properties[propname]
536             if isinstance(not prop, String):
537                 raise TypeError, "'%s' not a String property"%propname
538             requirements[propname] = requirements[propname].lower()
539         l = []
540         cldb = self.db.getclassdb(self.classname)
541         for nodeid in self.db.getnodeids(self.classname, cldb):
542             node = self.db.getnode(self.classname, nodeid, cldb)
543             if node.has_key(self.db.RETIRED_FLAG):
544                 continue
545             for key, value in requirements.items():
546                 if node[key] and node[key].lower() != value:
547                     break
548             else:
549                 l.append(nodeid)
550         cldb.close()
551         return l
553     def list(self):
554         """Return a list of the ids of the active nodes in this class."""
555         l = []
556         cn = self.classname
557         cldb = self.db.getclassdb(cn)
558         for nodeid in self.db.getnodeids(cn, cldb):
559             node = self.db.getnode(cn, nodeid, cldb)
560             if node.has_key(self.db.RETIRED_FLAG):
561                 continue
562             l.append(nodeid)
563         l.sort()
564         cldb.close()
565         return l
567     # XXX not in spec
568     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
569         ''' Return a list of the ids of the active nodes in this class that
570             match the 'filter' spec, sorted by the group spec and then the
571             sort spec
572         '''
573         cn = self.classname
575         # optimise filterspec
576         l = []
577         props = self.getprops()
578         for k, v in filterspec.items():
579             propclass = props[k]
580             if isinstance(propclass, Link):
581                 if type(v) is not type([]):
582                     v = [v]
583                 # replace key values with node ids
584                 u = []
585                 link_class =  self.db.classes[propclass.classname]
586                 for entry in v:
587                     if entry == '-1': entry = None
588                     elif not num_re.match(entry):
589                         try:
590                             entry = link_class.lookup(entry)
591                         except:
592                             raise ValueError, 'property "%s": %s not a %s'%(
593                                 k, entry, self.properties[k].classname)
594                     u.append(entry)
596                 l.append((0, k, u))
597             elif isinstance(propclass, Multilink):
598                 if type(v) is not type([]):
599                     v = [v]
600                 # replace key values with node ids
601                 u = []
602                 link_class =  self.db.classes[propclass.classname]
603                 for entry in v:
604                     if not num_re.match(entry):
605                         try:
606                             entry = link_class.lookup(entry)
607                         except:
608                             raise ValueError, 'new property "%s": %s not a %s'%(
609                                 k, entry, self.properties[k].classname)
610                     u.append(entry)
611                 l.append((1, k, u))
612             elif isinstance(propclass, String):
613                 # simple glob searching
614                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
615                 v = v.replace('?', '.')
616                 v = v.replace('*', '.*?')
617                 l.append((2, k, re.compile(v, re.I)))
618             else:
619                 l.append((6, k, v))
620         filterspec = l
622         # now, find all the nodes that are active and pass filtering
623         l = []
624         cldb = self.db.getclassdb(cn)
625         for nodeid in self.db.getnodeids(cn, cldb):
626             node = self.db.getnode(cn, nodeid, cldb)
627             if node.has_key(self.db.RETIRED_FLAG):
628                 continue
629             # apply filter
630             for t, k, v in filterspec:
631                 if t == 0 and node[k] not in v:
632                     # link - if this node'd property doesn't appear in the
633                     # filterspec's nodeid list, skip it
634                     break
635                 elif t == 1:
636                     # multilink - if any of the nodeids required by the
637                     # filterspec aren't in this node's property, then skip
638                     # it
639                     for value in v:
640                         if value not in node[k]:
641                             break
642                     else:
643                         continue
644                     break
645                 elif t == 2 and not v.search(node[k]):
646                     # RE search
647                     break
648                 elif t == 6 and node[k] != v:
649                     # straight value comparison for the other types
650                     break
651             else:
652                 l.append((nodeid, node))
653         l.sort()
654         cldb.close()
656         # optimise sort
657         m = []
658         for entry in sort:
659             if entry[0] != '-':
660                 m.append(('+', entry))
661             else:
662                 m.append((entry[0], entry[1:]))
663         sort = m
665         # optimise group
666         m = []
667         for entry in group:
668             if entry[0] != '-':
669                 m.append(('+', entry))
670             else:
671                 m.append((entry[0], entry[1:]))
672         group = m
673         # now, sort the result
674         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
675                 db = self.db, cl=self):
676             a_id, an = a
677             b_id, bn = b
678             # sort by group and then sort
679             for list in group, sort:
680                 for dir, prop in list:
681                     # handle the properties that might be "faked"
682                     if not an.has_key(prop):
683                         an[prop] = cl.get(a_id, prop)
684                     av = an[prop]
685                     if not bn.has_key(prop):
686                         bn[prop] = cl.get(b_id, prop)
687                     bv = bn[prop]
689                     # sorting is class-specific
690                     propclass = properties[prop]
692                     # String and Date values are sorted in the natural way
693                     if isinstance(propclass, String):
694                         # clean up the strings
695                         if av and av[0] in string.uppercase:
696                             av = an[prop] = av.lower()
697                         if bv and bv[0] in string.uppercase:
698                             bv = bn[prop] = bv.lower()
699                     if (isinstance(propclass, String) or
700                             isinstance(propclass, Date)):
701                         # it might be a string that's really an integer
702                         try:
703                             av = int(av)
704                             bv = int(bv)
705                         except:
706                             pass
707                         if dir == '+':
708                             r = cmp(av, bv)
709                             if r != 0: return r
710                         elif dir == '-':
711                             r = cmp(bv, av)
712                             if r != 0: return r
714                     # Link properties are sorted according to the value of
715                     # the "order" property on the linked nodes if it is
716                     # present; or otherwise on the key string of the linked
717                     # nodes; or finally on  the node ids.
718                     elif isinstance(propclass, Link):
719                         link = db.classes[propclass.classname]
720                         if av is None and bv is not None: return -1
721                         if av is not None and bv is None: return 1
722                         if av is None and bv is None: return 0
723                         if link.getprops().has_key('order'):
724                             if dir == '+':
725                                 r = cmp(link.get(av, 'order'),
726                                     link.get(bv, 'order'))
727                                 if r != 0: return r
728                             elif dir == '-':
729                                 r = cmp(link.get(bv, 'order'),
730                                     link.get(av, 'order'))
731                                 if r != 0: return r
732                         elif link.getkey():
733                             key = link.getkey()
734                             if dir == '+':
735                                 r = cmp(link.get(av, key), link.get(bv, key))
736                                 if r != 0: return r
737                             elif dir == '-':
738                                 r = cmp(link.get(bv, key), link.get(av, key))
739                                 if r != 0: return r
740                         else:
741                             if dir == '+':
742                                 r = cmp(av, bv)
743                                 if r != 0: return r
744                             elif dir == '-':
745                                 r = cmp(bv, av)
746                                 if r != 0: return r
748                     # Multilink properties are sorted according to how many
749                     # links are present.
750                     elif isinstance(propclass, Multilink):
751                         if dir == '+':
752                             r = cmp(len(av), len(bv))
753                             if r != 0: return r
754                         elif dir == '-':
755                             r = cmp(len(bv), len(av))
756                             if r != 0: return r
757                 # end for dir, prop in list:
758             # end for list in sort, group:
759             # if all else fails, compare the ids
760             return cmp(a[0], b[0])
762         l.sort(sortfun)
763         return [i[0] for i in l]
765     def count(self):
766         """Get the number of nodes in this class.
768         If the returned integer is 'numnodes', the ids of all the nodes
769         in this class run from 1 to numnodes, and numnodes+1 will be the
770         id of the next node to be created in this class.
771         """
772         return self.db.countnodes(self.classname)
774     # Manipulating properties:
776     def getprops(self, protected=1):
777         """Return a dictionary mapping property names to property objects.
778            If the "protected" flag is true, we include protected properties -
779            those which may not be modified."""
780         d = self.properties.copy()
781         if protected:
782             d['id'] = String()
783         return d
785     def addprop(self, **properties):
786         """Add properties to this class.
788         The keyword arguments in 'properties' must map names to property
789         objects, or a TypeError is raised.  None of the keys in 'properties'
790         may collide with the names of existing properties, or a ValueError
791         is raised before any properties have been added.
792         """
793         for key in properties.keys():
794             if self.properties.has_key(key):
795                 raise ValueError, key
796         self.properties.update(properties)
799 # XXX not in spec
800 class Node:
801     ''' A convenience wrapper for the given node
802     '''
803     def __init__(self, cl, nodeid):
804         self.__dict__['cl'] = cl
805         self.__dict__['nodeid'] = nodeid
806     def keys(self, protected=1):
807         return self.cl.getprops(protected=protected).keys()
808     def values(self, protected=1):
809         l = []
810         for name in self.cl.getprops(protected=protected).keys():
811             l.append(self.cl.get(self.nodeid, name))
812         return l
813     def items(self, protected=1):
814         l = []
815         for name in self.cl.getprops(protected=protected).keys():
816             l.append((name, self.cl.get(self.nodeid, name)))
817         return l
818     def has_key(self, name):
819         return self.cl.getprops().has_key(name)
820     def __getattr__(self, name):
821         if self.__dict__.has_key(name):
822             return self.__dict__[name]
823         try:
824             return self.cl.get(self.nodeid, name)
825         except KeyError, value:
826             raise AttributeError, str(value)
827     def __getitem__(self, name):
828         return self.cl.get(self.nodeid, name)
829     def __setattr__(self, name, value):
830         try:
831             return self.cl.set(self.nodeid, **{name: value})
832         except KeyError, value:
833             raise AttributeError, str(value)
834     def __setitem__(self, name, value):
835         self.cl.set(self.nodeid, **{name: value})
836     def history(self):
837         return self.cl.history(self.nodeid)
838     def retire(self):
839         return self.cl.retire(self.nodeid)
842 def Choice(name, *options):
843     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
844     for i in range(len(options)):
845         cl.create(name=option[i], order=i)
846     return hyperdb.Link(name)
849 # $Log: not supported by cvs2svn $
850 # Revision 1.31  2001/11/12 22:01:06  richard
851 # Fixed issues with nosy reaction and author copies.
853 # Revision 1.30  2001/11/09 10:11:08  richard
854 #  . roundup-admin now handles all hyperdb exceptions
856 # Revision 1.29  2001/10/27 00:17:41  richard
857 # Made Class.stringFind() do caseless matching.
859 # Revision 1.28  2001/10/21 04:44:50  richard
860 # bug #473124: UI inconsistency with Link fields.
861 #    This also prompted me to fix a fairly long-standing usability issue -
862 #    that of being able to turn off certain filters.
864 # Revision 1.27  2001/10/20 23:44:27  richard
865 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
867 # Revision 1.26  2001/10/16 03:48:01  richard
868 # admin tool now complains if a "find" is attempted with a non-link property.
870 # Revision 1.25  2001/10/11 00:17:51  richard
871 # Reverted a change in hyperdb so the default value for missing property
872 # values in a create() is None and not '' (the empty string.) This obviously
873 # breaks CSV import/export - the string 'None' will be created in an
874 # export/import operation.
876 # Revision 1.24  2001/10/10 03:54:57  richard
877 # Added database importing and exporting through CSV files.
878 # Uses the csv module from object-craft for exporting if it's available.
879 # Requires the csv module for importing.
881 # Revision 1.23  2001/10/09 23:58:10  richard
882 # Moved the data stringification up into the hyperdb.Class class' get, set
883 # and create methods. This means that the data is also stringified for the
884 # journal call, and removes duplication of code from the backends. The
885 # backend code now only sees strings.
887 # Revision 1.22  2001/10/09 07:25:59  richard
888 # Added the Password property type. See "pydoc roundup.password" for
889 # implementation details. Have updated some of the documentation too.
891 # Revision 1.21  2001/10/05 02:23:24  richard
892 #  . roundup-admin create now prompts for property info if none is supplied
893 #    on the command-line.
894 #  . hyperdb Class getprops() method may now return only the mutable
895 #    properties.
896 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
897 #    now support anonymous user access (read-only, unless there's an
898 #    "anonymous" user, in which case write access is permitted). Login
899 #    handling has been moved into cgi_client.Client.main()
900 #  . The "extended" schema is now the default in roundup init.
901 #  . The schemas have had their page headings modified to cope with the new
902 #    login handling. Existing installations should copy the interfaces.py
903 #    file from the roundup lib directory to their instance home.
904 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
905 #    Ping - has been removed.
906 #  . Fixed a whole bunch of places in the CGI interface where we should have
907 #    been returning Not Found instead of throwing an exception.
908 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
909 #    an item now throws an exception.
911 # Revision 1.20  2001/10/04 02:12:42  richard
912 # Added nicer command-line item adding: passing no arguments will enter an
913 # interactive more which asks for each property in turn. While I was at it, I
914 # fixed an implementation problem WRT the spec - I wasn't raising a
915 # ValueError if the key property was missing from a create(). Also added a
916 # protected=boolean argument to getprops() so we can list only the mutable
917 # properties (defaults to yes, which lists the immutables).
919 # Revision 1.19  2001/08/29 04:47:18  richard
920 # Fixed CGI client change messages so they actually include the properties
921 # changed (again).
923 # Revision 1.18  2001/08/16 07:34:59  richard
924 # better CGI text searching - but hidden filter fields are disappearing...
926 # Revision 1.17  2001/08/16 06:59:58  richard
927 # all searches use re now - and they're all case insensitive
929 # Revision 1.16  2001/08/15 23:43:18  richard
930 # Fixed some isFooTypes that I missed.
931 # Refactored some code in the CGI code.
933 # Revision 1.15  2001/08/12 06:32:36  richard
934 # using isinstance(blah, Foo) now instead of isFooType
936 # Revision 1.14  2001/08/07 00:24:42  richard
937 # stupid typo
939 # Revision 1.13  2001/08/07 00:15:51  richard
940 # Added the copyright/license notice to (nearly) all files at request of
941 # Bizar Software.
943 # Revision 1.12  2001/08/02 06:38:17  richard
944 # Roundupdb now appends "mailing list" information to its messages which
945 # include the e-mail address and web interface address. Templates may
946 # override this in their db classes to include specific information (support
947 # instructions, etc).
949 # Revision 1.11  2001/08/01 04:24:21  richard
950 # mailgw was assuming certain properties existed on the issues being created.
952 # Revision 1.10  2001/07/30 02:38:31  richard
953 # get() now has a default arg - for migration only.
955 # Revision 1.9  2001/07/29 09:28:23  richard
956 # Fixed sorting by clicking on column headings.
958 # Revision 1.8  2001/07/29 08:27:40  richard
959 # Fixed handling of passed-in values in form elements (ie. during a
960 # drill-down)
962 # Revision 1.7  2001/07/29 07:01:39  richard
963 # Added vim command to all source so that we don't get no steenkin' tabs :)
965 # Revision 1.6  2001/07/29 05:36:14  richard
966 # Cleanup of the link label generation.
968 # Revision 1.5  2001/07/29 04:05:37  richard
969 # Added the fabricated property "id".
971 # Revision 1.4  2001/07/27 06:25:35  richard
972 # Fixed some of the exceptions so they're the right type.
973 # Removed the str()-ification of node ids so we don't mask oopsy errors any
974 # more.
976 # Revision 1.3  2001/07/27 05:17:14  richard
977 # just some comments
979 # Revision 1.2  2001/07/22 12:09:32  richard
980 # Final commit of Grande Splite
982 # Revision 1.1  2001/07/22 11:58:35  richard
983 # More Grande Splite
986 # vim: set filetype=python ts=4 sw=4 et si