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.42 2001-12-16 10:53:37 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):
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.
249 """
250 if propname == 'id':
251 return nodeid
253 # get the node's dict
254 d = self.db.getnode(self.classname, nodeid)
255 if not d.has_key(propname) and default is not _marker:
256 return default
258 # get the value
259 prop = self.properties[propname]
261 # possibly convert the marshalled data to instances
262 if isinstance(prop, Date):
263 return date.Date(d[propname])
264 elif isinstance(prop, Interval):
265 return date.Interval(d[propname])
266 elif isinstance(prop, Password):
267 p = password.Password()
268 p.unpack(d[propname])
269 return p
271 return d[propname]
273 # XXX not in spec
274 def getnode(self, nodeid):
275 ''' Return a convenience wrapper for the node
276 '''
277 return Node(self, nodeid)
279 def set(self, nodeid, **propvalues):
280 """Modify a property on an existing node of this class.
282 'nodeid' must be the id of an existing node of this class or an
283 IndexError is raised.
285 Each key in 'propvalues' must be the name of a property of this
286 class or a KeyError is raised.
288 All values in 'propvalues' must be acceptable types for their
289 corresponding properties or a TypeError is raised.
291 If the value of the key property is set, it must not collide with
292 other key strings or a ValueError is raised.
294 If the value of a Link or Multilink property contains an invalid
295 node id, a ValueError is raised.
296 """
297 if not propvalues:
298 return
300 if propvalues.has_key('id'):
301 raise KeyError, '"id" is reserved'
303 if self.db.journaltag is None:
304 raise DatabaseError, 'Database open read-only'
306 node = self.db.getnode(self.classname, nodeid)
307 if node.has_key(self.db.RETIRED_FLAG):
308 raise IndexError
309 num_re = re.compile('^\d+$')
310 for key, value in propvalues.items():
311 # check to make sure we're not duplicating an existing key
312 if key == self.key and node[key] != value:
313 try:
314 self.lookup(value)
315 except KeyError:
316 pass
317 else:
318 raise ValueError, 'node with key "%s" exists'%value
320 # this will raise the KeyError if the property isn't valid
321 # ... we don't use getprops() here because we only care about
322 # the writeable properties.
323 prop = self.properties[key]
325 if isinstance(prop, Link):
326 link_class = self.properties[key].classname
327 # if it isn't a number, it's a key
328 if type(value) != type(''):
329 raise ValueError, 'link value must be String'
330 if not num_re.match(value):
331 try:
332 value = self.db.classes[link_class].lookup(value)
333 except (TypeError, KeyError):
334 raise IndexError, 'new property "%s": %s not a %s'%(
335 key, value, self.properties[key].classname)
337 if not self.db.hasnode(link_class, value):
338 raise IndexError, '%s has no node %s'%(link_class, value)
340 # register the unlink with the old linked node
341 if node[key] is not None:
342 self.db.addjournal(link_class, node[key], 'unlink',
343 (self.classname, nodeid, key))
345 # register the link with the newly linked node
346 if value is not None:
347 self.db.addjournal(link_class, value, 'link',
348 (self.classname, nodeid, key))
350 elif isinstance(prop, Multilink):
351 if type(value) != type([]):
352 raise TypeError, 'new property "%s" not a list of ids'%key
353 link_class = self.properties[key].classname
354 l = []
355 for entry in value:
356 # if it isn't a number, it's a key
357 if type(entry) != type(''):
358 raise ValueError, 'link value must be String'
359 if not num_re.match(entry):
360 try:
361 entry = self.db.classes[link_class].lookup(entry)
362 except (TypeError, KeyError):
363 raise IndexError, 'new property "%s": %s not a %s'%(
364 key, entry, self.properties[key].classname)
365 l.append(entry)
366 value = l
367 propvalues[key] = value
369 #handle removals
370 l = node[key]
371 for id in l[:]:
372 if id in value:
373 continue
374 # register the unlink with the old linked node
375 self.db.addjournal(link_class, id, 'unlink',
376 (self.classname, nodeid, key))
377 l.remove(id)
379 # handle additions
380 for id in value:
381 if not self.db.hasnode(link_class, id):
382 raise IndexError, '%s has no node %s'%(link_class, id)
383 if id in l:
384 continue
385 # register the link with the newly linked node
386 self.db.addjournal(link_class, id, 'link',
387 (self.classname, nodeid, key))
388 l.append(id)
390 elif isinstance(prop, String):
391 if value is not None and type(value) != type(''):
392 raise TypeError, 'new property "%s" not a string'%key
394 elif isinstance(prop, Password):
395 if not isinstance(value, password.Password):
396 raise TypeError, 'new property "%s" not a Password'% key
397 propvalues[key] = value = str(value)
399 elif isinstance(prop, Date):
400 if not isinstance(value, date.Date):
401 raise TypeError, 'new property "%s" not a Date'% key
402 propvalues[key] = value = value.get_tuple()
404 elif isinstance(prop, Interval):
405 if not isinstance(value, date.Interval):
406 raise TypeError, 'new property "%s" not an Interval'% key
407 propvalues[key] = value = value.get_tuple()
409 node[key] = value
411 self.db.setnode(self.classname, nodeid, node)
412 self.db.addjournal(self.classname, nodeid, 'set', propvalues)
414 def retire(self, nodeid):
415 """Retire a node.
417 The properties on the node remain available from the get() method,
418 and the node's id is never reused.
420 Retired nodes are not returned by the find(), list(), or lookup()
421 methods, and other nodes may reuse the values of their key properties.
422 """
423 if self.db.journaltag is None:
424 raise DatabaseError, 'Database open read-only'
425 node = self.db.getnode(self.classname, nodeid)
426 node[self.db.RETIRED_FLAG] = 1
427 self.db.setnode(self.classname, nodeid, node)
428 self.db.addjournal(self.classname, nodeid, 'retired', None)
430 def history(self, nodeid):
431 """Retrieve the journal of edits on a particular node.
433 'nodeid' must be the id of an existing node of this class or an
434 IndexError is raised.
436 The returned list contains tuples of the form
438 (date, tag, action, params)
440 'date' is a Timestamp object specifying the time of the change and
441 'tag' is the journaltag specified when the database was opened.
442 """
443 return self.db.getjournal(self.classname, nodeid)
445 # Locating nodes:
447 def setkey(self, propname):
448 """Select a String property of this class to be the key property.
450 'propname' must be the name of a String property of this class or
451 None, or a TypeError is raised. The values of the key property on
452 all existing nodes must be unique or a ValueError is raised.
453 """
454 # TODO: validate that the property is a String!
455 self.key = propname
457 def getkey(self):
458 """Return the name of the key property for this class or None."""
459 return self.key
461 def labelprop(self, default_to_id=0):
462 ''' Return the property name for a label for the given node.
464 This method attempts to generate a consistent label for the node.
465 It tries the following in order:
466 1. key property
467 2. "name" property
468 3. "title" property
469 4. first property from the sorted property name list
470 '''
471 k = self.getkey()
472 if k:
473 return k
474 props = self.getprops()
475 if props.has_key('name'):
476 return 'name'
477 elif props.has_key('title'):
478 return 'title'
479 if default_to_id:
480 return 'id'
481 props = props.keys()
482 props.sort()
483 return props[0]
485 # TODO: set up a separate index db file for this? profile?
486 def lookup(self, keyvalue):
487 """Locate a particular node by its key property and return its id.
489 If this class has no key property, a TypeError is raised. If the
490 'keyvalue' matches one of the values for the key property among
491 the nodes in this class, the matching node's id is returned;
492 otherwise a KeyError is raised.
493 """
494 cldb = self.db.getclassdb(self.classname)
495 for nodeid in self.db.getnodeids(self.classname, cldb):
496 node = self.db.getnode(self.classname, nodeid, cldb)
497 if node.has_key(self.db.RETIRED_FLAG):
498 continue
499 if node[self.key] == keyvalue:
500 return nodeid
501 raise KeyError, keyvalue
503 # XXX: change from spec - allows multiple props to match
504 def find(self, **propspec):
505 """Get the ids of nodes in this class which link to a given node.
507 'propspec' consists of keyword args propname=nodeid
508 'propname' must be the name of a property in this class, or a
509 KeyError is raised. That property must be a Link or Multilink
510 property, or a TypeError is raised.
512 'nodeid' must be the id of an existing node in the class linked
513 to by the given property, or an IndexError is raised.
514 """
515 propspec = propspec.items()
516 for propname, nodeid in propspec:
517 # check the prop is OK
518 prop = self.properties[propname]
519 if not isinstance(prop, Link) and not isinstance(prop, Multilink):
520 raise TypeError, "'%s' not a Link/Multilink property"%propname
521 if not self.db.hasnode(prop.classname, nodeid):
522 raise ValueError, '%s has no node %s'%(prop.classname, nodeid)
524 # ok, now do the find
525 cldb = self.db.getclassdb(self.classname)
526 l = []
527 for id in self.db.getnodeids(self.classname, cldb):
528 node = self.db.getnode(self.classname, id, cldb)
529 if node.has_key(self.db.RETIRED_FLAG):
530 continue
531 for propname, nodeid in propspec:
532 property = node[propname]
533 if isinstance(prop, Link) and nodeid == property:
534 l.append(id)
535 elif isinstance(prop, Multilink) and nodeid in property:
536 l.append(id)
537 return l
539 def stringFind(self, **requirements):
540 """Locate a particular node by matching a set of its String
541 properties in a caseless search.
543 If the property is not a String property, a TypeError is raised.
545 The return is a list of the id of all nodes that match.
546 """
547 for propname in requirements.keys():
548 prop = self.properties[propname]
549 if isinstance(not prop, String):
550 raise TypeError, "'%s' not a String property"%propname
551 requirements[propname] = requirements[propname].lower()
552 l = []
553 cldb = self.db.getclassdb(self.classname)
554 for nodeid in self.db.getnodeids(self.classname, cldb):
555 node = self.db.getnode(self.classname, nodeid, cldb)
556 if node.has_key(self.db.RETIRED_FLAG):
557 continue
558 for key, value in requirements.items():
559 if node[key] and node[key].lower() != value:
560 break
561 else:
562 l.append(nodeid)
563 return l
565 def list(self):
566 """Return a list of the ids of the active nodes in this class."""
567 l = []
568 cn = self.classname
569 cldb = self.db.getclassdb(cn)
570 for nodeid in self.db.getnodeids(cn, cldb):
571 node = self.db.getnode(cn, nodeid, cldb)
572 if node.has_key(self.db.RETIRED_FLAG):
573 continue
574 l.append(nodeid)
575 l.sort()
576 return l
578 # XXX not in spec
579 def filter(self, filterspec, sort, group, num_re = re.compile('^\d+$')):
580 ''' Return a list of the ids of the active nodes in this class that
581 match the 'filter' spec, sorted by the group spec and then the
582 sort spec
583 '''
584 cn = self.classname
586 # optimise filterspec
587 l = []
588 props = self.getprops()
589 for k, v in filterspec.items():
590 propclass = props[k]
591 if isinstance(propclass, Link):
592 if type(v) is not type([]):
593 v = [v]
594 # replace key values with node ids
595 u = []
596 link_class = self.db.classes[propclass.classname]
597 for entry in v:
598 if entry == '-1': entry = None
599 elif not num_re.match(entry):
600 try:
601 entry = link_class.lookup(entry)
602 except (TypeError,KeyError):
603 raise ValueError, 'property "%s": %s not a %s'%(
604 k, entry, self.properties[k].classname)
605 u.append(entry)
607 l.append((0, k, u))
608 elif isinstance(propclass, Multilink):
609 if type(v) is not type([]):
610 v = [v]
611 # replace key values with node ids
612 u = []
613 link_class = self.db.classes[propclass.classname]
614 for entry in v:
615 if not num_re.match(entry):
616 try:
617 entry = link_class.lookup(entry)
618 except (TypeError,KeyError):
619 raise ValueError, 'new property "%s": %s not a %s'%(
620 k, entry, self.properties[k].classname)
621 u.append(entry)
622 l.append((1, k, u))
623 elif isinstance(propclass, String):
624 # simple glob searching
625 v = re.sub(r'([\|\{\}\\\.\+\[\]\(\)])', r'\\\1', v)
626 v = v.replace('?', '.')
627 v = v.replace('*', '.*?')
628 l.append((2, k, re.compile(v, re.I)))
629 else:
630 l.append((6, k, v))
631 filterspec = l
633 # now, find all the nodes that are active and pass filtering
634 l = []
635 cldb = self.db.getclassdb(cn)
636 for nodeid in self.db.getnodeids(cn, cldb):
637 node = self.db.getnode(cn, nodeid, cldb)
638 if node.has_key(self.db.RETIRED_FLAG):
639 continue
640 # apply filter
641 for t, k, v in filterspec:
642 # this node doesn't have this property, so reject it
643 if not node.has_key(k): break
645 if t == 0 and node[k] not in v:
646 # link - if this node'd property doesn't appear in the
647 # filterspec's nodeid list, skip it
648 break
649 elif t == 1:
650 # multilink - if any of the nodeids required by the
651 # filterspec aren't in this node's property, then skip
652 # it
653 for value in v:
654 if value not in node[k]:
655 break
656 else:
657 continue
658 break
659 elif t == 2 and not v.search(node[k]):
660 # RE search
661 break
662 elif t == 6 and node[k] != v:
663 # straight value comparison for the other types
664 break
665 else:
666 l.append((nodeid, node))
667 l.sort()
669 # optimise sort
670 m = []
671 for entry in sort:
672 if entry[0] != '-':
673 m.append(('+', entry))
674 else:
675 m.append((entry[0], entry[1:]))
676 sort = m
678 # optimise group
679 m = []
680 for entry in group:
681 if entry[0] != '-':
682 m.append(('+', entry))
683 else:
684 m.append((entry[0], entry[1:]))
685 group = m
686 # now, sort the result
687 def sortfun(a, b, sort=sort, group=group, properties=self.getprops(),
688 db = self.db, cl=self):
689 a_id, an = a
690 b_id, bn = b
691 # sort by group and then sort
692 for list in group, sort:
693 for dir, prop in list:
694 # sorting is class-specific
695 propclass = properties[prop]
697 # handle the properties that might be "faked"
698 # also, handle possible missing properties
699 try:
700 if not an.has_key(prop):
701 an[prop] = cl.get(a_id, prop)
702 av = an[prop]
703 except KeyError:
704 # the node doesn't have a value for this property
705 if isinstance(propclass, Multilink): av = []
706 else: av = ''
707 try:
708 if not bn.has_key(prop):
709 bn[prop] = cl.get(b_id, prop)
710 bv = bn[prop]
711 except KeyError:
712 # the node doesn't have a value for this property
713 if isinstance(propclass, Multilink): bv = []
714 else: bv = ''
716 # String and Date values are sorted in the natural way
717 if isinstance(propclass, String):
718 # clean up the strings
719 if av and av[0] in string.uppercase:
720 av = an[prop] = av.lower()
721 if bv and bv[0] in string.uppercase:
722 bv = bn[prop] = bv.lower()
723 if (isinstance(propclass, String) or
724 isinstance(propclass, Date)):
725 # it might be a string that's really an integer
726 av = int(av)
727 bv = int(bv)
728 if dir == '+':
729 r = cmp(av, bv)
730 if r != 0: return r
731 elif dir == '-':
732 r = cmp(bv, av)
733 if r != 0: return r
735 # Link properties are sorted according to the value of
736 # the "order" property on the linked nodes if it is
737 # present; or otherwise on the key string of the linked
738 # nodes; or finally on the node ids.
739 elif isinstance(propclass, Link):
740 link = db.classes[propclass.classname]
741 if av is None and bv is not None: return -1
742 if av is not None and bv is None: return 1
743 if av is None and bv is None: return 0
744 if link.getprops().has_key('order'):
745 if dir == '+':
746 r = cmp(link.get(av, 'order'),
747 link.get(bv, 'order'))
748 if r != 0: return r
749 elif dir == '-':
750 r = cmp(link.get(bv, 'order'),
751 link.get(av, 'order'))
752 if r != 0: return r
753 elif link.getkey():
754 key = link.getkey()
755 if dir == '+':
756 r = cmp(link.get(av, key), link.get(bv, key))
757 if r != 0: return r
758 elif dir == '-':
759 r = cmp(link.get(bv, key), link.get(av, key))
760 if r != 0: return r
761 else:
762 if dir == '+':
763 r = cmp(av, bv)
764 if r != 0: return r
765 elif dir == '-':
766 r = cmp(bv, av)
767 if r != 0: return r
769 # Multilink properties are sorted according to how many
770 # links are present.
771 elif isinstance(propclass, Multilink):
772 if dir == '+':
773 r = cmp(len(av), len(bv))
774 if r != 0: return r
775 elif dir == '-':
776 r = cmp(len(bv), len(av))
777 if r != 0: return r
778 # end for dir, prop in list:
779 # end for list in sort, group:
780 # if all else fails, compare the ids
781 return cmp(a[0], b[0])
783 l.sort(sortfun)
784 return [i[0] for i in l]
786 def count(self):
787 """Get the number of nodes in this class.
789 If the returned integer is 'numnodes', the ids of all the nodes
790 in this class run from 1 to numnodes, and numnodes+1 will be the
791 id of the next node to be created in this class.
792 """
793 return self.db.countnodes(self.classname)
795 # Manipulating properties:
797 def getprops(self, protected=1):
798 """Return a dictionary mapping property names to property objects.
799 If the "protected" flag is true, we include protected properties -
800 those which may not be modified."""
801 d = self.properties.copy()
802 if protected:
803 d['id'] = String()
804 return d
806 def addprop(self, **properties):
807 """Add properties to this class.
809 The keyword arguments in 'properties' must map names to property
810 objects, or a TypeError is raised. None of the keys in 'properties'
811 may collide with the names of existing properties, or a ValueError
812 is raised before any properties have been added.
813 """
814 for key in properties.keys():
815 if self.properties.has_key(key):
816 raise ValueError, key
817 self.properties.update(properties)
820 # XXX not in spec
821 class Node:
822 ''' A convenience wrapper for the given node
823 '''
824 def __init__(self, cl, nodeid):
825 self.__dict__['cl'] = cl
826 self.__dict__['nodeid'] = nodeid
827 def keys(self, protected=1):
828 return self.cl.getprops(protected=protected).keys()
829 def values(self, protected=1):
830 l = []
831 for name in self.cl.getprops(protected=protected).keys():
832 l.append(self.cl.get(self.nodeid, name))
833 return l
834 def items(self, protected=1):
835 l = []
836 for name in self.cl.getprops(protected=protected).keys():
837 l.append((name, self.cl.get(self.nodeid, name)))
838 return l
839 def has_key(self, name):
840 return self.cl.getprops().has_key(name)
841 def __getattr__(self, name):
842 if self.__dict__.has_key(name):
843 return self.__dict__[name]
844 try:
845 return self.cl.get(self.nodeid, name)
846 except KeyError, value:
847 # we trap this but re-raise it as AttributeError - all other
848 # exceptions should pass through untrapped
849 pass
850 # nope, no such attribute
851 raise AttributeError, str(value)
852 def __getitem__(self, name):
853 return self.cl.get(self.nodeid, name)
854 def __setattr__(self, name, value):
855 try:
856 return self.cl.set(self.nodeid, **{name: value})
857 except KeyError, value:
858 raise AttributeError, str(value)
859 def __setitem__(self, name, value):
860 self.cl.set(self.nodeid, **{name: value})
861 def history(self):
862 return self.cl.history(self.nodeid)
863 def retire(self):
864 return self.cl.retire(self.nodeid)
867 def Choice(name, *options):
868 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
869 for i in range(len(options)):
870 cl.create(name=option[i], order=i)
871 return hyperdb.Link(name)
873 #
874 # $Log: not supported by cvs2svn $
875 # Revision 1.41 2001/12/15 23:47:47 richard
876 # Cleaned up some bare except statements
877 #
878 # Revision 1.40 2001/12/14 23:42:57 richard
879 # yuck, a gdbm instance tests false :(
880 # I've left the debugging code in - it should be removed one day if we're ever
881 # _really_ anal about performace :)
882 #
883 # Revision 1.39 2001/12/02 05:06:16 richard
884 # . We now use weakrefs in the Classes to keep the database reference, so
885 # the close() method on the database is no longer needed.
886 # I bumped the minimum python requirement up to 2.1 accordingly.
887 # . #487480 ] roundup-server
888 # . #487476 ] INSTALL.txt
889 #
890 # I also cleaned up the change message / post-edit stuff in the cgi client.
891 # There's now a clearly marked "TODO: append the change note" where I believe
892 # the change note should be added there. The "changes" list will obviously
893 # have to be modified to be a dict of the changes, or somesuch.
894 #
895 # More testing needed.
896 #
897 # Revision 1.38 2001/12/01 07:17:50 richard
898 # . We now have basic transaction support! Information is only written to
899 # the database when the commit() method is called. Only the anydbm
900 # backend is modified in this way - neither of the bsddb backends have been.
901 # The mail, admin and cgi interfaces all use commit (except the admin tool
902 # doesn't have a commit command, so interactive users can't commit...)
903 # . Fixed login/registration forwarding the user to the right page (or not,
904 # on a failure)
905 #
906 # Revision 1.37 2001/11/28 21:55:35 richard
907 # . login_action and newuser_action return values were being ignored
908 # . Woohoo! Found that bloody re-login bug that was killing the mail
909 # gateway.
910 # (also a minor cleanup in hyperdb)
911 #
912 # Revision 1.36 2001/11/27 03:16:09 richard
913 # Another place that wasn't handling missing properties.
914 #
915 # Revision 1.35 2001/11/22 15:46:42 jhermann
916 # Added module docstrings to all modules.
917 #
918 # Revision 1.34 2001/11/21 04:04:43 richard
919 # *sigh* more missing value handling
920 #
921 # Revision 1.33 2001/11/21 03:40:54 richard
922 # more new property handling
923 #
924 # Revision 1.32 2001/11/21 03:11:28 richard
925 # Better handling of new properties.
926 #
927 # Revision 1.31 2001/11/12 22:01:06 richard
928 # Fixed issues with nosy reaction and author copies.
929 #
930 # Revision 1.30 2001/11/09 10:11:08 richard
931 # . roundup-admin now handles all hyperdb exceptions
932 #
933 # Revision 1.29 2001/10/27 00:17:41 richard
934 # Made Class.stringFind() do caseless matching.
935 #
936 # Revision 1.28 2001/10/21 04:44:50 richard
937 # bug #473124: UI inconsistency with Link fields.
938 # This also prompted me to fix a fairly long-standing usability issue -
939 # that of being able to turn off certain filters.
940 #
941 # Revision 1.27 2001/10/20 23:44:27 richard
942 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
943 #
944 # Revision 1.26 2001/10/16 03:48:01 richard
945 # admin tool now complains if a "find" is attempted with a non-link property.
946 #
947 # Revision 1.25 2001/10/11 00:17:51 richard
948 # Reverted a change in hyperdb so the default value for missing property
949 # values in a create() is None and not '' (the empty string.) This obviously
950 # breaks CSV import/export - the string 'None' will be created in an
951 # export/import operation.
952 #
953 # Revision 1.24 2001/10/10 03:54:57 richard
954 # Added database importing and exporting through CSV files.
955 # Uses the csv module from object-craft for exporting if it's available.
956 # Requires the csv module for importing.
957 #
958 # Revision 1.23 2001/10/09 23:58:10 richard
959 # Moved the data stringification up into the hyperdb.Class class' get, set
960 # and create methods. This means that the data is also stringified for the
961 # journal call, and removes duplication of code from the backends. The
962 # backend code now only sees strings.
963 #
964 # Revision 1.22 2001/10/09 07:25:59 richard
965 # Added the Password property type. See "pydoc roundup.password" for
966 # implementation details. Have updated some of the documentation too.
967 #
968 # Revision 1.21 2001/10/05 02:23:24 richard
969 # . roundup-admin create now prompts for property info if none is supplied
970 # on the command-line.
971 # . hyperdb Class getprops() method may now return only the mutable
972 # properties.
973 # . Login now uses cookies, which makes it a whole lot more flexible. We can
974 # now support anonymous user access (read-only, unless there's an
975 # "anonymous" user, in which case write access is permitted). Login
976 # handling has been moved into cgi_client.Client.main()
977 # . The "extended" schema is now the default in roundup init.
978 # . The schemas have had their page headings modified to cope with the new
979 # login handling. Existing installations should copy the interfaces.py
980 # file from the roundup lib directory to their instance home.
981 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
982 # Ping - has been removed.
983 # . Fixed a whole bunch of places in the CGI interface where we should have
984 # been returning Not Found instead of throwing an exception.
985 # . Fixed a deviation from the spec: trying to modify the 'id' property of
986 # an item now throws an exception.
987 #
988 # Revision 1.20 2001/10/04 02:12:42 richard
989 # Added nicer command-line item adding: passing no arguments will enter an
990 # interactive more which asks for each property in turn. While I was at it, I
991 # fixed an implementation problem WRT the spec - I wasn't raising a
992 # ValueError if the key property was missing from a create(). Also added a
993 # protected=boolean argument to getprops() so we can list only the mutable
994 # properties (defaults to yes, which lists the immutables).
995 #
996 # Revision 1.19 2001/08/29 04:47:18 richard
997 # Fixed CGI client change messages so they actually include the properties
998 # changed (again).
999 #
1000 # Revision 1.18 2001/08/16 07:34:59 richard
1001 # better CGI text searching - but hidden filter fields are disappearing...
1002 #
1003 # Revision 1.17 2001/08/16 06:59:58 richard
1004 # all searches use re now - and they're all case insensitive
1005 #
1006 # Revision 1.16 2001/08/15 23:43:18 richard
1007 # Fixed some isFooTypes that I missed.
1008 # Refactored some code in the CGI code.
1009 #
1010 # Revision 1.15 2001/08/12 06:32:36 richard
1011 # using isinstance(blah, Foo) now instead of isFooType
1012 #
1013 # Revision 1.14 2001/08/07 00:24:42 richard
1014 # stupid typo
1015 #
1016 # Revision 1.13 2001/08/07 00:15:51 richard
1017 # Added the copyright/license notice to (nearly) all files at request of
1018 # Bizar Software.
1019 #
1020 # Revision 1.12 2001/08/02 06:38:17 richard
1021 # Roundupdb now appends "mailing list" information to its messages which
1022 # include the e-mail address and web interface address. Templates may
1023 # override this in their db classes to include specific information (support
1024 # instructions, etc).
1025 #
1026 # Revision 1.11 2001/08/01 04:24:21 richard
1027 # mailgw was assuming certain properties existed on the issues being created.
1028 #
1029 # Revision 1.10 2001/07/30 02:38:31 richard
1030 # get() now has a default arg - for migration only.
1031 #
1032 # Revision 1.9 2001/07/29 09:28:23 richard
1033 # Fixed sorting by clicking on column headings.
1034 #
1035 # Revision 1.8 2001/07/29 08:27:40 richard
1036 # Fixed handling of passed-in values in form elements (ie. during a
1037 # drill-down)
1038 #
1039 # Revision 1.7 2001/07/29 07:01:39 richard
1040 # Added vim command to all source so that we don't get no steenkin' tabs :)
1041 #
1042 # Revision 1.6 2001/07/29 05:36:14 richard
1043 # Cleanup of the link label generation.
1044 #
1045 # Revision 1.5 2001/07/29 04:05:37 richard
1046 # Added the fabricated property "id".
1047 #
1048 # Revision 1.4 2001/07/27 06:25:35 richard
1049 # Fixed some of the exceptions so they're the right type.
1050 # Removed the str()-ification of node ids so we don't mask oopsy errors any
1051 # more.
1052 #
1053 # Revision 1.3 2001/07/27 05:17:14 richard
1054 # just some comments
1055 #
1056 # Revision 1.2 2001/07/22 12:09:32 richard
1057 # Final commit of Grande Splite
1058 #
1059 # Revision 1.1 2001/07/22 11:58:35 richard
1060 # More Grande Splite
1061 #
1062 #
1063 # vim: set filetype=python ts=4 sw=4 et si