Code

Fixed issues with nosy reaction and author copies.
[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.31 2001-11-12 22:01:06 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             if not node.has_key(key):
299                 raise KeyError, key
301             # check to make sure we're not duplicating an existing key
302             if key == self.key and node[key] != value:
303                 try:
304                     self.lookup(value)
305                 except KeyError:
306                     pass
307                 else:
308                     raise ValueError, 'node with key "%s" exists'%value
310             prop = self.properties[key]
312             if isinstance(prop, Link):
313                 link_class = self.properties[key].classname
314                 # if it isn't a number, it's a key
315                 if type(value) != type(''):
316                     raise ValueError, 'link value must be String'
317                 if not num_re.match(value):
318                     try:
319                         value = self.db.classes[link_class].lookup(value)
320                     except:
321                         raise IndexError, 'new property "%s": %s not a %s'%(
322                             key, value, self.properties[key].classname)
324                 if not self.db.hasnode(link_class, value):
325                     raise IndexError, '%s has no node %s'%(link_class, value)
327                 # register the unlink with the old linked node
328                 if node[key] is not None:
329                     self.db.addjournal(link_class, node[key], 'unlink',
330                         (self.classname, nodeid, key))
332                 # register the link with the newly linked node
333                 if value is not None:
334                     self.db.addjournal(link_class, value, 'link',
335                         (self.classname, nodeid, key))
337             elif isinstance(prop, Multilink):
338                 if type(value) != type([]):
339                     raise TypeError, 'new property "%s" not a list of ids'%key
340                 link_class = self.properties[key].classname
341                 l = []
342                 for entry in value:
343                     # if it isn't a number, it's a key
344                     if type(entry) != type(''):
345                         raise ValueError, 'link value must be String'
346                     if not num_re.match(entry):
347                         try:
348                             entry = self.db.classes[link_class].lookup(entry)
349                         except:
350                             raise IndexError, 'new property "%s": %s not a %s'%(
351                                 key, entry, self.properties[key].classname)
352                     l.append(entry)
353                 value = l
354                 propvalues[key] = value
356                 #handle removals
357                 l = node[key]
358                 for id in l[:]:
359                     if id in value:
360                         continue
361                     # register the unlink with the old linked node
362                     self.db.addjournal(link_class, id, 'unlink',
363                         (self.classname, nodeid, key))
364                     l.remove(id)
366                 # handle additions
367                 for id in value:
368                     if not self.db.hasnode(link_class, id):
369                         raise IndexError, '%s has no node %s'%(link_class, id)
370                     if id in l:
371                         continue
372                     # register the link with the newly linked node
373                     self.db.addjournal(link_class, id, 'link',
374                         (self.classname, nodeid, key))
375                     l.append(id)
377             elif isinstance(prop, String):
378                 if value is not None and type(value) != type(''):
379                     raise TypeError, 'new property "%s" not a string'%key
381             elif isinstance(prop, Password):
382                 if not isinstance(value, password.Password):
383                     raise TypeError, 'new property "%s" not a Password'% key
384                 propvalues[key] = value = str(value)
386             elif isinstance(prop, Date):
387                 if not isinstance(value, date.Date):
388                     raise TypeError, 'new property "%s" not a Date'% key
389                 propvalues[key] = value = value.get_tuple()
391             elif isinstance(prop, Interval):
392                 if not isinstance(value, date.Interval):
393                     raise TypeError, 'new property "%s" not an Interval'% key
394                 propvalues[key] = value = value.get_tuple()
396             node[key] = value
398         self.db.setnode(self.classname, nodeid, node)
399         self.db.addjournal(self.classname, nodeid, 'set', propvalues)
401     def retire(self, nodeid):
402         """Retire a node.
403         
404         The properties on the node remain available from the get() method,
405         and the node's id is never reused.
406         
407         Retired nodes are not returned by the find(), list(), or lookup()
408         methods, and other nodes may reuse the values of their key properties.
409         """
410         if self.db.journaltag is None:
411             raise DatabaseError, 'Database open read-only'
412         node = self.db.getnode(self.classname, nodeid)
413         node[self.db.RETIRED_FLAG] = 1
414         self.db.setnode(self.classname, nodeid, node)
415         self.db.addjournal(self.classname, nodeid, 'retired', None)
417     def history(self, nodeid):
418         """Retrieve the journal of edits on a particular node.
420         'nodeid' must be the id of an existing node of this class or an
421         IndexError is raised.
423         The returned list contains tuples of the form
425             (date, tag, action, params)
427         'date' is a Timestamp object specifying the time of the change and
428         'tag' is the journaltag specified when the database was opened.
429         """
430         return self.db.getjournal(self.classname, nodeid)
432     # Locating nodes:
434     def setkey(self, propname):
435         """Select a String property of this class to be the key property.
437         'propname' must be the name of a String property of this class or
438         None, or a TypeError is raised.  The values of the key property on
439         all existing nodes must be unique or a ValueError is raised.
440         """
441         # TODO: validate that the property is a String!
442         self.key = propname
444     def getkey(self):
445         """Return the name of the key property for this class or None."""
446         return self.key
448     def labelprop(self, default_to_id=0):
449         ''' Return the property name for a label for the given node.
451         This method attempts to generate a consistent label for the node.
452         It tries the following in order:
453             1. key property
454             2. "name" property
455             3. "title" property
456             4. first property from the sorted property name list
457         '''
458         k = self.getkey()
459         if  k:
460             return k
461         props = self.getprops()
462         if props.has_key('name'):
463             return 'name'
464         elif props.has_key('title'):
465             return 'title'
466         if default_to_id:
467             return 'id'
468         props = props.keys()
469         props.sort()
470         return props[0]
472     # TODO: set up a separate index db file for this? profile?
473     def lookup(self, keyvalue):
474         """Locate a particular node by its key property and return its id.
476         If this class has no key property, a TypeError is raised.  If the
477         'keyvalue' matches one of the values for the key property among
478         the nodes in this class, the matching node's id is returned;
479         otherwise a KeyError is raised.
480         """
481         cldb = self.db.getclassdb(self.classname)
482         for nodeid in self.db.getnodeids(self.classname, cldb):
483             node = self.db.getnode(self.classname, nodeid, cldb)
484             if node.has_key(self.db.RETIRED_FLAG):
485                 continue
486             if node[self.key] == keyvalue:
487                 return nodeid
488         cldb.close()
489         raise KeyError, keyvalue
491     # XXX: change from spec - allows multiple props to match
492     def find(self, **propspec):
493         """Get the ids of nodes in this class which link to a given node.
495         'propspec' consists of keyword args propname=nodeid   
496           'propname' must be the name of a property in this class, or a
497             KeyError is raised.  That property must be a Link or Multilink
498             property, or a TypeError is raised.
500           'nodeid' must be the id of an existing node in the class linked
501             to by the given property, or an IndexError is raised.
502         """
503         propspec = propspec.items()
504         for propname, nodeid in propspec:
505             # check the prop is OK
506             prop = self.properties[propname]
507             if not isinstance(prop, Link) and not isinstance(prop, Multilink):
508                 raise TypeError, "'%s' not a Link/Multilink property"%propname
509             if not self.db.hasnode(prop.classname, nodeid):
510                 raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
512         # ok, now do the find
513         cldb = self.db.getclassdb(self.classname)
514         l = []
515         for id in self.db.getnodeids(self.classname, cldb):
516             node = self.db.getnode(self.classname, id, cldb)
517             if node.has_key(self.db.RETIRED_FLAG):
518                 continue
519             for propname, nodeid in propspec:
520                 property = node[propname]
521                 if isinstance(prop, Link) and nodeid == property:
522                     l.append(id)
523                 elif isinstance(prop, Multilink) and nodeid in property:
524                     l.append(id)
525         cldb.close()
526         return l
528     def stringFind(self, **requirements):
529         """Locate a particular node by matching a set of its String
530         properties in a caseless search.
532         If the property is not a String property, a TypeError is raised.
533         
534         The return is a list of the id of all nodes that match.
535         """
536         for propname in requirements.keys():
537             prop = self.properties[propname]
538             if isinstance(not prop, String):
539                 raise TypeError, "'%s' not a String property"%propname
540             requirements[propname] = requirements[propname].lower()
541         l = []
542         cldb = self.db.getclassdb(self.classname)
543         for nodeid in self.db.getnodeids(self.classname, cldb):
544             node = self.db.getnode(self.classname, nodeid, cldb)
545             if node.has_key(self.db.RETIRED_FLAG):
546                 continue
547             for key, value in requirements.items():
548                 if node[key] and node[key].lower() != value:
549                     break
550             else:
551                 l.append(nodeid)
552         cldb.close()
553         return l
555     def list(self):
556         """Return a list of the ids of the active nodes in this class."""
557         l = []
558         cn = self.classname
559         cldb = self.db.getclassdb(cn)
560         for nodeid in self.db.getnodeids(cn, cldb):
561             node = self.db.getnode(cn, nodeid, cldb)
562             if node.has_key(self.db.RETIRED_FLAG):
563                 continue
564             l.append(nodeid)
565         l.sort()
566         cldb.close()
567         return l
569     # XXX not in spec
570     def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
571         ''' Return a list of the ids of the active nodes in this class that
572             match the 'filter' spec, sorted by the group spec and then the
573             sort spec
574         '''
575         cn = self.classname
577         # optimise filterspec
578         l = []
579         props = self.getprops()
580         for k, v in filterspec.items():
581             propclass = props[k]
582             if isinstance(propclass, Link):
583                 if type(v) is not type([]):
584                     v = [v]
585                 # replace key values with node ids
586                 u = []
587                 link_class =  self.db.classes[propclass.classname]
588                 for entry in v:
589                     if entry == '-1': entry = None
590                     elif not num_re.match(entry):
591                         try:
592                             entry = link_class.lookup(entry)
593                         except:
594                             raise ValueError, 'property "%s": %s not a %s'%(
595                                 k, entry, self.properties[k].classname)
596                     u.append(entry)
598                 l.append((0, k, u))
599             elif isinstance(propclass, Multilink):
600                 if type(v) is not type([]):
601                     v = [v]
602                 # replace key values with node ids
603                 u = []
604                 link_class =  self.db.classes[propclass.classname]
605                 for entry in v:
606                     if not num_re.match(entry):
607                         try:
608                             entry = link_class.lookup(entry)
609                         except:
610                             raise ValueError, 'new property "%s": %s not a %s'%(
611                                 k, entry, self.properties[k].classname)
612                     u.append(entry)
613                 l.append((1, k, u))
614             elif isinstance(propclass, String):
615                 # simple glob searching
616                 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
617                 v = v.replace('?', '.')
618                 v = v.replace('*', '.*?')
619                 l.append((2, k, re.compile(v, re.I)))
620             else:
621                 l.append((6, k, v))
622         filterspec = l
624         # now, find all the nodes that are active and pass filtering
625         l = []
626         cldb = self.db.getclassdb(cn)
627         for nodeid in self.db.getnodeids(cn, cldb):
628             node = self.db.getnode(cn, nodeid, cldb)
629             if node.has_key(self.db.RETIRED_FLAG):
630                 continue
631             # apply filter
632             for t, k, v in filterspec:
633                 if t == 0 and node[k] not in v:
634                     # link - if this node'd property doesn't appear in the
635                     # filterspec's nodeid list, skip it
636                     break
637                 elif t == 1:
638                     # multilink - if any of the nodeids required by the
639                     # filterspec aren't in this node's property, then skip
640                     # it
641                     for value in v:
642                         if value not in node[k]:
643                             break
644                     else:
645                         continue
646                     break
647                 elif t == 2 and not v.search(node[k]):
648                     # RE search
649                     break
650                 elif t == 6 and node[k] != v:
651                     # straight value comparison for the other types
652                     break
653             else:
654                 l.append((nodeid, node))
655         l.sort()
656         cldb.close()
658         # optimise sort
659         m = []
660         for entry in sort:
661             if entry[0] != '-':
662                 m.append(('+', entry))
663             else:
664                 m.append((entry[0], entry[1:]))
665         sort = m
667         # optimise group
668         m = []
669         for entry in group:
670             if entry[0] != '-':
671                 m.append(('+', entry))
672             else:
673                 m.append((entry[0], entry[1:]))
674         group = m
675         # now, sort the result
676         def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
677                 db = self.db, cl=self):
678             a_id, an = a
679             b_id, bn = b
680             # sort by group and then sort
681             for list in group, sort:
682                 for dir, prop in list:
683                     # handle the properties that might be "faked"
684                     if not an.has_key(prop):
685                         an[prop] = cl.get(a_id, prop)
686                     av = an[prop]
687                     if not bn.has_key(prop):
688                         bn[prop] = cl.get(b_id, prop)
689                     bv = bn[prop]
691                     # sorting is class-specific
692                     propclass = properties[prop]
694                     # String and Date values are sorted in the natural way
695                     if isinstance(propclass, String):
696                         # clean up the strings
697                         if av and av[0] in string.uppercase:
698                             av = an[prop] = av.lower()
699                         if bv and bv[0] in string.uppercase:
700                             bv = bn[prop] = bv.lower()
701                     if (isinstance(propclass, String) or
702                             isinstance(propclass, Date)):
703                         # it might be a string that's really an integer
704                         try:
705                             av = int(av)
706                             bv = int(bv)
707                         except:
708                             pass
709                         if dir == '+':
710                             r = cmp(av, bv)
711                             if r != 0: return r
712                         elif dir == '-':
713                             r = cmp(bv, av)
714                             if r != 0: return r
716                     # Link properties are sorted according to the value of
717                     # the "order" property on the linked nodes if it is
718                     # present; or otherwise on the key string of the linked
719                     # nodes; or finally on  the node ids.
720                     elif isinstance(propclass, Link):
721                         link = db.classes[propclass.classname]
722                         if av is None and bv is not None: return -1
723                         if av is not None and bv is None: return 1
724                         if av is None and bv is None: return 0
725                         if link.getprops().has_key('order'):
726                             if dir == '+':
727                                 r = cmp(link.get(av, 'order'),
728                                     link.get(bv, 'order'))
729                                 if r != 0: return r
730                             elif dir == '-':
731                                 r = cmp(link.get(bv, 'order'),
732                                     link.get(av, 'order'))
733                                 if r != 0: return r
734                         elif link.getkey():
735                             key = link.getkey()
736                             if dir == '+':
737                                 r = cmp(link.get(av, key), link.get(bv, key))
738                                 if r != 0: return r
739                             elif dir == '-':
740                                 r = cmp(link.get(bv, key), link.get(av, key))
741                                 if r != 0: return r
742                         else:
743                             if dir == '+':
744                                 r = cmp(av, bv)
745                                 if r != 0: return r
746                             elif dir == '-':
747                                 r = cmp(bv, av)
748                                 if r != 0: return r
750                     # Multilink properties are sorted according to how many
751                     # links are present.
752                     elif isinstance(propclass, Multilink):
753                         if dir == '+':
754                             r = cmp(len(av), len(bv))
755                             if r != 0: return r
756                         elif dir == '-':
757                             r = cmp(len(bv), len(av))
758                             if r != 0: return r
759                 # end for dir, prop in list:
760             # end for list in sort, group:
761             # if all else fails, compare the ids
762             return cmp(a[0], b[0])
764         l.sort(sortfun)
765         return [i[0] for i in l]
767     def count(self):
768         """Get the number of nodes in this class.
770         If the returned integer is 'numnodes', the ids of all the nodes
771         in this class run from 1 to numnodes, and numnodes+1 will be the
772         id of the next node to be created in this class.
773         """
774         return self.db.countnodes(self.classname)
776     # Manipulating properties:
778     def getprops(self, protected=1):
779         """Return a dictionary mapping property names to property objects.
780            If the "protected" flag is true, we include protected properties -
781            those which may not be modified."""
782         d = self.properties.copy()
783         if protected:
784             d['id'] = String()
785         return d
787     def addprop(self, **properties):
788         """Add properties to this class.
790         The keyword arguments in 'properties' must map names to property
791         objects, or a TypeError is raised.  None of the keys in 'properties'
792         may collide with the names of existing properties, or a ValueError
793         is raised before any properties have been added.
794         """
795         for key in properties.keys():
796             if self.properties.has_key(key):
797                 raise ValueError, key
798         self.properties.update(properties)
801 # XXX not in spec
802 class Node:
803     ''' A convenience wrapper for the given node
804     '''
805     def __init__(self, cl, nodeid):
806         self.__dict__['cl'] = cl
807         self.__dict__['nodeid'] = nodeid
808     def keys(self, protected=1):
809         return self.cl.getprops(protected=protected).keys()
810     def values(self, protected=1):
811         l = []
812         for name in self.cl.getprops(protected=protected).keys():
813             l.append(self.cl.get(self.nodeid, name))
814         return l
815     def items(self, protected=1):
816         l = []
817         for name in self.cl.getprops(protected=protected).keys():
818             l.append((name, self.cl.get(self.nodeid, name)))
819         return l
820     def has_key(self, name):
821         return self.cl.getprops().has_key(name)
822     def __getattr__(self, name):
823         if self.__dict__.has_key(name):
824             return self.__dict__[name]
825         try:
826             return self.cl.get(self.nodeid, name)
827         except KeyError, value:
828             raise AttributeError, str(value)
829     def __getitem__(self, name):
830         return self.cl.get(self.nodeid, name)
831     def __setattr__(self, name, value):
832         try:
833             return self.cl.set(self.nodeid, **{name: value})
834         except KeyError, value:
835             raise AttributeError, str(value)
836     def __setitem__(self, name, value):
837         self.cl.set(self.nodeid, **{name: value})
838     def history(self):
839         return self.cl.history(self.nodeid)
840     def retire(self):
841         return self.cl.retire(self.nodeid)
844 def Choice(name, *options):
845     cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
846     for i in range(len(options)):
847         cl.create(name=option[i], order=i)
848     return hyperdb.Link(name)
851 # $Log: not supported by cvs2svn $
852 # Revision 1.30  2001/11/09 10:11:08  richard
853 #  . roundup-admin now handles all hyperdb exceptions
855 # Revision 1.29  2001/10/27 00:17:41  richard
856 # Made Class.stringFind() do caseless matching.
858 # Revision 1.28  2001/10/21 04:44:50  richard
859 # bug #473124: UI inconsistency with Link fields.
860 #    This also prompted me to fix a fairly long-standing usability issue -
861 #    that of being able to turn off certain filters.
863 # Revision 1.27  2001/10/20 23:44:27  richard
864 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
866 # Revision 1.26  2001/10/16 03:48:01  richard
867 # admin tool now complains if a "find" is attempted with a non-link property.
869 # Revision 1.25  2001/10/11 00:17:51  richard
870 # Reverted a change in hyperdb so the default value for missing property
871 # values in a create() is None and not '' (the empty string.) This obviously
872 # breaks CSV import/export - the string 'None' will be created in an
873 # export/import operation.
875 # Revision 1.24  2001/10/10 03:54:57  richard
876 # Added database importing and exporting through CSV files.
877 # Uses the csv module from object-craft for exporting if it's available.
878 # Requires the csv module for importing.
880 # Revision 1.23  2001/10/09 23:58:10  richard
881 # Moved the data stringification up into the hyperdb.Class class' get, set
882 # and create methods. This means that the data is also stringified for the
883 # journal call, and removes duplication of code from the backends. The
884 # backend code now only sees strings.
886 # Revision 1.22  2001/10/09 07:25:59  richard
887 # Added the Password property type. See "pydoc roundup.password" for
888 # implementation details. Have updated some of the documentation too.
890 # Revision 1.21  2001/10/05 02:23:24  richard
891 #  . roundup-admin create now prompts for property info if none is supplied
892 #    on the command-line.
893 #  . hyperdb Class getprops() method may now return only the mutable
894 #    properties.
895 #  . Login now uses cookies, which makes it a whole lot more flexible. We can
896 #    now support anonymous user access (read-only, unless there's an
897 #    "anonymous" user, in which case write access is permitted). Login
898 #    handling has been moved into cgi_client.Client.main()
899 #  . The "extended" schema is now the default in roundup init.
900 #  . The schemas have had their page headings modified to cope with the new
901 #    login handling. Existing installations should copy the interfaces.py
902 #    file from the roundup lib directory to their instance home.
903 #  . Incorrectly had a Bizar Software copyright on the cgitb.py module from
904 #    Ping - has been removed.
905 #  . Fixed a whole bunch of places in the CGI interface where we should have
906 #    been returning Not Found instead of throwing an exception.
907 #  . Fixed a deviation from the spec: trying to modify the 'id' property of
908 #    an item now throws an exception.
910 # Revision 1.20  2001/10/04 02:12:42  richard
911 # Added nicer command-line item adding: passing no arguments will enter an
912 # interactive more which asks for each property in turn. While I was at it, I
913 # fixed an implementation problem WRT the spec - I wasn't raising a
914 # ValueError if the key property was missing from a create(). Also added a
915 # protected=boolean argument to getprops() so we can list only the mutable
916 # properties (defaults to yes, which lists the immutables).
918 # Revision 1.19  2001/08/29 04:47:18  richard
919 # Fixed CGI client change messages so they actually include the properties
920 # changed (again).
922 # Revision 1.18  2001/08/16 07:34:59  richard
923 # better CGI text searching - but hidden filter fields are disappearing...
925 # Revision 1.17  2001/08/16 06:59:58  richard
926 # all searches use re now - and they're all case insensitive
928 # Revision 1.16  2001/08/15 23:43:18  richard
929 # Fixed some isFooTypes that I missed.
930 # Refactored some code in the CGI code.
932 # Revision 1.15  2001/08/12 06:32:36  richard
933 # using isinstance(blah, Foo) now instead of isFooType
935 # Revision 1.14  2001/08/07 00:24:42  richard
936 # stupid typo
938 # Revision 1.13  2001/08/07 00:15:51  richard
939 # Added the copyright/license notice to (nearly) all files at request of
940 # Bizar Software.
942 # Revision 1.12  2001/08/02 06:38:17  richard
943 # Roundupdb now appends "mailing list" information to its messages which
944 # include the e-mail address and web interface address. Templates may
945 # override this in their db classes to include specific information (support
946 # instructions, etc).
948 # Revision 1.11  2001/08/01 04:24:21  richard
949 # mailgw was assuming certain properties existed on the issues being created.
951 # Revision 1.10  2001/07/30 02:38:31  richard
952 # get() now has a default arg - for migration only.
954 # Revision 1.9  2001/07/29 09:28:23  richard
955 # Fixed sorting by clicking on column headings.
957 # Revision 1.8  2001/07/29 08:27:40  richard
958 # Fixed handling of passed-in values in form elements (ie. during a
959 # drill-down)
961 # Revision 1.7  2001/07/29 07:01:39  richard
962 # Added vim command to all source so that we don't get no steenkin' tabs :)
964 # Revision 1.6  2001/07/29 05:36:14  richard
965 # Cleanup of the link label generation.
967 # Revision 1.5  2001/07/29 04:05:37  richard
968 # Added the fabricated property "id".
970 # Revision 1.4  2001/07/27 06:25:35  richard
971 # Fixed some of the exceptions so they're the right type.
972 # Removed the str()-ification of node ids so we don't mask oopsy errors any
973 # more.
975 # Revision 1.3  2001/07/27 05:17:14  richard
976 # just some comments
978 # Revision 1.2  2001/07/22 12:09:32  richard
979 # Final commit of Grande Splite
981 # Revision 1.1  2001/07/22 11:58:35  richard
982 # More Grande Splite
985 # vim: set filetype=python ts=4 sw=4 et si