comparison roundup/hyperdb.py @ 3682:193f316dbbe9

More transitive-property support. - Implemented transitive properties in sort and group specs. Sort/group specs can now be lists of specs. - All regression tests except for one metakit backend test related to metakit having no representation of NULL pass - Fixed more PEP 8 whitespace peeves (and probably introduced some new ones :-) - Moved Proptree from support.py to hyperdb.py due to circular import - Moved some proptree-specific methods from Class to Proptree - Added a test for sorting by ids -> should be numeric sort (which now really works for all backends) - Added "required" attribute to all property classes in hyperdb (e.g., String, Link,...), see Feature Requests [SF#539081] -> factored common stuff to _Type. Note that I also converted to a new-style class when I was at it. Bad: The repr changes for new-style classes which made some SQL backends break (!) because the repr of Multilink is used in the schema storage. Fixed the repr to be independent of the class type. - Added get_required_props to Class. Todo: should also automagically make the key property required... - Add a sort_repr method to property classes. This defines the sort-order. Individual backends may use diffent routines if the outcome is the same. This one has a special case for id properties to make the sorting numeric. Using these methods isn't mandatory in backends as long as the sort-order is correct. - Multilink sorting takes orderprop into account. It used to sort by ids. You can restore the old behaviour by specifying id as the orderprop of the Multilink if you really need that. - If somebody specified a Link or Multilink as orderprop, we sort by labelprop of that class -- not transitively by orderprop. I've resited the tempation to implement recursive orderprop here: There could even be loops if several classes specify a Link or Multilink as the orderprop... - Fixed a bug in Metakit-Backend: When sorting by Links, the backend would do a natural join to the Link class. It would rename the "id" attribute before joining but *not* all the other attributes of the joined class. So in one test-case we had a name-clash with priority.name and status.name when sorting *and* grouping by these attributes. Depending on the order of joining this would produce a name-clash with broken sort-results (and broken display if the original class has an attribute that clashes). I'm now doing the sorting of Links in the generic filter method for the metakit backend. I've left the dead code in the metakit-backend since correctly implementing this in the backend will probably be more efficient. - updated doc/design.html with the new docstring of filter.
author Ralf Schlatterbeck <schlatterbeck@users.sourceforge.net>
date Mon, 21 Aug 2006 12:19:48 +0000
parents a15c15510e99
children bffa231ec3bc
comparison
equal deleted inserted replaced
3681:b9301ae1c34d 3682:193f316dbbe9
13 # BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 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" 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, 15 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 16 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
17 # 17 #
18 # $Id: hyperdb.py,v 1.123 2006-08-11 04:50:24 richard Exp $ 18 # $Id: hyperdb.py,v 1.124 2006-08-21 12:19:48 schlatterbeck Exp $
19 19
20 """Hyperdatabase implementation, especially field types. 20 """Hyperdatabase implementation, especially field types.
21 """ 21 """
22 __docformat__ = 'restructuredtext' 22 __docformat__ = 'restructuredtext'
23 23
24 # standard python modules 24 # standard python modules
25 import sys, os, time, re, shutil, weakref 25 import sys, os, time, re, shutil, weakref
26 from sets import Set
26 27
27 # roundup modules 28 # roundup modules
28 import date, password 29 import date, password
29 from support import ensureParentsExist, PrioList, Proptree 30 from support import ensureParentsExist, PrioList, sorted, reversed
30 31
31 # 32 #
32 # Types 33 # Types
33 # 34 #
34 class String: 35 class _Type(object):
35 """An object designating a String property.""" 36 """A roundup property type."""
36 def __init__(self, indexme='no'): 37 def __init__(self, required=False):
37 self.indexme = indexme == 'yes' 38 self.required = required
38 def __repr__(self): 39 def __repr__(self):
39 ' more useful for dumps ' 40 ' more useful for dumps '
40 return '<%s>'%self.__class__ 41 return '<%s.%s>'%(self.__class__.__module__, self.__class__.__name__)
42 def sort_repr (self, cls, val, name):
43 """Representation used for sorting. This should be a python
44 built-in type, otherwise sorting will take ages. Note that
45 individual backends may chose to use something different for
46 sorting as long as the outcome is the same.
47 """
48 return val
49
50 class String(_Type):
51 """An object designating a String property."""
52 def __init__(self, indexme='no', required=False):
53 super(String, self).__init__(required)
54 self.indexme = indexme == 'yes'
41 def from_raw(self, value, **kw): 55 def from_raw(self, value, **kw):
42 """fix the CRLF/CR -> LF stuff""" 56 """fix the CRLF/CR -> LF stuff"""
43 return fixNewlines(value) 57 return fixNewlines(value)
44 58 def sort_repr (self, cls, val, name):
45 class Password: 59 if name == 'id':
60 return int(val)
61 return val
62
63 class Password(_Type):
46 """An object designating a Password property.""" 64 """An object designating a Password property."""
47 def __repr__(self):
48 ' more useful for dumps '
49 return '<%s>'%self.__class__
50 def from_raw(self, value, **kw): 65 def from_raw(self, value, **kw):
51 if not value: 66 if not value:
52 return None 67 return None
53 m = password.Password.pwre.match(value) 68 m = password.Password.pwre.match(value)
54 if m: 69 if m:
64 try: 79 try:
65 value = password.Password(value) 80 value = password.Password(value)
66 except password.PasswordValueError, message: 81 except password.PasswordValueError, message:
67 raise HyperdbValueError, 'property %s: %s'%(propname, message) 82 raise HyperdbValueError, 'property %s: %s'%(propname, message)
68 return value 83 return value
69 84 def sort_repr (self, cls, val, name):
70 class Date: 85 if not val:
86 return val
87 return str(val)
88
89 class Date(_Type):
71 """An object designating a Date property.""" 90 """An object designating a Date property."""
72 def __init__(self, offset = None): 91 def __init__(self, offset=None, required=False):
92 super(Date, self).__init__(required)
73 self._offset = offset 93 self._offset = offset
74 def __repr__(self): 94 def offset(self, db):
75 ' more useful for dumps ' 95 if self._offset is not None:
76 return '<%s>'%self.__class__
77 def offset (self, db) :
78 if self._offset is not None :
79 return self._offset 96 return self._offset
80 return db.getUserTimezone () 97 return db.getUserTimezone()
81 def from_raw(self, value, db, **kw): 98 def from_raw(self, value, db, **kw):
82 try: 99 try:
83 value = date.Date(value, self.offset(db)) 100 value = date.Date(value, self.offset(db))
84 except ValueError, message: 101 except ValueError, message:
85 raise HyperdbValueError, 'property %s: %r is an invalid '\ 102 raise HyperdbValueError, 'property %s: %r is an invalid '\
86 'date (%s)'%(kw['propname'], value, message) 103 'date (%s)'%(kw['propname'], value, message)
87 return value 104 return value
88 def range_from_raw(self, value, db): 105 def range_from_raw(self, value, db):
89 """return Range value from given raw value with offset correction""" 106 """return Range value from given raw value with offset correction"""
90 return date.Range(value, date.Date, offset=self.offset(db)) 107 return date.Range(value, date.Date, offset=self.offset(db))
91 108 def sort_repr (self, cls, val, name):
92 class Interval: 109 if not val:
110 return val
111 return str(val)
112
113 class Interval(_Type):
93 """An object designating an Interval property.""" 114 """An object designating an Interval property."""
94 def __repr__(self):
95 ' more useful for dumps '
96 return '<%s>'%self.__class__
97 def from_raw(self, value, **kw): 115 def from_raw(self, value, **kw):
98 try: 116 try:
99 value = date.Interval(value) 117 value = date.Interval(value)
100 except ValueError, message: 118 except ValueError, message:
101 raise HyperdbValueError, 'property %s: %r is an invalid '\ 119 raise HyperdbValueError, 'property %s: %r is an invalid '\
102 'date interval (%s)'%(kw['propname'], value, message) 120 'date interval (%s)'%(kw['propname'], value, message)
103 return value 121 return value
104 122 def sort_repr (self, cls, val, name):
105 class Link: 123 if not val:
106 """An object designating a Link property that links to a 124 return val
107 node in a specified class.""" 125 return val.as_seconds()
108 def __init__(self, classname, do_journal='yes'): 126
109 ''' Default is to not journal link and unlink events 127 class _Pointer(_Type):
110 ''' 128 """An object designating a Pointer property that links or multilinks
129 to a node in a specified class."""
130 def __init__(self, classname, do_journal='yes', required=False):
131 ''' Default is to journal link and unlink events
132 '''
133 super(_Pointer, self).__init__(required)
111 self.classname = classname 134 self.classname = classname
112 self.do_journal = do_journal == 'yes' 135 self.do_journal = do_journal == 'yes'
113 def __repr__(self): 136 def __repr__(self):
114 ' more useful for dumps ' 137 """more useful for dumps. But beware: This is also used in schema
115 return '<%s to "%s">'%(self.__class__, self.classname) 138 storage in SQL backends!
139 """
140 return '<%s.%s to "%s">'%(self.__class__.__module__,
141 self.__class__.__name__, self.classname)
142
143 class Link(_Pointer):
144 """An object designating a Link property that links to a
145 node in a specified class."""
116 def from_raw(self, value, db, propname, **kw): 146 def from_raw(self, value, db, propname, **kw):
117 if value == '-1' or not value: 147 if value == '-1' or not value:
118 value = None 148 value = None
119 else: 149 else:
120 value = convertLinkValue(db, propname, self, value) 150 value = convertLinkValue(db, propname, self, value)
121 return value 151 return value
122 152 def sort_repr (self, cls, val, name):
123 class Multilink: 153 if not val:
154 return val
155 op = cls.labelprop()
156 if op == 'id':
157 return int(cls.get(val, op))
158 return cls.get(val, op)
159
160 class Multilink(_Pointer):
124 """An object designating a Multilink property that links 161 """An object designating a Multilink property that links
125 to nodes in a specified class. 162 to nodes in a specified class.
126 163
127 "classname" indicates the class to link to 164 "classname" indicates the class to link to
128 165
129 "do_journal" indicates whether the linked-to nodes should have 166 "do_journal" indicates whether the linked-to nodes should have
130 'link' and 'unlink' events placed in their journal 167 'link' and 'unlink' events placed in their journal
131 """ 168 """
132 def __init__(self, classname, do_journal='yes'):
133 ''' Default is to not journal link and unlink events
134 '''
135 self.classname = classname
136 self.do_journal = do_journal == 'yes'
137 def __repr__(self):
138 ' more useful for dumps '
139 return '<%s to "%s">'%(self.__class__, self.classname)
140 def from_raw(self, value, db, klass, propname, itemid, **kw): 169 def from_raw(self, value, db, klass, propname, itemid, **kw):
141 if not value: 170 if not value:
142 return [] 171 return []
143 172
144 # get the current item value if it's not a new item 173 # get the current item value if it's not a new item
201 value = [int(x) for x in value] 230 value = [int(x) for x in value]
202 value.sort() 231 value.sort()
203 value = [str(x) for x in value] 232 value = [str(x) for x in value]
204 return value 233 return value
205 234
206 class Boolean: 235 def sort_repr (self, cls, val, name):
236 if not val:
237 return val
238 op = cls.labelprop()
239 if op == 'id':
240 return [int(cls.get(v, op)) for v in val]
241 return [cls.get(v, op) for v in val]
242
243 class Boolean(_Type):
207 """An object designating a boolean property""" 244 """An object designating a boolean property"""
208 def __repr__(self):
209 'more useful for dumps'
210 return '<%s>' % self.__class__
211 def from_raw(self, value, **kw): 245 def from_raw(self, value, **kw):
212 value = value.strip() 246 value = value.strip()
213 # checked is a common HTML checkbox value 247 # checked is a common HTML checkbox value
214 value = value.lower() in ('checked', 'yes', 'true', 'on', '1') 248 value = value.lower() in ('checked', 'yes', 'true', 'on', '1')
215 return value 249 return value
216 250
217 class Number: 251 class Number(_Type):
218 """An object designating a numeric property""" 252 """An object designating a numeric property"""
219 def __repr__(self):
220 'more useful for dumps'
221 return '<%s>' % self.__class__
222 def from_raw(self, value, **kw): 253 def from_raw(self, value, **kw):
223 value = value.strip() 254 value = value.strip()
224 try: 255 try:
225 value = float(value) 256 value = float(value)
226 except ValueError: 257 except ValueError:
238 m = dre.match(designator) 269 m = dre.match(designator)
239 if m is None: 270 if m is None:
240 raise DesignatorError, '"%s" not a node designator'%designator 271 raise DesignatorError, '"%s" not a node designator'%designator
241 return m.group(1), m.group(2) 272 return m.group(1), m.group(2)
242 273
274 class Proptree(object):
275 ''' Simple tree data structure for optimizing searching of
276 properties. Each node in the tree represents a roundup Class
277 Property that has to be navigated for finding the given search
278 or sort properties. The sort_type attribute is used for
279 distinguishing nodes in the tree used for sorting or searching: If
280 it is 0 for a node, that node is not used for sorting. If it is 1,
281 it is used for both, sorting and searching. If it is 2 it is used
282 for sorting only.
283
284 The Proptree is also used for transitively searching attributes for
285 backends that do not support transitive search (e.g. anydbm). The
286 _val attribute with set_val is used for this.
287 '''
288
289 def __init__(self, db, cls, name, props, parent = None):
290 self.db = db
291 self.name = name
292 self.props = props
293 self.parent = parent
294 self._val = None
295 self.has_values = False
296 self.cls = cls
297 self.classname = None
298 self.uniqname = None
299 self.children = []
300 self.sortattr = []
301 self.propdict = {}
302 self.sort_type = 0
303 self.sort_direction = None
304 self.sort_ids = None
305 self.sort_ids_needed = False
306 self.sort_result = None
307 self.attr_sort_done = False
308 self.tree_sort_done = False
309 self.propclass = None
310 if parent:
311 self.root = parent.root
312 self.depth = parent.depth + 1
313 else:
314 self.root = self
315 self.seqno = 1
316 self.depth = 0
317 self.sort_type = 1
318 self.id = self.root.seqno
319 self.root.seqno += 1
320 if self.cls:
321 self.classname = self.cls.classname
322 self.uniqname = '%s%s' % (self.cls.classname, self.id)
323 if not self.parent:
324 self.uniqname = self.cls.classname
325
326 def append(self, name, sort_type = 0):
327 """Append a property to self.children. Will create a new
328 propclass for the child.
329 """
330 if name in self.propdict:
331 pt = self.propdict[name]
332 if sort_type and not pt.sort_type:
333 pt.sort_type = 1
334 return pt
335 propclass = self.props[name]
336 cls = None
337 props = None
338 if isinstance(propclass, (Link, Multilink)):
339 cls = self.db.getclass(propclass.classname)
340 props = cls.getprops()
341 child = self.__class__(self.db, cls, name, props, parent = self)
342 child.sort_type = sort_type
343 child.propclass = propclass
344 self.children.append(child)
345 self.propdict[name] = child
346 return child
347
348 def compute_sort_done(self, mlseen=False):
349 """ Recursively check if attribute is needed for sorting
350 (self.sort_type > 0) or all children have tree_sort_done set and
351 sort_ids_needed unset: set self.tree_sort_done if one of the conditions
352 holds. Also remove sort_ids_needed recursively once having seen a
353 Multilink.
354 """
355 if isinstance (self.propclass, Multilink):
356 mlseen = True
357 if mlseen:
358 self.sort_ids_needed = False
359 self.tree_sort_done = True
360 for p in self.children:
361 p.compute_sort_done(mlseen)
362 if not p.tree_sort_done:
363 self.tree_sort_done = False
364 if not self.sort_type:
365 self.tree_sort_done = True
366 if mlseen:
367 self.tree_sort_done = False
368
369 def ancestors(self):
370 p = self
371 while p.parent:
372 yield p
373 p = p.parent
374
375 def search(self, search_matches=None, sort=True):
376 """ Recursively search for the given properties in a proptree.
377 Once all properties are non-transitive, the search generates a
378 simple _filter call which does the real work
379 """
380 filterspec = {}
381 for p in self.children:
382 if p.sort_type < 2:
383 if p.children:
384 p.search(sort = False)
385 filterspec[p.name] = p.val
386 self.val = self.cls._filter(search_matches, filterspec, sort and self)
387 return self.val
388
389 def sort (self, ids=None):
390 """ Sort ids by the order information stored in self. With
391 optimisations: Some order attributes may be precomputed (by the
392 backend) and some properties may already be sorted.
393 """
394 if ids is None:
395 ids = self.val
396 if self.sortattr and [s for s in self.sortattr if not s.attr_sort_done]:
397 return self._searchsort(ids, True, True)
398 return ids
399
400 def sortable_children(self, intermediate=False):
401 """ All children needed for sorting. If intermediate is True,
402 intermediate nodes (not being a sort attribute) are returned,
403 too.
404 """
405 return [p for p in self.children
406 if p.sort_type > 0 and (intermediate or p.sort_direction)]
407
408 def __iter__(self):
409 """ Yield nodes in depth-first order -- visited nodes first """
410 for p in self.children:
411 yield p
412 for c in p:
413 yield c
414
415 def _get (self, ids):
416 """Lookup given ids -- possibly a list of list. We recurse until
417 we have a list of ids.
418 """
419 if not ids:
420 return ids
421 if isinstance (ids[0], list):
422 cids = [self._get(i) for i in ids]
423 else:
424 cids = [i and self.parent.cls.get(i, self.name) for i in ids]
425 if self.sortattr:
426 cids = [self._searchsort(i, False, True) for i in cids]
427 return cids
428
429 def _searchsort(self, ids=None, update=True, dosort=True):
430 """ Recursively compute the sort attributes. Note that ids
431 may be a deeply nested list of lists of ids if several
432 multilinks are encountered on the way from the root to an
433 individual attribute. We make sure that everything is properly
434 sorted on the way up. Note that the individual backend may
435 already have precomputed self.result or self.sort_ids. In this
436 case we do nothing for existing sa.result and recurse further if
437 self.sort_ids is available.
438
439 Yech, Multilinks: This gets especially complicated if somebody
440 sorts by different attributes of the same multilink (or
441 transitively across several multilinks). My use-case is sorting
442 by issue.messages.author and (reverse) by issue.messages.date.
443 In this case we sort the messages by author and date and use
444 this sorted list twice for sorting issues. This means that
445 issues are sorted by author and then by the time of the messages
446 *of this author*. Probably what the user intends in that case,
447 so we do *not* use two sorted lists of messages, one sorted by
448 author and one sorted by date for sorting issues.
449 """
450 for pt in self.sortable_children(intermediate = True):
451 # ids can be an empty list
452 if pt.tree_sort_done or not ids:
453 continue
454 if pt.sort_ids: # cached or computed by backend
455 cids = pt.sort_ids
456 else:
457 cids = pt._get(ids)
458 if pt.sort_direction and not pt.sort_result:
459 sortrep = pt.propclass.sort_repr
460 pt.sort_result = pt._sort_repr(sortrep, cids)
461 pt.sort_ids = cids
462 if pt.children:
463 pt._searchsort(cids, update, False)
464 if self.sortattr and dosort:
465 ids = self._sort(ids)
466 if not update:
467 for pt in self.sortable_children(intermediate = True):
468 pt.sort_ids = None
469 for pt in self.sortattr:
470 pt.sort_result = None
471 return ids
472
473 def _set_val(self, val):
474 """Check if self._val is already defined. If yes, we compute the
475 intersection of the old and the new value(s)
476 """
477 if self.has_values:
478 v = self._val
479 if not isinstance(self._val, type([])):
480 v = [self._val]
481 vals = Set(v)
482 vals.intersection_update(val)
483 self._val = [v for v in vals]
484 else:
485 self._val = val
486 self.has_values = True
487
488 val = property(lambda self: self._val, _set_val)
489
490 def _sort(self, val):
491 """Finally sort by the given sortattr.sort_result. Note that we
492 do not sort by attrs having attr_sort_done set. The caller is
493 responsible for setting attr_sort_done only for trailing
494 attributes (otherwise the sort order is wrong). Since pythons
495 sort is stable, we can sort already sorted lists without
496 destroying the sort-order for items that compare equal with the
497 current sort.
498
499 Sorting-Strategy: We sort repeatedly by different sort-keys from
500 right to left. Since pythons sort is stable, we can safely do
501 that. An optimisation is a "run-length encoding" of the
502 sort-directions: If several sort attributes sort in the same
503 direction we can combine them into a single sort. Note that
504 repeated sorting is probably more efficient than using
505 compare-methods in python due to the overhead added by compare
506 methods.
507 """
508 if not val:
509 return val
510 sortattr = []
511 directions = []
512 dir_idx = []
513 idx = 0
514 curdir = None
515 for sa in self.sortattr:
516 if sa.attr_sort_done:
517 break
518 if sortattr:
519 assert len(sortattr[0]) == len(sa.sort_result)
520 sortattr.append (sa.sort_result)
521 if curdir != sa.sort_direction:
522 dir_idx.append (idx)
523 directions.append (sa.sort_direction)
524 curdir = sa.sort_direction
525 idx += 1
526 sortattr.append (val)
527 #print >> sys.stderr, "\nsortattr", sortattr
528 sortattr = zip (*sortattr)
529 for dir, i in reversed(zip(directions, dir_idx)):
530 rev = dir == '-'
531 sortattr = sorted (sortattr, key = lambda x:x[i:idx], reverse = rev)
532 idx = i
533 return [x[-1] for x in sortattr]
534
535 def _sort_repr(self, sortrep, ids):
536 """Call sortrep for given ids -- possibly a list of list. We
537 recurse until we have a list of ids.
538 """
539 if not ids:
540 return ids
541 if isinstance (ids[0], list):
542 res = [self._sort_repr(sortrep, i) for i in ids]
543 else:
544 res = [sortrep(self.cls, i, self.name) for i in ids]
545 return res
546
547 def __repr__(self):
548 r = ["proptree:" + self.name]
549 for n in self:
550 r.append("proptree:" + " " * n.depth + n.name)
551 return '\n'.join(r)
552 __str__ = __repr__
553
243 # 554 #
244 # the base Database class 555 # the base Database class
245 # 556 #
246 class DatabaseError(ValueError): 557 class DatabaseError(ValueError):
247 '''Error to be raised when there is some problem in the database code 558 '''Error to be raised when there is some problem in the database code
464 self.do_journal = 1 775 self.do_journal = 1
465 776
466 # do the db-related init stuff 777 # do the db-related init stuff
467 db.addclass(self) 778 db.addclass(self)
468 779
469 actions = "create set retire restore".split () 780 actions = "create set retire restore".split()
470 self.auditors = dict ([(a, PrioList ()) for a in actions]) 781 self.auditors = dict([(a, PrioList()) for a in actions])
471 self.reactors = dict ([(a, PrioList ()) for a in actions]) 782 self.reactors = dict([(a, PrioList()) for a in actions])
472 783
473 def __repr__(self): 784 def __repr__(self):
474 '''Slightly more useful representation 785 '''Slightly more useful representation
475 ''' 786 '''
476 return '<hyperdb.Class "%s">'%self.classname 787 return '<hyperdb.Class "%s">'%self.classname
616 None, or a TypeError is raised. The values of the key property on 927 None, or a TypeError is raised. The values of the key property on
617 all existing nodes must be unique or a ValueError is raised. 928 all existing nodes must be unique or a ValueError is raised.
618 """ 929 """
619 raise NotImplementedError 930 raise NotImplementedError
620 931
621 def setlabelprop (self, labelprop): 932 def setlabelprop(self, labelprop):
622 """Set the label property. Used for override of labelprop 933 """Set the label property. Used for override of labelprop
623 resolution order. 934 resolution order.
624 """ 935 """
625 if labelprop not in self.getprops () : 936 if labelprop not in self.getprops():
626 raise ValueError, "Not a property name: %s" % labelprop 937 raise ValueError, "Not a property name: %s" % labelprop
627 self._labelprop = labelprop 938 self._labelprop = labelprop
628 939
629 def setorderprop (self, orderprop): 940 def setorderprop(self, orderprop):
630 """Set the order property. Used for override of orderprop 941 """Set the order property. Used for override of orderprop
631 resolution order 942 resolution order
632 """ 943 """
633 if orderprop not in self.getprops () : 944 if orderprop not in self.getprops():
634 raise ValueError, "Not a property name: %s" % orderprop 945 raise ValueError, "Not a property name: %s" % orderprop
635 self._orderprop = orderprop 946 self._orderprop = orderprop
636 947
637 def getkey(self): 948 def getkey(self):
638 """Return the name of the key property for this class or None.""" 949 """Return the name of the key property for this class or None."""
648 1. key property 959 1. key property
649 2. "name" property 960 2. "name" property
650 3. "title" property 961 3. "title" property
651 4. first property from the sorted property name list 962 4. first property from the sorted property name list
652 """ 963 """
653 if hasattr (self, '_labelprop') : 964 if hasattr(self, '_labelprop'):
654 return self._labelprop 965 return self._labelprop
655 k = self.getkey() 966 k = self.getkey()
656 if k: 967 if k:
657 return k 968 return k
658 props = self.getprops() 969 props = self.getprops()
664 return 'id' 975 return 'id'
665 props = props.keys() 976 props = props.keys()
666 props.sort() 977 props.sort()
667 return props[0] 978 return props[0]
668 979
669 def orderprop (self): 980 def orderprop(self):
670 """Return the property name to use for sorting for the given node. 981 """Return the property name to use for sorting for the given node.
671 982
672 This method computes the property for sorting. 983 This method computes the property for sorting.
673 It tries the following in order: 984 It tries the following in order:
674 985
675 0. self._orderprop if set 986 0. self._orderprop if set
676 1. "order" property 987 1. "order" property
677 2. self.labelprop () 988 2. self.labelprop()
678 """ 989 """
679 990
680 if hasattr (self, '_orderprop') : 991 if hasattr(self, '_orderprop'):
681 return self._orderprop 992 return self._orderprop
682 props = self.getprops () 993 props = self.getprops()
683 if props.has_key ('order'): 994 if props.has_key('order'):
684 return 'order' 995 return 'order'
685 return self.labelprop () 996 return self.labelprop()
686 997
687 def lookup(self, keyvalue): 998 def lookup(self, keyvalue):
688 """Locate a particular node by its key property and return its id. 999 """Locate a particular node by its key property and return its id.
689 1000
690 If this class has no key property, a TypeError is raised. If the 1001 If this class has no key property, a TypeError is raised. If the
716 """For some backends this implements the non-transitive 1027 """For some backends this implements the non-transitive
717 search, for more information see the filter method. 1028 search, for more information see the filter method.
718 """ 1029 """
719 raise NotImplementedError 1030 raise NotImplementedError
720 1031
721 def _proptree (self, filterspec): 1032 def _proptree(self, filterspec, sortattr=[]):
722 """Build a tree of all transitive properties in the given 1033 """Build a tree of all transitive properties in the given
723 filterspec. 1034 filterspec.
724 """ 1035 """
725 proptree = Proptree (self.db, self, '', self.getprops ()) 1036 proptree = Proptree(self.db, self, '', self.getprops())
726 for key, v in filterspec.iteritems (): 1037 for key, v in filterspec.iteritems():
727 keys = key.split ('.') 1038 keys = key.split('.')
728 p = proptree 1039 p = proptree
729 for k in keys: 1040 for k in keys:
730 p = p.append (k) 1041 p = p.append(k)
731 p.val = v 1042 p.val = v
1043 multilinks = {}
1044 for s in sortattr:
1045 keys = s[1].split('.')
1046 p = proptree
1047 for k in keys:
1048 p = p.append(k, sort_type = 2)
1049 if isinstance (p.propclass, Multilink):
1050 multilinks[p] = True
1051 if p.cls:
1052 p = p.append(p.cls.orderprop(), sort_type = 2)
1053 if p.sort_direction: # if an orderprop is also specified explicitly
1054 continue
1055 p.sort_direction = s[0]
1056 proptree.sortattr.append (p)
1057 for p in multilinks.iterkeys():
1058 sattr = {}
1059 for c in p:
1060 if c.sort_direction:
1061 sattr [c] = True
1062 for sa in proptree.sortattr:
1063 if sa in sattr:
1064 p.sortattr.append (sa)
732 return proptree 1065 return proptree
733 1066
734 def _propsearch (self, search_matches, proptree, sort, group): 1067 def get_transitive_prop(self, propname_path, default = None):
735 """ Recursively search for the given properties in proptree.
736 Once all properties are non-transitive, the search generates a
737 simple _filter call which does the real work
738 """
739 for p in proptree.children:
740 if not p.children:
741 continue
742 p.val = p.cls._propsearch (None, p, (None, None), (None, None))
743 filterspec = dict ([(p.name, p.val) for p in proptree.children])
744 return self._filter (search_matches, filterspec, sort, group)
745
746 def get_transitive_prop (self, propname_path, default = None) :
747 """Expand a transitive property (individual property names 1068 """Expand a transitive property (individual property names
748 separated by '.' into a new property at the end of the path. If 1069 separated by '.' into a new property at the end of the path. If
749 one of the names does not refer to a valid property, we return 1070 one of the names does not refer to a valid property, we return
750 None. 1071 None.
751 Example propname_path (for class issue): "messages.author" 1072 Example propname_path (for class issue): "messages.author"
752 """ 1073 """
753 props = self.db.getclass(self.classname).getprops() 1074 props = self.db.getclass(self.classname).getprops()
754 for k in propname_path.split('.'): 1075 for k in propname_path.split('.'):
755 try : 1076 try:
756 prop = props[k] 1077 prop = props[k]
757 except KeyError, TypeError: 1078 except KeyError, TypeError:
758 return default 1079 return default
759 cl = getattr (prop, 'classname', None) 1080 cl = getattr(prop, 'classname', None)
760 props = None 1081 props = None
761 if cl: 1082 if cl:
762 props = self.db.getclass (cl).getprops() 1083 props = self.db.getclass(cl).getprops()
763 return prop 1084 return prop
764 1085
765 def filter(self, search_matches, filterspec, sort=(None,None), 1086 def _sortattr(self, sort=[], group=[]):
766 group=(None,None)): 1087 """Build a single list of sort attributes in the correct order
1088 with sanity checks (no duplicate properties) included. Always
1089 sort last by id -- if id is not already in sortattr.
1090 """
1091 seen = {}
1092 sortattr = []
1093 for srt in group, sort:
1094 if not isinstance(srt, list):
1095 srt = [srt]
1096 for s in srt:
1097 if s[1] and s[1] not in seen:
1098 sortattr.append((s[0] or '+', s[1]))
1099 seen[s[1]] = True
1100 if 'id' not in seen :
1101 sortattr.append(('+', 'id'))
1102 return sortattr
1103
1104 def filter(self, search_matches, filterspec, sort=[], group=[]):
767 """Return a list of the ids of the active nodes in this class that 1105 """Return a list of the ids of the active nodes in this class that
768 match the 'filter' spec, sorted by the group spec and then the 1106 match the 'filter' spec, sorted by the group spec and then the
769 sort spec. 1107 sort spec.
770 1108
771 "filterspec" is {propname: value(s)} 1109 "filterspec" is {propname: value(s)}
772 1110
773 Note that now the propname in filterspec may be transitive, 1111 "sort" and "group" are [(dir, prop), ...] where dir is '+', '-'
774 i.e., it may contain properties of the form link.link.link.name, 1112 or None and prop is a prop name or None. Note that for
775 e.g. you can search for all issues where a message was added by 1113 backward-compatibility reasons a single (dir, prop) tuple is
776 a certain user in the last week with a filterspec of 1114 also allowed.
777 {'messages.author' : '42', 'messages.creation' : '.-1w;'}
778
779 "sort" and "group" are (dir, prop) where dir is '+', '-' or None
780 and prop is a prop name or None
781 1115
782 "search_matches" is {nodeid: marker} 1116 "search_matches" is {nodeid: marker}
783 1117
784 The filter must match all properties specificed. If the property 1118 The filter must match all properties specificed. If the property
785 value to match is a list: 1119 value to match is a list:
786 1120
787 1. String properties must match all elements in the list, and 1121 1. String properties must match all elements in the list, and
788 2. Other properties must match any of the elements in the list. 1122 2. Other properties must match any of the elements in the list.
1123
1124 Note that now the propname in filterspec and prop in a
1125 sort/group spec may be transitive, i.e., it may contain
1126 properties of the form link.link.link.name, e.g. you can search
1127 for all issues where a message was added by a certain user in
1128 the last week with a filterspec of
1129 {'messages.author' : '42', 'messages.creation' : '.-1w;'}
789 1130
790 Implementation note: 1131 Implementation note:
791 This implements a non-optimized version of Transitive search 1132 This implements a non-optimized version of Transitive search
792 using _filter implemented in a backend class. A more efficient 1133 using _filter implemented in a backend class. A more efficient
793 version can be implemented in the individual backends -- e.g., 1134 version can be implemented in the individual backends -- e.g.,
794 an SQL backen will want to create a single SQL statement and 1135 an SQL backen will want to create a single SQL statement and
795 override the filter method instead of implementing _filter. 1136 override the filter method instead of implementing _filter.
796 """ 1137 """
797 proptree = self._proptree (filterspec) 1138 sortattr = self._sortattr(sort = sort, group = group)
798 return self._propsearch (search_matches, proptree, sort, group) 1139 proptree = self._proptree(filterspec, sortattr)
1140 proptree.search(search_matches)
1141 return proptree.sort()
799 1142
800 def count(self): 1143 def count(self):
801 """Get the number of nodes in this class. 1144 """Get the number of nodes in this class.
802 1145
803 If the returned integer is 'numnodes', the ids of all the nodes 1146 If the returned integer is 'numnodes', the ids of all the nodes
811 """Return a dictionary mapping property names to property objects. 1154 """Return a dictionary mapping property names to property objects.
812 If the "protected" flag is true, we include protected properties - 1155 If the "protected" flag is true, we include protected properties -
813 those which may not be modified. 1156 those which may not be modified.
814 """ 1157 """
815 raise NotImplementedError 1158 raise NotImplementedError
1159
1160 def get_required_props(self, propnames = []):
1161 """Return a dict of property names mapping to property objects.
1162 All properties that have the "required" flag set will be
1163 returned in addition to all properties in the propnames
1164 parameter.
1165 """
1166 props = self.getprops(protected = False)
1167 pdict = dict([(p, props[p]) for p in propnames])
1168 pdict.update([(k, v) for k, v in props.iteritems() if v.required])
1169 return pdict
816 1170
817 def addprop(self, **properties): 1171 def addprop(self, **properties):
818 """Add properties to this class. 1172 """Add properties to this class.
819 1173
820 The keyword arguments in 'properties' must map names to property 1174 The keyword arguments in 'properties' must map names to property

Roundup Issue Tracker: http://roundup-tracker.org/