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