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