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.40 2001-12-14 23:42:57 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:
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:
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:
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:
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:
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:
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 try:
727 av = int(av)
728 bv = int(bv)
729 except:
730 pass
731 if dir == '+':
732 r = cmp(av, bv)
733 if r != 0: return r
734 elif dir == '-':
735 r = cmp(bv, av)
736 if r != 0: return r
738 # Link properties are sorted according to the value of
739 # the "order" property on the linked nodes if it is
740 # present; or otherwise on the key string of the linked
741 # nodes; or finally on the node ids.
742 elif isinstance(propclass, Link):
743 link = db.classes[propclass.classname]
744 if av is None and bv is not None: return -1
745 if av is not None and bv is None: return 1
746 if av is None and bv is None: return 0
747 if link.getprops().has_key('order'):
748 if dir == '+':
749 r = cmp(link.get(av, 'order'),
750 link.get(bv, 'order'))
751 if r != 0: return r
752 elif dir == '-':
753 r = cmp(link.get(bv, 'order'),
754 link.get(av, 'order'))
755 if r != 0: return r
756 elif link.getkey():
757 key = link.getkey()
758 if dir == '+':
759 r = cmp(link.get(av, key), link.get(bv, key))
760 if r != 0: return r
761 elif dir == '-':
762 r = cmp(link.get(bv, key), link.get(av, key))
763 if r != 0: return r
764 else:
765 if dir == '+':
766 r = cmp(av, bv)
767 if r != 0: return r
768 elif dir == '-':
769 r = cmp(bv, av)
770 if r != 0: return r
772 # Multilink properties are sorted according to how many
773 # links are present.
774 elif isinstance(propclass, Multilink):
775 if dir == '+':
776 r = cmp(len(av), len(bv))
777 if r != 0: return r
778 elif dir == '-':
779 r = cmp(len(bv), len(av))
780 if r != 0: return r
781 # end for dir, prop in list:
782 # end for list in sort, group:
783 # if all else fails, compare the ids
784 return cmp(a[0], b[0])
786 l.sort(sortfun)
787 return [i[0] for i in l]
789 def count(self):
790 """Get the number of nodes in this class.
792 If the returned integer is 'numnodes', the ids of all the nodes
793 in this class run from 1 to numnodes, and numnodes+1 will be the
794 id of the next node to be created in this class.
795 """
796 return self.db.countnodes(self.classname)
798 # Manipulating properties:
800 def getprops(self, protected=1):
801 """Return a dictionary mapping property names to property objects.
802 If the "protected" flag is true, we include protected properties -
803 those which may not be modified."""
804 d = self.properties.copy()
805 if protected:
806 d['id'] = String()
807 return d
809 def addprop(self, **properties):
810 """Add properties to this class.
812 The keyword arguments in 'properties' must map names to property
813 objects, or a TypeError is raised. None of the keys in 'properties'
814 may collide with the names of existing properties, or a ValueError
815 is raised before any properties have been added.
816 """
817 for key in properties.keys():
818 if self.properties.has_key(key):
819 raise ValueError, key
820 self.properties.update(properties)
823 # XXX not in spec
824 class Node:
825 ''' A convenience wrapper for the given node
826 '''
827 def __init__(self, cl, nodeid):
828 self.__dict__['cl'] = cl
829 self.__dict__['nodeid'] = nodeid
830 def keys(self, protected=1):
831 return self.cl.getprops(protected=protected).keys()
832 def values(self, protected=1):
833 l = []
834 for name in self.cl.getprops(protected=protected).keys():
835 l.append(self.cl.get(self.nodeid, name))
836 return l
837 def items(self, protected=1):
838 l = []
839 for name in self.cl.getprops(protected=protected).keys():
840 l.append((name, self.cl.get(self.nodeid, name)))
841 return l
842 def has_key(self, name):
843 return self.cl.getprops().has_key(name)
844 def __getattr__(self, name):
845 if self.__dict__.has_key(name):
846 return self.__dict__[name]
847 try:
848 return self.cl.get(self.nodeid, name)
849 except KeyError, value:
850 raise AttributeError, str(value)
851 def __getitem__(self, name):
852 return self.cl.get(self.nodeid, name)
853 def __setattr__(self, name, value):
854 try:
855 return self.cl.set(self.nodeid, **{name: value})
856 except KeyError, value:
857 raise AttributeError, str(value)
858 def __setitem__(self, name, value):
859 self.cl.set(self.nodeid, **{name: value})
860 def history(self):
861 return self.cl.history(self.nodeid)
862 def retire(self):
863 return self.cl.retire(self.nodeid)
866 def Choice(name, *options):
867 cl = Class(db, name, name=hyperdb.String(), order=hyperdb.String())
868 for i in range(len(options)):
869 cl.create(name=option[i], order=i)
870 return hyperdb.Link(name)
872 #
873 # $Log: not supported by cvs2svn $
874 # Revision 1.39 2001/12/02 05:06:16 richard
875 # . We now use weakrefs in the Classes to keep the database reference, so
876 # the close() method on the database is no longer needed.
877 # I bumped the minimum python requirement up to 2.1 accordingly.
878 # . #487480 ] roundup-server
879 # . #487476 ] INSTALL.txt
880 #
881 # I also cleaned up the change message / post-edit stuff in the cgi client.
882 # There's now a clearly marked "TODO: append the change note" where I believe
883 # the change note should be added there. The "changes" list will obviously
884 # have to be modified to be a dict of the changes, or somesuch.
885 #
886 # More testing needed.
887 #
888 # Revision 1.38 2001/12/01 07:17:50 richard
889 # . We now have basic transaction support! Information is only written to
890 # the database when the commit() method is called. Only the anydbm
891 # backend is modified in this way - neither of the bsddb backends have been.
892 # The mail, admin and cgi interfaces all use commit (except the admin tool
893 # doesn't have a commit command, so interactive users can't commit...)
894 # . Fixed login/registration forwarding the user to the right page (or not,
895 # on a failure)
896 #
897 # Revision 1.37 2001/11/28 21:55:35 richard
898 # . login_action and newuser_action return values were being ignored
899 # . Woohoo! Found that bloody re-login bug that was killing the mail
900 # gateway.
901 # (also a minor cleanup in hyperdb)
902 #
903 # Revision 1.36 2001/11/27 03:16:09 richard
904 # Another place that wasn't handling missing properties.
905 #
906 # Revision 1.35 2001/11/22 15:46:42 jhermann
907 # Added module docstrings to all modules.
908 #
909 # Revision 1.34 2001/11/21 04:04:43 richard
910 # *sigh* more missing value handling
911 #
912 # Revision 1.33 2001/11/21 03:40:54 richard
913 # more new property handling
914 #
915 # Revision 1.32 2001/11/21 03:11:28 richard
916 # Better handling of new properties.
917 #
918 # Revision 1.31 2001/11/12 22:01:06 richard
919 # Fixed issues with nosy reaction and author copies.
920 #
921 # Revision 1.30 2001/11/09 10:11:08 richard
922 # . roundup-admin now handles all hyperdb exceptions
923 #
924 # Revision 1.29 2001/10/27 00:17:41 richard
925 # Made Class.stringFind() do caseless matching.
926 #
927 # Revision 1.28 2001/10/21 04:44:50 richard
928 # bug #473124: UI inconsistency with Link fields.
929 # This also prompted me to fix a fairly long-standing usability issue -
930 # that of being able to turn off certain filters.
931 #
932 # Revision 1.27 2001/10/20 23:44:27 richard
933 # Hyperdatabase sorts strings-that-look-like-numbers as numbers now.
934 #
935 # Revision 1.26 2001/10/16 03:48:01 richard
936 # admin tool now complains if a "find" is attempted with a non-link property.
937 #
938 # Revision 1.25 2001/10/11 00:17:51 richard
939 # Reverted a change in hyperdb so the default value for missing property
940 # values in a create() is None and not '' (the empty string.) This obviously
941 # breaks CSV import/export - the string 'None' will be created in an
942 # export/import operation.
943 #
944 # Revision 1.24 2001/10/10 03:54:57 richard
945 # Added database importing and exporting through CSV files.
946 # Uses the csv module from object-craft for exporting if it's available.
947 # Requires the csv module for importing.
948 #
949 # Revision 1.23 2001/10/09 23:58:10 richard
950 # Moved the data stringification up into the hyperdb.Class class' get, set
951 # and create methods. This means that the data is also stringified for the
952 # journal call, and removes duplication of code from the backends. The
953 # backend code now only sees strings.
954 #
955 # Revision 1.22 2001/10/09 07:25:59 richard
956 # Added the Password property type. See "pydoc roundup.password" for
957 # implementation details. Have updated some of the documentation too.
958 #
959 # Revision 1.21 2001/10/05 02:23:24 richard
960 # . roundup-admin create now prompts for property info if none is supplied
961 # on the command-line.
962 # . hyperdb Class getprops() method may now return only the mutable
963 # properties.
964 # . Login now uses cookies, which makes it a whole lot more flexible. We can
965 # now support anonymous user access (read-only, unless there's an
966 # "anonymous" user, in which case write access is permitted). Login
967 # handling has been moved into cgi_client.Client.main()
968 # . The "extended" schema is now the default in roundup init.
969 # . The schemas have had their page headings modified to cope with the new
970 # login handling. Existing installations should copy the interfaces.py
971 # file from the roundup lib directory to their instance home.
972 # . Incorrectly had a Bizar Software copyright on the cgitb.py module from
973 # Ping - has been removed.
974 # . Fixed a whole bunch of places in the CGI interface where we should have
975 # been returning Not Found instead of throwing an exception.
976 # . Fixed a deviation from the spec: trying to modify the 'id' property of
977 # an item now throws an exception.
978 #
979 # Revision 1.20 2001/10/04 02:12:42 richard
980 # Added nicer command-line item adding: passing no arguments will enter an
981 # interactive more which asks for each property in turn. While I was at it, I
982 # fixed an implementation problem WRT the spec - I wasn't raising a
983 # ValueError if the key property was missing from a create(). Also added a
984 # protected=boolean argument to getprops() so we can list only the mutable
985 # properties (defaults to yes, which lists the immutables).
986 #
987 # Revision 1.19 2001/08/29 04:47:18 richard
988 # Fixed CGI client change messages so they actually include the properties
989 # changed (again).
990 #
991 # Revision 1.18 2001/08/16 07:34:59 richard
992 # better CGI text searching - but hidden filter fields are disappearing...
993 #
994 # Revision 1.17 2001/08/16 06:59:58 richard
995 # all searches use re now - and they're all case insensitive
996 #
997 # Revision 1.16 2001/08/15 23:43:18 richard
998 # Fixed some isFooTypes that I missed.
999 # Refactored some code in the CGI code.
1000 #
1001 # Revision 1.15 2001/08/12 06:32:36 richard
1002 # using isinstance(blah, Foo) now instead of isFooType
1003 #
1004 # Revision 1.14 2001/08/07 00:24:42 richard
1005 # stupid typo
1006 #
1007 # Revision 1.13 2001/08/07 00:15:51 richard
1008 # Added the copyright/license notice to (nearly) all files at request of
1009 # Bizar Software.
1010 #
1011 # Revision 1.12 2001/08/02 06:38:17 richard
1012 # Roundupdb now appends "mailing list" information to its messages which
1013 # include the e-mail address and web interface address. Templates may
1014 # override this in their db classes to include specific information (support
1015 # instructions, etc).
1016 #
1017 # Revision 1.11 2001/08/01 04:24:21 richard
1018 # mailgw was assuming certain properties existed on the issues being created.
1019 #
1020 # Revision 1.10 2001/07/30 02:38:31 richard
1021 # get() now has a default arg - for migration only.
1022 #
1023 # Revision 1.9 2001/07/29 09:28:23 richard
1024 # Fixed sorting by clicking on column headings.
1025 #
1026 # Revision 1.8 2001/07/29 08:27:40 richard
1027 # Fixed handling of passed-in values in form elements (ie. during a
1028 # drill-down)
1029 #
1030 # Revision 1.7 2001/07/29 07:01:39 richard
1031 # Added vim command to all source so that we don't get no steenkin' tabs :)
1032 #
1033 # Revision 1.6 2001/07/29 05:36:14 richard
1034 # Cleanup of the link label generation.
1035 #
1036 # Revision 1.5 2001/07/29 04:05:37 richard
1037 # Added the fabricated property "id".
1038 #
1039 # Revision 1.4 2001/07/27 06:25:35 richard
1040 # Fixed some of the exceptions so they're the right type.
1041 # Removed the str()-ification of node ids so we don't mask oopsy errors any
1042 # more.
1043 #
1044 # Revision 1.3 2001/07/27 05:17:14 richard
1045 # just some comments
1046 #
1047 # Revision 1.2 2001/07/22 12:09:32 richard
1048 # Final commit of Grande Splite
1049 #
1050 # Revision 1.1 2001/07/22 11:58:35 richard
1051 # More Grande Splite
1052 #
1053 #
1054 # vim: set filetype=python ts=4 sw=4 et si