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