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