ea4817479b29002fdd3cfb0964754fea0c455b71
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.44 2002-01-02 02:31:38 richard Exp $
20 __doc__ = """
21 Hyperdatabase implementation, especially field types.
22 """
24 # standard python modules
25 import cPickle, re, string, weakref
27 # roundup modules
28 import date, password
31 #
32 # Types
33 #
34 class String:
35 """An object designating a String property."""
36 def __repr__(self):
37 return '<%s>'%self.__class__
39 class Password:
40 """An object designating a Password property."""
41 def __repr__(self):
42 return '<%s>'%self.__class__
44 class Date:
45 """An object designating a Date property."""
46 def __repr__(self):
47 return '<%s>'%self.__class__
49 class Interval:
50 """An object designating an Interval property."""
51 def __repr__(self):
52 return '<%s>'%self.__class__
54 class Link:
55 """An object designating a Link property that links to a
56 node in a specified class."""
57 def __init__(self, classname):
58 self.classname = classname
59 def __repr__(self):
60 return '<%s to "%s">'%(self.__class__, self.classname)
62 class Multilink:
63 """An object designating a Multilink property that links
64 to nodes in a specified class.
65 """
66 def __init__(self, classname):
67 self.classname = classname
68 def __repr__(self):
69 return '<%s to "%s">'%(self.__class__, self.classname)
71 class DatabaseError(ValueError):
72 pass
75 #
76 # the base Database class
77 #
78 class Database:
79 # flag to set on retired entries
80 RETIRED_FLAG = '__hyperdb_retired'
83 _marker = []
84 #
85 # The base Class class
86 #
87 class Class:
88 """The handle to a particular class of nodes in a hyperdatabase."""
90 def __init__(self, db, classname, **properties):
91 """Create a new class with a given name and property specification.
93 'classname' must not collide with the name of an existing class,
94 or a ValueError is raised. The keyword arguments in 'properties'
95 must map names to property objects, or a TypeError is raised.
96 """
97 self.classname = classname
98 self.properties = properties
99 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
100 self.key = ''
102 # do the db-related init stuff
103 db.addclass(self)
105 def __repr__(self):
106 return '<hypderdb.Class "%s">'%self.classname
108 # Editing nodes:
110 def create(self, **propvalues):
111 """Create a new node of this class and return its id.
113 The keyword arguments in 'propvalues' map property names to values.
115 The values of arguments must be acceptable for the types of their
116 corresponding properties or a TypeError is raised.
118 If this class has a key property, it must be present and its value
119 must not collide with other key strings or a ValueError is raised.
121 Any other properties on this class that are missing from the
122 'propvalues' dictionary are set to None.
124 If an id in a link or multilink property does not refer to a valid
125 node, an IndexError is raised.
126 """
127 if propvalues.has_key('id'):
128 raise KeyError, '"id" is reserved'
130 if self.db.journaltag is None:
131 raise DatabaseError, 'Database open read-only'
133 # new node's id
134 newid = str(self.count() + 1)
136 # validate propvalues
137 num_re = re.compile('^\d+$')
138 for key, value in propvalues.items():
139 if key == self.key:
140 try:
141 self.lookup(value)
142 except KeyError:
143 pass
144 else:
145 raise ValueError, 'node with key "%s" exists'%value
147 # try to handle this property
148 try:
149 prop = self.properties[key]
150 except KeyError:
151 raise KeyError, '"%s" has no property "%s"'%(self.classname,
152 key)
154 if isinstance(prop, Link):
155 if type(value) != type(''):
156 raise ValueError, 'link value must be String'
157 link_class = self.properties[key].classname
158 # if it isn't a number, it's a key
159 if not num_re.match(value):
160 try:
161 value = self.db.classes[link_class].lookup(value)
162 except (TypeError, KeyError):
163 raise IndexError, 'new property "%s": %s not a %s'%(
164 key, value, link_class)
165 elif not self.db.hasnode(link_class, value):
166 raise IndexError, '%s has no node %s'%(link_class, value)
168 # save off the value
169 propvalues[key] = value
171 # register the link with the newly linked node
172 self.db.addjournal(link_class, value, 'link',
173 (self.classname, newid, key))
175 elif isinstance(prop, Multilink):
176 if type(value) != type([]):
177 raise TypeError, 'new property "%s" not a list of ids'%key
178 link_class = self.properties[key].classname
179 l = []
180 for entry in value:
181 if type(entry) != type(''):
182 raise ValueError, 'link value must be String'
183 # if it isn't a number, it's a key
184 if not num_re.match(entry):
185 try:
186 entry = self.db.classes[link_class].lookup(entry)
187 except (TypeError, KeyError):
188 raise IndexError, 'new property "%s": %s not a %s'%(
189 key, entry, self.properties[key].classname)
190 l.append(entry)
191 value = l
192 propvalues[key] = value
194 # handle additions
195 for id in value:
196 if not self.db.hasnode(link_class, id):
197 raise IndexError, '%s has no node %s'%(link_class, id)
198 # register the link with the newly linked node
199 self.db.addjournal(link_class, id, 'link',
200 (self.classname, newid, key))
202 elif isinstance(prop, String):
203 if type(value) != type(''):
204 raise TypeError, 'new property "%s" not a string'%key
206 elif isinstance(prop, Password):
207 if not isinstance(value, password.Password):
208 raise TypeError, 'new property "%s" not a Password'%key
210 elif isinstance(prop, Date):
211 if not isinstance(value, date.Date):
212 raise TypeError, 'new property "%s" not a Date'%key
214 elif isinstance(prop, Interval):
215 if not isinstance(value, date.Interval):
216 raise TypeError, 'new property "%s" not an Interval'%key
218 # make sure there's data where there needs to be
219 for key, prop in self.properties.items():
220 if propvalues.has_key(key):
221 continue
222 if key == self.key:
223 raise ValueError, 'key property "%s" is required'%key
224 if isinstance(prop, Multilink):
225 propvalues[key] = []
226 else:
227 propvalues[key] = None
229 # convert all data to strings
230 for key, prop in self.properties.items():
231 if isinstance(prop, Date):
232 propvalues[key] = propvalues[key].get_tuple()
233 elif isinstance(prop, Interval):
234 propvalues[key] = propvalues[key].get_tuple()
235 elif isinstance(prop, Password):
236 propvalues[key] = str(propvalues[key])
238 # done
239 self.db.addnode(self.classname, newid, propvalues)
240 self.db.addjournal(self.classname, newid, 'create', propvalues)
241 return newid
243 def get(self, nodeid, propname, default=_marker, cache=1):
244 """Get the value of a property on an existing node of this class.
246 'nodeid' must be the id of an existing node of this class or an
247 IndexError is raised. 'propname' must be the name of a property
248 of this class or a KeyError is raised.
250 'cache' indicates whether the transaction cache should be queried
251 for the node. If the node has been modified and you need to
252 determine what its values prior to modification are, you need to
253 set cache=0.
254 """
255 if propname == 'id':
256 return nodeid
258 # get the node's dict
259 d = self.db.getnode(self.classname, nodeid, cache=cache)
260 if not d.has_key(propname) and default is not _marker:
261 return default
263 # get the value
264 prop = self.properties[propname]
266 # possibly convert the marshalled data to instances
267 if isinstance(prop, Date):
268 return date.Date(d[propname])
269 elif isinstance(prop, Interval):
270 return date.Interval(d[propname])
271 elif isinstance(prop, Password):
272 p = password.Password()
273 p.unpack(d[propname])
274 return p
276 return d[propname]
278 # XXX not in spec
279 def getnode(self, nodeid, cache=1):
280 ''' Return a convenience wrapper for the node.
282 'nodeid' must be the id of an existing node of this class or an
283 IndexError is raised.
285 'cache' indicates whether the transaction cache should be queried
286 for the node. If the node has been modified and you need to
287 determine what its values prior to modification are, you need to
288 set cache=0.
289 '''
290 return Node(self, nodeid, cache=cache)
292 def set(self, nodeid, **propvalues):
293 """Modify a property on an existing node of this class.
295 'nodeid' must be the id of an existing node of this class or an
296 IndexError is raised.
298 Each key in 'propvalues' must be the name of a property of this
299 class or a KeyError is raised.
301 All values in 'propvalues' must be acceptable types for their
302 corresponding properties or a TypeError is raised.
304 If the value of the key property is set, it must not collide with
305 other key strings or a ValueError is raised.
307 If the value of a Link or Multilink property contains an invalid
308 node id, a ValueError is raised.
309 """
310 if not propvalues:
311 return
313 if propvalues.has_key('id'):
314 raise KeyError, '"id" is reserved'
316 if self.db.journaltag is None:
317 raise DatabaseError, 'Database open read-only'
319 node = self.db.getnode(self.classname, nodeid)
320 if node.has_key(self.db.RETIRED_FLAG):
321 raise IndexError
322 num_re = re.compile('^\d+$')
323 for key, value in propvalues.items():
324 # check to make sure we're not duplicating an existing key
325 if key == self.key and node[key] != value:
326 try:
327 self.lookup(value)
328 except KeyError:
329 pass
330 else:
331 raise ValueError, 'node with key "%s" exists'%value
333 # this will raise the KeyError if the property isn't valid
334 # ... we don't use getprops() here because we only care about
335 # the writeable properties.
336 prop = self.properties[key]
338 if isinstance(prop, Link):
339 link_class = self.properties[key].classname
340 # if it isn't a number, it's a key
341 if type(value) != type(''):
342 raise ValueError, 'link value must be String'
343 if not num_re.match(value):
344 try:
345 value = self.db.classes[link_class].lookup(value)
346 except (TypeError, KeyError):
347 raise IndexError, 'new property "%s": %s not a %s'%(
348 key, value, self.properties[key].classname)
350 if not self.db.hasnode(link_class, value):
351 raise IndexError, '%s has no node %s'%(link_class, value)
353 # register the unlink with the old linked node
354 if node[key] is not None:
355 self.db.addjournal(link_class, node[key], 'unlink',
356 (self.classname, nodeid, key))
358 # register the link with the newly linked node
359 if value is not None:
360 self.db.addjournal(link_class, value, 'link',
361 (self.classname, nodeid, key))
363 elif isinstance(prop, Multilink):
364 if type(value) != type([]):
365 raise TypeError, 'new property "%s" not a list of ids'%key
366 link_class = self.properties[key].classname
367 l = []
368 for entry in value:
369 # if it isn't a number, it's a key
370 if type(entry) != type(''):
371 raise ValueError, 'link value must be String'
372 if not num_re.match(entry):
373 try:
374 entry = self.db.classes[link_class].lookup(entry)
375 except (TypeError, KeyError):
376 raise IndexError, 'new property "%s": %s not a %s'%(
377 key, entry, self.properties[key].classname)
378 l.append(entry)
379 value = l
380 propvalues[key] = value
382 #handle removals
383 l = node[key]
384 for id in l[:]:
385 if id in value:
386 continue
387 # register the unlink with the old linked node
388 self.db.addjournal(link_class, id, 'unlink',
389 (self.classname, nodeid, key))
390 l.remove(id)
392 # handle additions
393 for id in value:
394 if not self.db.hasnode(link_class, id):
395 raise IndexError, '%s has no node %s'%(link_class, id)
396 if id in l:
397 continue
398 # register the link with the newly linked node
399 self.db.addjournal(link_class, id, 'link',
400 (self.classname, nodeid, key))
401 l.append(id)
403 elif isinstance(prop, String):
404 if value is not None and type(value) != type(''):
405 raise TypeError, 'new property "%s" not a string'%key
407 elif isinstance(prop, Password):
408 if not isinstance(value, password.Password):
409 raise TypeError, 'new property "%s" not a Password'% key
410 propvalues[key] = value = str(value)
412 elif isinstance(prop, Date):
413 if not isinstance(value, date.Date):
414 raise TypeError, 'new property "%s" not a Date'% key
415 propvalues[key] = value = value.get_tuple()
417 elif isinstance(prop, Interval):
418 if not isinstance(value, date.Interval):
419 raise TypeError, 'new property "%s" not an Interval'% key
420 propvalues[key] = value = value.get_tuple()
422 node[key] = value
424 self.db.setnode(self.classname, nodeid, node)
425 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
427 def retire(self, nodeid):
428 """Retire a node.
430 The properties on the node remain available from the get() method,
431 and the node's id is never reused.
433 Retired nodes are not returned by the find(), list(), or lookup()
434 methods, and other nodes may reuse the values of their key properties.
435 """
436 if self.db.journaltag is None:
437 raise DatabaseError, 'Database open read-only'
438 node = self.db.getnode(self.classname, nodeid)
439 node[self.db.RETIRED_FLAG] = 1
440 self.db.setnode(self.classname, nodeid, node)
441 self.db.addjournal(self.classname, nodeid, 'retired', None)
443 def history(self, nodeid):
444 """Retrieve the journal of edits on a particular node.
446 'nodeid' must be the id of an existing node of this class or an
447 IndexError is raised.
449 The returned list contains tuples of the form
451 (date, tag, action, params)
453 'date' is a Timestamp object specifying the time of the change and
454 'tag' is the journaltag specified when the database was opened.
455 """
456 return self.db.getjournal(self.classname, nodeid)
458 # Locating nodes:
460 def setkey(self, propname):
461 """Select a String property of this class to be the key property.
463 'propname' must be the name of a String property of this class or
464 None, or a TypeError is raised. The values of the key property on
465 all existing nodes must be unique or a ValueError is raised.
466 """
467 # TODO: validate that the property is a String!
468 self.key = propname
470 def getkey(self):
471 """Return the name of the key property for this class or None."""
472 return self.key
474 def labelprop(self, default_to_id=0):
475 ''' Return the property name for a label for the given node.
477 This method attempts to generate a consistent label for the node.
478 It tries the following in order:
479 1. key property
480 2. "name" property
481 3. "title" property
482 4. first property from the sorted property name list
483 '''
484 k = self.getkey()
485 if k:
486 return k
487 props = self.getprops()
488 if props.has_key('name'):
489 return 'name'
490 elif props.has_key('title'):
491 return 'title'
492 if default_to_id:
493 return 'id'
494 props = props.keys()
495 props.sort()
496 return props[0]
498 # TODO: set up a separate index db file for this? profile?
499 def lookup(self, keyvalue):
500 """Locate a particular node by its key property and return its id.
502 If this class has no key property, a TypeError is raised. If the
503 'keyvalue' matches one of the values for the key property among
504 the nodes in this class, the matching node's id is returned;
505 otherwise a KeyError is raised.
506 """
507 cldb = self.db.getclassdb(self.classname)
508 for nodeid in self.db.getnodeids(self.classname, cldb):
509 node = self.db.getnode(self.classname, nodeid, cldb)
510 if node.has_key(self.db.RETIRED_FLAG):
511 continue
512 if node[self.key] == keyvalue:
513 return nodeid
514 raise KeyError, keyvalue
516 # XXX: change from spec - allows multiple props to match
517 def find(self, **propspec):
518 """Get the ids of nodes in this class which link to a given node.
520 'propspec' consists of keyword args propname=nodeid
521 'propname' must be the name of a property in this class, or a
522 KeyError is raised. That property must be a Link or Multilink
523 property, or a TypeError is raised.
525 'nodeid' must be the id of an existing node in the class linked
526 to by the given property, or an IndexError is raised.
527 """
528 propspec = propspec.items()
529 for propname, nodeid in propspec:
530 # check the prop is OK
531 prop = self.properties[propname]
532 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
533 raise TypeError, "'%s' not a Link/Multilink property"%propname
534 if not self.db.hasnode(prop.classname, nodeid):
535 raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
537 # ok, now do the find
538 cldb = self.db.getclassdb(self.classname)
539 l = []
540 for id in self.db.getnodeids(self.classname, cldb):
541 node = self.db.getnode(self.classname, id, cldb)
542 if node.has_key(self.db.RETIRED_FLAG):
543 continue
544 for propname, nodeid in propspec:
545 property = node[propname]
546 if isinstance(prop, Link) and nodeid == property:
547 l.append(id)
548 elif isinstance(prop, Multilink) and nodeid in property:
549 l.append(id)
550 return l
552 def stringFind(self, **requirements):
553 """Locate a particular node by matching a set of its String
554 properties in a caseless search.
556 If the property is not a String property, a TypeError is raised.
558 The return is a list of the id of all nodes that match.
559 """
560 for propname in requirements.keys():
561 prop = self.properties[propname]
562 if isinstance(not prop, String):
563 raise TypeError, "'%s' not a String property"%propname
564 requirements[propname] = requirements[propname].lower()
565 l = []
566 cldb = self.db.getclassdb(self.classname)
567 for nodeid in self.db.getnodeids(self.classname, cldb):
568 node = self.db.getnode(self.classname, nodeid, cldb)
569 if node.has_key(self.db.RETIRED_FLAG):
570 continue
571 for key, value in requirements.items():
572 if node[key] and node[key].lower() != value:
573 break
574 else:
575 l.append(nodeid)
576 return l
578 def list(self):
579 """Return a list of the ids of the active nodes in this class."""
580 l = []
581 cn = self.classname
582 cldb = self.db.getclassdb(cn)
583 for nodeid in self.db.getnodeids(cn, cldb):
584 node = self.db.getnode(cn, nodeid, cldb)
585 if node.has_key(self.db.RETIRED_FLAG):
586 continue
587 l.append(nodeid)
588 l.sort()
589 return l
591 # XXX not in spec
592 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
593 ''' Return a list of the ids of the active nodes in this class that
594 match the 'filter' spec, sorted by the group spec and then the
595 sort spec
596 '''
597 cn = self.classname
599 # optimise filterspec
600 l = []
601 props = self.getprops()
602 for k, v in filterspec.items():
603 propclass = props[k]
604 if isinstance(propclass, Link):
605 if type(v) is not type([]):
606 v = [v]
607 # replace key values with node ids
608 u = []
609 link_class = self.db.classes[propclass.classname]
610 for entry in v:
611 if entry == '-1': entry = None
612 elif not num_re.match(entry):
613 try:
614 entry = link_class.lookup(entry)
615 except (TypeError,KeyError):
616 raise ValueError, 'property "%s": %s not a %s'%(
617 k, entry, self.properties[k].classname)
618 u.append(entry)
620 l.append((0, k, u))
621 elif isinstance(propclass, Multilink):
622 if type(v) is not type([]):
623 v = [v]
624 # replace key values with node ids
625 u = []
626 link_class = self.db.classes[propclass.classname]
627 for entry in v:
628 if not num_re.match(entry):
629 try:
630 entry = link_class.lookup(entry)
631 except (TypeError,KeyError):
632 raise ValueError, 'new property "%s": %s not a %s'%(
633 k, entry, self.properties[k].classname)
634 u.append(entry)
635 l.append((1, k, u))
636 elif isinstance(propclass, String):
637 # simple glob searching
638 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
639 v = v.replace('?', '.')
640 v = v.replace('*', '.*?')
641 l.append((2, k, re.compile(v, re.I)))
642 else:
643 l.append((6, k, v))
644 filterspec = l
646 # now, find all the nodes that are active and pass filtering
647 l = []
648 cldb = self.db.getclassdb(cn)
649 for nodeid in self.db.getnodeids(cn, cldb):
650 node = self.db.getnode(cn, nodeid, cldb)
651 if node.has_key(self.db.RETIRED_FLAG):
652 continue
653 # apply filter
654 for t, k, v in filterspec:
655 # this node doesn't have this property, so reject it
656 if not node.has_key(k): break
658 if t == 0 and node[k] not in v:
659 # link - if this node'd property doesn't appear in the
660 # filterspec's nodeid list, skip it
661 break
662 elif t == 1:
663 # multilink - if any of the nodeids required by the
664 # filterspec aren't in this node's property, then skip
665 # it
666 for value in v:
667 if value not in node[k]:
668 break
669 else:
670 continue
671 break
672 elif t == 2 and not v.search(node[k]):
673 # RE search
674 break
675 elif t == 6 and node[k] != v:
676 # straight value comparison for the other types
677 break
678 else:
679 l.append((nodeid, node))
680 l.sort()
682 # optimise sort
683 m = []
684 for entry in sort:
685 if entry[0] != '-':
686 m.append(('+', entry))
687 else:
688 m.append((entry[0], entry[1:]))
689 sort = m
691 # optimise group
692 m = []
693 for entry in group:
694 if entry[0] != '-':
695 m.append(('+', entry))
696 else:
697 m.append((entry[0], entry[1:]))
698 group = m
699 # now, sort the result
700 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
701 db = self.db, cl=self):
702 a_id, an = a
703 b_id, bn = b
704 # sort by group and then sort
705 for list in group, sort:
706 for dir, prop in list:
707 # sorting is class-specific
708 propclass = properties[prop]
710 # handle the properties that might be "faked"
711 # also, handle possible missing properties
712 try:
713 if not an.has_key(prop):
714 an[prop] = cl.get(a_id, prop)
715 av = an[prop]
716 except KeyError:
717 # the node doesn't have a value for this property
718 if isinstance(propclass, Multilink): av = []
719 else: av = ''
720 try:
721 if not bn.has_key(prop):
722 bn[prop] = cl.get(b_id, prop)
723 bv = bn[prop]
724 except KeyError:
725 # the node doesn't have a value for this property
726 if isinstance(propclass, Multilink): bv = []
727 else: bv = ''
729 # String and Date values are sorted in the natural way
730 if isinstance(propclass, String):
731 # clean up the strings
732 if av and av[0] in string.uppercase:
733 av = an[prop] = av.lower()
734 if bv and bv[0] in string.uppercase:
735 bv = bn[prop] = bv.lower()
736 if (isinstance(propclass, String) or
737 isinstance(propclass, Date)):
738 # it might be a string that's really an integer
739 try:
740 av = int(av)
741 bv = int(bv)
742 except:
743 pass
744 if dir == '+':
745 r = cmp(av, bv)
746 if r != 0: return r
747 elif dir == '-':
748 r = cmp(bv, av)
749 if r != 0: return r
751 # Link properties are sorted according to the value of
752 # the "order" property on the linked nodes if it is
753 # present; or otherwise on the key string of the linked
754 # nodes; or finally on the node ids.
755 elif isinstance(propclass, Link):
756 link = db.classes[propclass.classname]
757 if av is None and bv is not None: return -1
758 if av is not None and bv is None: return 1
759 if av is None and bv is None: continue
760 if link.getprops().has_key('order'):
761 if dir == '+':
762 r = cmp(link.get(av, 'order'),
763 link.get(bv, 'order'))
764 if r != 0: return r
765 elif dir == '-':
766 r = cmp(link.get(bv, 'order'),
767 link.get(av, 'order'))
768 if r != 0: return r
769 elif link.getkey():
770 key = link.getkey()
771 if dir == '+':
772 r = cmp(link.get(av, key), link.get(bv, key))
773 if r != 0: return r
774 elif dir == '-':
775 r = cmp(link.get(bv, key), link.get(av, key))
776 if r != 0: return r
777 else:
778 if dir == '+':
779 r = cmp(av, bv)
780 if r != 0: return r
781 elif dir == '-':
782 r = cmp(bv, av)
783 if r != 0: return r
785 # Multilink properties are sorted according to how many
786 # links are present.
787 elif isinstance(propclass, Multilink):
788 if dir == '+':
789 r = cmp(len(av), len(bv))
790 if r != 0: return r
791 elif dir == '-':
792 r = cmp(len(bv), len(av))
793 if r != 0: return r
794 # end for dir, prop in list:
795 # end for list in sort, group:
796 # if all else fails, compare the ids
797 return cmp(a[0], b[0])
799 l.sort(sortfun)
800 return [i[0] for i in l]
802 def count(self):
803 """Get the number of nodes in this class.
805 If the returned integer is 'numnodes', the ids of all the nodes
806 in this class run from 1 to numnodes, and numnodes+1 will be the
807 id of the next node to be created in this class.
808 """
809 return self.db.countnodes(self.classname)
811 # Manipulating properties:
813 def getprops(self, protected=1):
814 """Return a dictionary mapping property names to property objects.
815 If the "protected" flag is true, we include protected properties -
816 those which may not be modified."""
817 d = self.properties.copy()
818 if protected:
819 d['id'] = String()
820 return d
822 def addprop(self, **properties):
823 """Add properties to this class.
825 The keyword arguments in 'properties' must map names to property
826 objects, or a TypeError is raised. None of the keys in 'properties'
827 may collide with the names of existing properties, or a ValueError
828 is raised before any properties have been added.
829 """
830 for key in properties.keys():
831 if self.properties.has_key(key):
832 raise ValueError, key
833 self.properties.update(properties)
836 # XXX not in spec
837 class Node:
838 ''' A convenience wrapper for the given node
839 '''
840 def __init__(self, cl, nodeid, cache=1):
841 self.__dict__['cl'] = cl
842 self.__dict__['nodeid'] = nodeid
843 self.cache = cache
844 def keys(self, protected=1):
845 return self.cl.getprops(protected=protected).keys()
846 def values(self, protected=1):
847 l = []
848 for name in self.cl.getprops(protected=protected).keys():
849 l.append(self.cl.get(self.nodeid, name, cache=self.cache))
850 return l
851 def items(self, protected=1):
852 l = []
853 for name in self.cl.getprops(protected=protected).keys():
854 l.append((name, self.cl.get(self.nodeid, name, cache=self.cache)))
855 return l
856 def has_key(self, name):
857 return self.cl.getprops().has_key(name)
858 def __getattr__(self, name):
859 if self.__dict__.has_key(name):
860 return self.__dict__[name]
861 try:
862 return self.cl.get(self.nodeid, name, cache=self.cache)
863 except KeyError, value:
864 # we trap this but re-raise it as AttributeError - all other
865 # exceptions should pass through untrapped
866 pass
867 # nope, no such attribute
868 raise AttributeError, str(value)
869 def __getitem__(self, name):
870 return self.cl.get(self.nodeid, name, cache=self.cache)
871 def __setattr__(self, name, value):
872 try:
873 return self.cl.set(self.nodeid, **{name: value})
874 except KeyError, value:
875 raise AttributeError, str(value)
876 def __setitem__(self, name, value):
877 self.cl.set(self.nodeid, **{name: value})
878 def history(self):
879 return self.cl.history(self.nodeid)
880 def retire(self):
881 return self.cl.retire(self.nodeid)
884 def Choice(name, *options):
885 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
886 for i in range(len(options)):
887 cl.create(name=option[i], order=i)
888 return hyperdb.Link(name)
890 #
891 # $Log: not supported by cvs2svn $
892 # Revision 1.43 2001/12/20 06:13:24 rochecompaan
893 # Bugs fixed:
894 # . Exception handling in hyperdb for strings-that-look-like numbers got
895 # lost somewhere
896 # . Internet Explorer submits full path for filename - we now strip away
897 # the path
898 # Features added:
899 # . Link and multilink properties are now displayed sorted in the cgi
900 # interface
901 #
902 # Revision 1.42 2001/12/16 10:53:37 richard
903 # take a copy of the node dict so that the subsequent set
904 # operation doesn't modify the oldvalues structure
905 #
906 # Revision 1.41 2001/12/15 23:47:47 richard
907 # Cleaned up some bare except statements
908 #
909 # Revision 1.40 2001/12/14 23:42:57 richard
910 # yuck, a gdbm instance tests false :(
911 # I've left the debugging code in - it should be removed one day if we're ever
912 # _really_ anal about performace :)
913 #
914 # Revision 1.39 2001/12/02 05:06:16 richard
915 # . We now use weakrefs in the Classes to keep the database reference, so
916 # the close() method on the database is no longer needed.
917 # I bumped the minimum python requirement up to 2.1 accordingly.
918 # . #487480 ] roundup-server
919 # . #487476 ] INSTALL.txt
920 #
921 # I also cleaned up the change message / post-edit stuff in the cgi client.
922 # There's now a clearly marked "TODO: append the change note" where I believe
923 # the change note should be added there. The "changes" list will obviously
924 # have to be modified to be a dict of the changes, or somesuch.
925 #
926 # More testing needed.
927 #
928 # Revision 1.38 2001/12/01 07:17:50 richard
929 # . We now have basic transaction support! Information is only written to
930 # the database when the commit() method is called. Only the anydbm
931 # backend is modified in this way - neither of the bsddb backends have been.
932 # The mail, admin and cgi interfaces all use commit (except the admin tool
933 # doesn't have a commit command, so interactive users can't commit...)
934 # . Fixed login/registration forwarding the user to the right page (or not,
935 # on a failure)
936 #
937 # Revision 1.37 2001/11/28 21:55:35 richard
938 # . login_action and newuser_action return values were being ignored
939 # . Woohoo! Found that bloody re-login bug that was killing the mail
940 # gateway.
941 # (also a minor cleanup in hyperdb)
942 #
943 # Revision 1.36 2001/11/27 03:16:09 richard
944 # Another place that wasn't handling missing properties.
945 #
946 # Revision 1.35 2001/11/22 15:46:42 jhermann
947 # Added module docstrings to all modules.
948 #
949 # Revision 1.34 2001/11/21 04:04:43 richard
950 # *sigh* more missing value handling
951 #
952 # Revision 1.33 2001/11/21 03:40:54 richard
953 # more new property handling
954 #
955 # Revision 1.32 2001/11/21 03:11:28 richard
956 # Better handling of new properties.
957 #
958 # Revision 1.31 2001/11/12 22:01:06 richard
959 # Fixed issues with nosy reaction and author copies.
960 #
961 # Revision 1.30 2001/11/09 10:11:08 richard
962 # . roundup-admin now handles all hyperdb exceptions
963 #
964 # Revision 1.29 2001/10/27 00:17:41 richard
965 # Made Class.stringFind() do caseless matching.
966 #
967 # Revision 1.28 2001/10/21 04:44:50 richard
968 # bug #473124: UI inconsistency with Link fields.
969 # This also prompted me to fix a fairly long-standing usability issue -
970 # that of being able to turn off certain filters.
971 #
972 # Revision 1.27 2001/10/20 23:44:27 richard
973 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
974 #
975 # Revision 1.26 2001/10/16 03:48:01 richard
976 # admin tool now complains if a "find" is attempted with a non-link property.
977 #
978 # Revision 1.25 2001/10/11 00:17:51 richard
979 # Reverted a change in hyperdb so the default value for missing property
980 # values in a create() is None and not '' (the empty string.) This obviously
981 # breaks CSV import/export - the string 'None' will be created in an
982 # export/import operation.
983 #
984 # Revision 1.24 2001/10/10 03:54:57 richard
985 # Added database importing and exporting through CSV files.
986 # Uses the csv module from object-craft for exporting if it's available.
987 # Requires the csv module for importing.
988 #
989 # Revision 1.23 2001/10/09 23:58:10 richard
990 # Moved the data stringification up into the hyperdb.Class class' get, set
991 # and create methods. This means that the data is also stringified for the
992 # journal call, and removes duplication of code from the backends. The
993 # backend code now only sees strings.
994 #
995 # Revision 1.22 2001/10/09 07:25:59 richard
996 # Added the Password property type. See "pydoc roundup.password" for
997 # implementation details. Have updated some of the documentation too.
998 #
999 # Revision 1.21 2001/10/05 02:23:24 richard
1000 # . roundup-admin create now prompts for property info if none is supplied
1001 # on the command-line.
1002 # . hyperdb Class getprops() method may now return only the mutable
1003 # properties.
1004 # . Login now uses cookies, which makes it a whole lot more flexible. We can
1005 # now support anonymous user access (read-only, unless there's an
1006 # "anonymous" user, in which case write access is permitted). Login
1007 # handling has been moved into cgi_client.Client.main()
1008 # . The "extended" schema is now the default in roundup init.
1009 # . The schemas have had their page headings modified to cope with the new
1010 # login handling. Existing installations should copy the interfaces.py
1011 # file from the roundup lib directory to their instance home.
1012 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
1013 # Ping - has been removed.
1014 # . Fixed a whole bunch of places in the CGI interface where we should have
1015 # been returning Not Found instead of throwing an exception.
1016 # . Fixed a deviation from the spec: trying to modify the 'id' property of
1017 # an item now throws an exception.
1018 #
1019 # Revision 1.20 2001/10/04 02:12:42 richard
1020 # Added nicer command-line item adding: passing no arguments will enter an
1021 # interactive more which asks for each property in turn. While I was at it, I
1022 # fixed an implementation problem WRT the spec - I wasn't raising a
1023 # ValueError if the key property was missing from a create(). Also added a
1024 # protected=boolean argument to getprops() so we can list only the mutable
1025 # properties (defaults to yes, which lists the immutables).
1026 #
1027 # Revision 1.19 2001/08/29 04:47:18 richard
1028 # Fixed CGI client change messages so they actually include the properties
1029 # changed (again).
1030 #
1031 # Revision 1.18 2001/08/16 07:34:59 richard
1032 # better CGI text searching - but hidden filter fields are disappearing...
1033 #
1034 # Revision 1.17 2001/08/16 06:59:58 richard
1035 # all searches use re now - and they're all case insensitive
1036 #
1037 # Revision 1.16 2001/08/15 23:43:18 richard
1038 # Fixed some isFooTypes that I missed.
1039 # Refactored some code in the CGI code.
1040 #
1041 # Revision 1.15 2001/08/12 06:32:36 richard
1042 # using isinstance(blah, Foo) now instead of isFooType
1043 #
1044 # Revision 1.14 2001/08/07 00:24:42 richard
1045 # stupid typo
1046 #
1047 # Revision 1.13 2001/08/07 00:15:51 richard
1048 # Added the copyright/license notice to (nearly) all files at request of
1049 # Bizar Software.
1050 #
1051 # Revision 1.12 2001/08/02 06:38:17 richard
1052 # Roundupdb now appends "mailing list" information to its messages which
1053 # include the e-mail address and web interface address. Templates may
1054 # override this in their db classes to include specific information (support
1055 # instructions, etc).
1056 #
1057 # Revision 1.11 2001/08/01 04:24:21 richard
1058 # mailgw was assuming certain properties existed on the issues being created.
1059 #
1060 # Revision 1.10 2001/07/30 02:38:31 richard
1061 # get() now has a default arg - for migration only.
1062 #
1063 # Revision 1.9 2001/07/29 09:28:23 richard
1064 # Fixed sorting by clicking on column headings.
1065 #
1066 # Revision 1.8 2001/07/29 08:27:40 richard
1067 # Fixed handling of passed-in values in form elements (ie. during a
1068 # drill-down)
1069 #
1070 # Revision 1.7 2001/07/29 07:01:39 richard
1071 # Added vim command to all source so that we don't get no steenkin' tabs :)
1072 #
1073 # Revision 1.6 2001/07/29 05:36:14 richard
1074 # Cleanup of the link label generation.
1075 #
1076 # Revision 1.5 2001/07/29 04:05:37 richard
1077 # Added the fabricated property "id".
1078 #
1079 # Revision 1.4 2001/07/27 06:25:35 richard
1080 # Fixed some of the exceptions so they're the right type.
1081 # Removed the str()-ification of node ids so we don't mask oopsy errors any
1082 # more.
1083 #
1084 # Revision 1.3 2001/07/27 05:17:14 richard
1085 # just some comments
1086 #
1087 # Revision 1.2 2001/07/22 12:09:32 richard
1088 # Final commit of Grande Splite
1089 #
1090 # Revision 1.1 2001/07/22 11:58:35 richard
1091 # More Grande Splite
1092 #
1093 #
1094 # vim: set filetype=python ts=4 sw=4 et si