comparison roundup/hyperdb.py @ 6006:cf800f1febe6

Flake8 fixes; use isinstance rather than type equality. Also remove assignment for unused variable. Replace x with _x in for loop variables that are not used but required to unpack tuple.
author John Rouillard <rouilj@ieee.org>
date Sat, 28 Dec 2019 14:41:55 -0500
parents acc4a128ab9b
children 8497bf3f23a1
comparison
equal deleted inserted replaced
6005:292c9dfd06bd 6006:cf800f1febe6
33 from roundup.anypy.cmp_ import NoneAndDictComparable 33 from roundup.anypy.cmp_ import NoneAndDictComparable
34 from roundup.anypy.strings import eval_import 34 from roundup.anypy.strings import eval_import
35 35
36 logger = logging.getLogger('roundup.hyperdb') 36 logger = logging.getLogger('roundup.hyperdb')
37 37
38
38 # 39 #
39 # Types 40 # Types
40 # 41 #
41 class _Type(object): 42 class _Type(object):
42 """A roundup property type.""" 43 """A roundup property type."""
43 def __init__(self, required=False, default_value = None, quiet=False): 44 def __init__(self, required=False, default_value=None, quiet=False):
44 self.required = required 45 self.required = required
45 self.__default_value = default_value 46 self.__default_value = default_value
46 self.quiet = quiet 47 self.quiet = quiet
48
47 def __repr__(self): 49 def __repr__(self):
48 ' more useful for dumps ' 50 ' more useful for dumps '
49 return '<%s.%s>'%(self.__class__.__module__, self.__class__.__name__) 51 return '<%s.%s>' % (self.__class__.__module__, self.__class__.__name__)
52
50 def get_default_value(self): 53 def get_default_value(self):
51 """The default value when creating a new instance of this property.""" 54 """The default value when creating a new instance of this property."""
52 return self.__default_value 55 return self.__default_value
53 def sort_repr (self, cls, val, name): 56
57 def sort_repr(self, cls, val, name):
54 """Representation used for sorting. This should be a python 58 """Representation used for sorting. This should be a python
55 built-in type, otherwise sorting will take ages. Note that 59 built-in type, otherwise sorting will take ages. Note that
56 individual backends may chose to use something different for 60 individual backends may chose to use something different for
57 sorting as long as the outcome is the same. 61 sorting as long as the outcome is the same.
58 """ 62 """
59 return val 63 return val
60 64
65
61 class String(_Type): 66 class String(_Type):
62 """An object designating a String property.""" 67 """An object designating a String property."""
63 def __init__(self, indexme='no', required=False, default_value = "", quiet=False): 68 def __init__(self, indexme='no', required=False, default_value="",
69 quiet=False):
64 super(String, self).__init__(required, default_value, quiet) 70 super(String, self).__init__(required, default_value, quiet)
65 self.indexme = indexme == 'yes' 71 self.indexme = indexme == 'yes'
72
66 def from_raw(self, value, propname='', **kw): 73 def from_raw(self, value, propname='', **kw):
67 """fix the CRLF/CR -> LF stuff""" 74 """fix the CRLF/CR -> LF stuff"""
68 if propname == 'content': 75 if propname == 'content':
69 # Why oh why wasn't the FileClass content property a File 76 # Why oh why wasn't the FileClass content property a File
70 # type from the beginning? 77 # type from the beginning?
71 return value 78 return value
72 return fixNewlines(value) 79 return fixNewlines(value)
73 def sort_repr (self, cls, val, name): 80
81 def sort_repr(self, cls, val, name):
74 if not val: 82 if not val:
75 return val 83 return val
76 if name == 'id': 84 if name == 'id':
77 return int(val) 85 return int(val)
78 return val.lower() 86 return val.lower()
79 87
88
80 class Password(_Type): 89 class Password(_Type):
81 """An object designating a Password property.""" 90 """An object designating a Password property."""
82 def __init__(self, scheme=None, required=False, default_value = None, quiet=False): 91 def __init__(self, scheme=None, required=False, default_value=None,
92 quiet=False):
83 super(Password, self).__init__(required, default_value, quiet) 93 super(Password, self).__init__(required, default_value, quiet)
84 self.scheme = scheme 94 self.scheme = scheme
85 95
86 def from_raw(self, value, **kw): 96 def from_raw(self, value, **kw):
87 if not value: 97 if not value:
88 return None 98 return None
89 try: 99 try:
90 return password.Password(encrypted=value, scheme=self.scheme, strict=True) 100 return password.Password(encrypted=value, scheme=self.scheme,
101 strict=True)
91 except password.PasswordValueError as message: 102 except password.PasswordValueError as message:
92 raise HyperdbValueError(_('property %s: %s')%(kw['propname'], message)) 103 raise HyperdbValueError(_('property %s: %s') %
93 104 (kw['propname'], message))
94 def sort_repr (self, cls, val, name): 105
106 def sort_repr(self, cls, val, name):
95 if not val: 107 if not val:
96 return val 108 return val
97 return str(val) 109 return str(val)
98 110
111
99 class Date(_Type): 112 class Date(_Type):
100 """An object designating a Date property.""" 113 """An object designating a Date property."""
101 def __init__(self, offset=None, required=False, default_value = None, quiet=False): 114 def __init__(self, offset=None, required=False, default_value=None,
102 super(Date, self).__init__(required = required, 115 quiet=False):
103 default_value = default_value, 116 super(Date, self).__init__(required=required,
117 default_value=default_value,
104 quiet=quiet) 118 quiet=quiet)
105 self._offset = offset 119 self._offset = offset
120
106 def offset(self, db): 121 def offset(self, db):
107 if self._offset is not None: 122 if self._offset is not None:
108 return self._offset 123 return self._offset
109 return db.getUserTimezone() 124 return db.getUserTimezone()
125
110 def from_raw(self, value, db, **kw): 126 def from_raw(self, value, db, **kw):
111 try: 127 try:
112 value = date.Date(value, self.offset(db)) 128 value = date.Date(value, self.offset(db))
113 except ValueError as message: 129 except ValueError as message:
114 raise HyperdbValueError(_('property %s: %r is an invalid '\ 130 raise HyperdbValueError(_('property %s: %r is an invalid '
115 'date (%s)')%(kw['propname'], value, message)) 131 'date (%s)') % (kw['propname'],
132 value, message))
116 return value 133 return value
134
117 def range_from_raw(self, value, db): 135 def range_from_raw(self, value, db):
118 """return Range value from given raw value with offset correction""" 136 """return Range value from given raw value with offset correction"""
119 return date.Range(value, date.Date, offset=self.offset(db)) 137 return date.Range(value, date.Date, offset=self.offset(db))
120 def sort_repr (self, cls, val, name): 138
139 def sort_repr(self, cls, val, name):
121 if not val: 140 if not val:
122 return val 141 return val
123 return str(val) 142 return str(val)
143
124 144
125 class Interval(_Type): 145 class Interval(_Type):
126 """An object designating an Interval property.""" 146 """An object designating an Interval property."""
127 def from_raw(self, value, **kw): 147 def from_raw(self, value, **kw):
128 try: 148 try:
129 value = date.Interval(value) 149 value = date.Interval(value)
130 except ValueError as message: 150 except ValueError as message:
131 raise HyperdbValueError(_('property %s: %r is an invalid '\ 151 raise HyperdbValueError(_('property %s: %r is an invalid '
132 'date interval (%s)')%(kw['propname'], value, message)) 152 'date interval (%s)') %
153 (kw['propname'], value, message))
133 return value 154 return value
134 def sort_repr (self, cls, val, name): 155
156 def sort_repr(self, cls, val, name):
135 if not val: 157 if not val:
136 return val 158 return val
137 return val.as_seconds() 159 return val.as_seconds()
160
138 161
139 class _Pointer(_Type): 162 class _Pointer(_Type):
140 """An object designating a Pointer property that links or multilinks 163 """An object designating a Pointer property that links or multilinks
141 to a node in a specified class.""" 164 to a node in a specified class."""
142 def __init__(self, classname, do_journal='yes', try_id_parsing='yes', 165 def __init__(self, classname, do_journal='yes', try_id_parsing='yes',
143 required=False, default_value=None, 166 required=False, default_value=None,
144 msg_header_property = None, quiet=False): 167 msg_header_property=None, quiet=False):
145 """ Default is to journal link and unlink events. 168 """ Default is to journal link and unlink events.
146 When try_id_parsing is false, we don't allow IDs in input 169 When try_id_parsing is false, we don't allow IDs in input
147 fields (the key of the Link or Multilink property must be 170 fields (the key of the Link or Multilink property must be
148 given instead). This is useful when the name of a property 171 given instead). This is useful when the name of a property
149 can be numeric. It will only work if the linked item has a 172 can be numeric. It will only work if the linked item has a
161 'X-Roundup-issue-assigned_to: joe_user'. 184 'X-Roundup-issue-assigned_to: joe_user'.
162 """ 185 """
163 super(_Pointer, self).__init__(required, default_value, quiet) 186 super(_Pointer, self).__init__(required, default_value, quiet)
164 self.classname = classname 187 self.classname = classname
165 self.do_journal = do_journal == 'yes' 188 self.do_journal = do_journal == 'yes'
166 self.try_id_parsing = try_id_parsing == 'yes' 189 self.try_id_parsing = try_id_parsing == 'yes'
167 self.msg_header_property = msg_header_property 190 self.msg_header_property = msg_header_property
191
168 def __repr__(self): 192 def __repr__(self):
169 """more useful for dumps. But beware: This is also used in schema 193 """more useful for dumps. But beware: This is also used in schema
170 storage in SQL backends! 194 storage in SQL backends!
171 """ 195 """
172 return '<%s.%s to "%s">'%(self.__class__.__module__, 196 return '<%s.%s to "%s">' % (self.__class__.__module__,
173 self.__class__.__name__, self.classname) 197 self.__class__.__name__, self.classname)
198
174 199
175 class Link(_Pointer): 200 class Link(_Pointer):
176 """An object designating a Link property that links to a 201 """An object designating a Link property that links to a
177 node in a specified class.""" 202 node in a specified class."""
178 def from_raw(self, value, db, propname, **kw): 203 def from_raw(self, value, db, propname, **kw):
182 if self.try_id_parsing: 207 if self.try_id_parsing:
183 value = convertLinkValue(db, propname, self, value) 208 value = convertLinkValue(db, propname, self, value)
184 else: 209 else:
185 value = convertLinkValue(db, propname, self, value, None) 210 value = convertLinkValue(db, propname, self, value, None)
186 return value 211 return value
187 def sort_repr (self, cls, val, name): 212
213 def sort_repr(self, cls, val, name):
188 if not val: 214 if not val:
189 return val 215 return val
190 op = cls.labelprop() 216 op = cls.labelprop()
191 if op == 'id': 217 if op == 'id':
192 return int(cls.get(val, op)) 218 return int(cls.get(val, op))
193 return cls.get(val, op) 219 return cls.get(val, op)
194 220
221
195 class Multilink(_Pointer): 222 class Multilink(_Pointer):
196 """An object designating a Multilink property that links 223 """An object designating a Multilink property that links
197 to nodes in a specified class. 224 to nodes in a specified class.
198 225
199 "classname" indicates the class to link to 226 "classname" indicates the class to link to
200 227
201 "do_journal" indicates whether the linked-to nodes should have 228 "do_journal" indicates whether the linked-to nodes should have
202 'link' and 'unlink' events placed in their journal 229 'link' and 'unlink' events placed in their journal
203 """ 230 """
204 231
205 def __init__(self, classname, do_journal = 'yes', required = False, quiet=False, try_id_parsing='yes'): 232 def __init__(self, classname, do_journal='yes', required=False,
233 quiet=False, try_id_parsing='yes'):
206 234
207 super(Multilink, self).__init__(classname, 235 super(Multilink, self).__init__(classname,
208 do_journal, 236 do_journal,
209 required = required, 237 required=required,
210 default_value = [], quiet=quiet, 238 default_value=[], quiet=quiet,
211 try_id_parsing=try_id_parsing) 239 try_id_parsing=try_id_parsing)
212 240
213 def from_raw(self, value, db, klass, propname, itemid, **kw): 241 def from_raw(self, value, db, klass, propname, itemid, **kw):
214 if not value: 242 if not value:
215 return [] 243 return []
280 value = [int(x) for x in value] 308 value = [int(x) for x in value]
281 value.sort() 309 value.sort()
282 value = [str(x) for x in value] 310 value = [str(x) for x in value]
283 return value 311 return value
284 312
285 def sort_repr (self, cls, val, name): 313 def sort_repr(self, cls, val, name):
286 if not val: 314 if not val:
287 return val 315 return val
288 op = cls.labelprop() 316 op = cls.labelprop()
289 if op == 'id': 317 if op == 'id':
290 return [int(cls.get(v, op)) for v in val] 318 return [int(cls.get(v, op)) for v in val]
291 return [cls.get(v, op) for v in val] 319 return [cls.get(v, op) for v in val]
320
292 321
293 class Boolean(_Type): 322 class Boolean(_Type):
294 """An object designating a boolean property""" 323 """An object designating a boolean property"""
295 def from_raw(self, value, **kw): 324 def from_raw(self, value, **kw):
296 value = value.strip() 325 value = value.strip()
297 # checked is a common HTML checkbox value 326 # checked is a common HTML checkbox value
298 value = value.lower() in ('checked', 'yes', 'true', 'on', '1') 327 value = value.lower() in ('checked', 'yes', 'true', 'on', '1')
299 return value 328 return value
300 329
330
301 class Number(_Type): 331 class Number(_Type):
302 """An object designating a numeric property""" 332 """An object designating a numeric property"""
303 def __init__(self, use_double = False, **kw): 333 def __init__(self, use_double=False, **kw):
304 """ The value use_double tells the database backend to use a 334 """ The value use_double tells the database backend to use a
305 floating-point format with more precision than the default. 335 floating-point format with more precision than the default.
306 Usually implemented by type 'double precision' in the sql 336 Usually implemented by type 'double precision' in the sql
307 backend. The default is to use single-precision float (aka 337 backend. The default is to use single-precision float (aka
308 'real') in the db. Note that sqlite already uses 8-byte for 338 'real') in the db. Note that sqlite already uses 8-byte for
309 floating point numbers. 339 floating point numbers.
310 """ 340 """
311 self.use_double = use_double 341 self.use_double = use_double
312 super(Number, self).__init__(**kw) 342 super(Number, self).__init__(**kw)
343
313 def from_raw(self, value, **kw): 344 def from_raw(self, value, **kw):
314 value = value.strip() 345 value = value.strip()
315 try: 346 try:
316 value = float(value) 347 value = float(value)
317 except ValueError: 348 except ValueError:
318 raise HyperdbValueError(_('property %s: %r is not a number')%( 349 raise HyperdbValueError(_('property %s: %r is not a number') %
319 kw['propname'], value)) 350 (kw['propname'], value))
320 return value 351 return value
352
321 353
322 class Integer(_Type): 354 class Integer(_Type):
323 """An object designating an integer property""" 355 """An object designating an integer property"""
324 def from_raw(self, value, **kw): 356 def from_raw(self, value, **kw):
325 value = value.strip() 357 value = value.strip()
326 try: 358 try:
327 value = int(value) 359 value = int(value)
328 except ValueError: 360 except ValueError:
329 raise HyperdbValueError(_('property %s: %r is not an integer')%( 361 raise HyperdbValueError(_('property %s: %r is not an integer') %
330 kw['propname'], value)) 362 (kw['propname'], value))
331 return value 363 return value
364
365
332 # 366 #
333 # Support for splitting designators 367 # Support for splitting designators
334 # 368 #
335 class DesignatorError(ValueError): 369 class DesignatorError(ValueError):
336 pass 370 pass
371
372
337 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')): 373 def splitDesignator(designator, dre=re.compile(r'([^\d]+)(\d+)')):
338 """ Take a foo123 and return ('foo', 123) 374 """ Take a foo123 and return ('foo', 123)
339 """ 375 """
340 m = dre.match(designator) 376 m = dre.match(designator)
341 if m is None: 377 if m is None:
342 raise DesignatorError(_('"%s" not a node designator')%designator) 378 raise DesignatorError(_('"%s" not a node designator') % designator)
343 return m.group(1), m.group(2) 379 return m.group(1), m.group(2)
380
344 381
345 class Exact_Match(object): 382 class Exact_Match(object):
346 """ Used to encapsulate exact match semantics search values 383 """ Used to encapsulate exact match semantics search values
347 """ 384 """
348 def __init__(self, value): 385 def __init__(self, value):
349 self.value = value 386 self.value = value
387
350 388
351 class Proptree(object): 389 class Proptree(object):
352 """ Simple tree data structure for property lookup. Each node in 390 """ Simple tree data structure for property lookup. Each node in
353 the tree is a roundup Class Property that has to be navigated to 391 the tree is a roundup Class Property that has to be navigated to
354 find given property. The need_for attribute is used to mark nodes 392 find given property. The need_for attribute is used to mark nodes
372 self.classname = None 410 self.classname = None
373 self.uniqname = None 411 self.uniqname = None
374 self.children = [] 412 self.children = []
375 self.sortattr = [] 413 self.sortattr = []
376 self.propdict = {} 414 self.propdict = {}
377 self.need_for = {'search' : True} 415 self.need_for = {'search': True}
378 self.sort_direction = None 416 self.sort_direction = None
379 self.sort_ids = None 417 self.sort_ids = None
380 self.sort_ids_needed = False 418 self.sort_ids_needed = False
381 self.sort_result = None 419 self.sort_result = None
382 self.attr_sort_done = False 420 self.attr_sort_done = False
383 self.tree_sort_done = False 421 self.tree_sort_done = False
384 self.propclass = None 422 self.propclass = None
385 self.orderby = [] 423 self.orderby = []
386 self.sql_idx = None # index of retrieved column in sql result 424 self.sql_idx = None # index of retrieved column in sql result
387 if parent: 425 if parent:
388 self.root = parent.root 426 self.root = parent.root
389 self.depth = parent.depth + 1 427 self.depth = parent.depth + 1
390 else: 428 else:
391 self.root = self 429 self.root = self
416 cls = None 454 cls = None
417 props = None 455 props = None
418 if isinstance(propclass, (Link, Multilink)): 456 if isinstance(propclass, (Link, Multilink)):
419 cls = self.db.getclass(propclass.classname) 457 cls = self.db.getclass(propclass.classname)
420 props = cls.getprops() 458 props = cls.getprops()
421 child = self.__class__(self.db, cls, name, props, parent = self) 459 child = self.__class__(self.db, cls, name, props, parent=self)
422 child.need_for = {need_for : True} 460 child.need_for = {need_for: True}
423 child.propclass = propclass 461 child.propclass = propclass
424 self.children.append(child) 462 self.children.append(child)
425 self.propdict[name] = child 463 self.propdict[name] = child
426 if retr and isinstance(child.propclass, Link): 464 if retr and isinstance(child.propclass, Link):
427 child.append_retr_props() 465 child.append_retr_props()
439 ('sort' in self.need_for) or all children have tree_sort_done set and 477 ('sort' in self.need_for) or all children have tree_sort_done set and
440 sort_ids_needed unset: set self.tree_sort_done if one of the conditions 478 sort_ids_needed unset: set self.tree_sort_done if one of the conditions
441 holds. Also remove sort_ids_needed recursively once having seen a 479 holds. Also remove sort_ids_needed recursively once having seen a
442 Multilink that is used for sorting. 480 Multilink that is used for sorting.
443 """ 481 """
444 if isinstance (self.propclass, Multilink) and 'sort' in self.need_for: 482 if isinstance(self.propclass, Multilink) and 'sort' in self.need_for:
445 mlseen = True 483 mlseen = True
446 if mlseen: 484 if mlseen:
447 self.sort_ids_needed = False 485 self.sort_ids_needed = False
448 self.tree_sort_done = True 486 self.tree_sort_done = True
449 for p in self.children: 487 for p in self.children:
469 filterspec = {} 507 filterspec = {}
470 exact_match_spec = {} 508 exact_match_spec = {}
471 for p in self.children: 509 for p in self.children:
472 if 'search' in p.need_for: 510 if 'search' in p.need_for:
473 if p.children: 511 if p.children:
474 p.search(sort = False) 512 p.search(sort=False)
475 if isinstance(p.val, type([])): 513 if isinstance(p.val, type([])):
476 exact = [] 514 exact = []
477 subst = [] 515 subst = []
478 for v in p.val: 516 for v in p.val:
479 if isinstance(v, Exact_Match): 517 if isinstance(v, Exact_Match):
490 self.val = self.cls._filter(search_matches, filterspec, sort and self, 528 self.val = self.cls._filter(search_matches, filterspec, sort and self,
491 retired=retired, 529 retired=retired,
492 exact_match_spec=exact_match_spec) 530 exact_match_spec=exact_match_spec)
493 return self.val 531 return self.val
494 532
495 def sort (self, ids=None): 533 def sort(self, ids=None):
496 """ Sort ids by the order information stored in self. With 534 """ Sort ids by the order information stored in self. With
497 optimisations: Some order attributes may be precomputed (by the 535 optimisations: Some order attributes may be precomputed (by the
498 backend) and some properties may already be sorted. 536 backend) and some properties may already be sorted.
499 """ 537 """
500 if ids is None: 538 if ids is None:
501 ids = self.val 539 ids = self.val
502 if self.sortattr and [s for s in self.sortattr if not s.attr_sort_done]: 540 if self.sortattr and [s for s in self.sortattr
541 if not s.attr_sort_done]:
503 return self._searchsort(ids, True, True) 542 return self._searchsort(ids, True, True)
504 return ids 543 return ids
505 544
506 def sortable_children(self, intermediate=False): 545 def sortable_children(self, intermediate=False):
507 """ All children needed for sorting. If intermediate is True, 546 """ All children needed for sorting. If intermediate is True,
516 for p in self.children: 555 for p in self.children:
517 yield p 556 yield p
518 for c in p: 557 for c in p:
519 yield c 558 yield c
520 559
521 def _get (self, ids): 560 def _get(self, ids):
522 """Lookup given ids -- possibly a list of list. We recurse until 561 """Lookup given ids -- possibly a list of list. We recurse until
523 we have a list of ids. 562 we have a list of ids.
524 """ 563 """
525 if not ids: 564 if not ids:
526 return ids 565 return ids
527 if isinstance (ids[0], list): 566 if isinstance(ids[0], list):
528 cids = [self._get(i) for i in ids] 567 cids = [self._get(i) for i in ids]
529 else: 568 else:
530 cids = [i and self.parent.cls.get(i, self.name) for i in ids] 569 cids = [i and self.parent.cls.get(i, self.name) for i in ids]
531 if self.sortattr: 570 if self.sortattr:
532 cids = [self._searchsort(i, False, True) for i in cids] 571 cids = [self._searchsort(i, False, True) for i in cids]
551 issues are sorted by author and then by the time of the messages 590 issues are sorted by author and then by the time of the messages
552 *of this author*. Probably what the user intends in that case, 591 *of this author*. Probably what the user intends in that case,
553 so we do *not* use two sorted lists of messages, one sorted by 592 so we do *not* use two sorted lists of messages, one sorted by
554 author and one sorted by date for sorting issues. 593 author and one sorted by date for sorting issues.
555 """ 594 """
556 for pt in self.sortable_children(intermediate = True): 595 for pt in self.sortable_children(intermediate=True):
557 # ids can be an empty list 596 # ids can be an empty list
558 if pt.tree_sort_done or not ids: 597 if pt.tree_sort_done or not ids:
559 continue 598 continue
560 if pt.sort_ids: # cached or computed by backend 599 if pt.sort_ids: # cached or computed by backend
561 cids = pt.sort_ids 600 cids = pt.sort_ids
562 else: 601 else:
563 cids = pt._get(ids) 602 cids = pt._get(ids)
564 if pt.sort_direction and not pt.sort_result: 603 if pt.sort_direction and not pt.sort_result:
565 sortrep = pt.propclass.sort_repr 604 sortrep = pt.propclass.sort_repr
568 if pt.children: 607 if pt.children:
569 pt._searchsort(cids, update, False) 608 pt._searchsort(cids, update, False)
570 if self.sortattr and dosort: 609 if self.sortattr and dosort:
571 ids = self._sort(ids) 610 ids = self._sort(ids)
572 if not update: 611 if not update:
573 for pt in self.sortable_children(intermediate = True): 612 for pt in self.sortable_children(intermediate=True):
574 pt.sort_ids = None 613 pt.sort_ids = None
575 for pt in self.sortattr: 614 for pt in self.sortattr:
576 pt.sort_result = None 615 pt.sort_result = None
577 return ids 616 return ids
578 617
634 for sa in self.sortattr: 673 for sa in self.sortattr:
635 if sa.attr_sort_done: 674 if sa.attr_sort_done:
636 break 675 break
637 if sortattr: 676 if sortattr:
638 assert len(sortattr[0]) == len(sa.sort_result) 677 assert len(sortattr[0]) == len(sa.sort_result)
639 sortattr.append (sa.sort_result) 678 sortattr.append(sa.sort_result)
640 if curdir != sa.sort_direction: 679 if curdir != sa.sort_direction:
641 dir_idx.append (idx) 680 dir_idx.append(idx)
642 directions.append (sa.sort_direction) 681 directions.append(sa.sort_direction)
643 curdir = sa.sort_direction 682 curdir = sa.sort_direction
644 idx += 1 683 idx += 1
645 sortattr.append (val) 684 sortattr.append(val)
646 sortattr = zip (*sortattr) 685 sortattr = zip(*sortattr)
647 for dir, i in reversed(list(zip(directions, dir_idx))): 686 for dir, i in reversed(list(zip(directions, dir_idx))):
648 rev = dir == '-' 687 rev = dir == '-'
649 sortattr = sorted (sortattr, 688 sortattr = sorted(sortattr,
650 key = lambda x: NoneAndDictComparable(x[i:idx]), 689 key=lambda x: NoneAndDictComparable(x[i:idx]),
651 reverse = rev) 690 reverse=rev)
652 idx = i 691 idx = i
653 return [x[-1] for x in sortattr] 692 return [x[-1] for x in sortattr]
654 693
655 def _sort_repr(self, sortrep, ids): 694 def _sort_repr(self, sortrep, ids):
656 """Call sortrep for given ids -- possibly a list of list. We 695 """Call sortrep for given ids -- possibly a list of list. We
657 recurse until we have a list of ids. 696 recurse until we have a list of ids.
658 """ 697 """
659 if not ids: 698 if not ids:
660 return ids 699 return ids
661 if isinstance (ids[0], list): 700 if isinstance(ids[0], list):
662 res = [self._sort_repr(sortrep, i) for i in ids] 701 res = [self._sort_repr(sortrep, i) for i in ids]
663 else: 702 else:
664 res = [sortrep(self.cls, i, self.name) for i in ids] 703 res = [sortrep(self.cls, i, self.name) for i in ids]
665 return res 704 return res
666 705
668 r = ["proptree:" + self.name] 707 r = ["proptree:" + self.name]
669 for n in self: 708 for n in self:
670 r.append("proptree:" + " " * n.depth + n.name) 709 r.append("proptree:" + " " * n.depth + n.name)
671 return '\n'.join(r) 710 return '\n'.join(r)
672 __str__ = __repr__ 711 __str__ = __repr__
712
673 713
674 # 714 #
675 # the base Database class 715 # the base Database class
676 # 716 #
677 class DatabaseError(ValueError): 717 class DatabaseError(ValueError):
678 """Error to be raised when there is some problem in the database code 718 """Error to be raised when there is some problem in the database code
679 """ 719 """
680 pass 720 pass
721
722
681 class Database: 723 class Database:
682 """A database for storing records containing flexible data types. 724 """A database for storing records containing flexible data types.
683 725
684 This class defines a hyperdatabase storage layer, which the Classes use to 726 This class defines a hyperdatabase storage layer, which the Classes use to
685 store their data. 727 store their data.
863 """Close the database. 905 """Close the database.
864 906
865 This method must be called at the end of processing. 907 This method must be called at the end of processing.
866 908
867 """ 909 """
910
868 911
869 def iter_roles(roles): 912 def iter_roles(roles):
870 ''' handle the text processing of turning the roles list 913 ''' handle the text processing of turning the roles list
871 into something python can use more easily 914 into something python can use more easily
872 ''' 915 '''
893 or a ValueError is raised. The keyword arguments in 'properties' 936 or a ValueError is raised. The keyword arguments in 'properties'
894 must map names to property objects, or a TypeError is raised. 937 must map names to property objects, or a TypeError is raised.
895 """ 938 """
896 for name in 'creation activity creator actor'.split(): 939 for name in 'creation activity creator actor'.split():
897 if name in properties: 940 if name in properties:
898 raise ValueError('"creation", "activity", "creator" and '\ 941 raise ValueError('"creation", "activity", "creator" and '
899 '"actor" are reserved') 942 '"actor" are reserved')
900 943
901 self.classname = classname 944 self.classname = classname
902 self.properties = properties 945 self.properties = properties
903 self.db = weakref.proxy(db) # use a weak ref to avoid circularity 946 self.db = weakref.proxy(db) # use a weak ref to avoid circularity
904 self.key = '' 947 self.key = ''
914 self.reactors = dict([(a, PrioList()) for a in actions]) 957 self.reactors = dict([(a, PrioList()) for a in actions])
915 958
916 def __repr__(self): 959 def __repr__(self):
917 """Slightly more useful representation 960 """Slightly more useful representation
918 """ 961 """
919 return '<hyperdb.Class "%s">'%self.classname 962 return '<hyperdb.Class "%s">' % self.classname
920 963
921 # Editing nodes: 964 # Editing nodes:
922 965
923 def create(self, **propvalues): 966 def create(self, **propvalues):
924 """Create a new node of this class and return its id. 967 """Create a new node of this class and return its id.
938 node, an IndexError is raised. 981 node, an IndexError is raised.
939 """ 982 """
940 raise NotImplementedError 983 raise NotImplementedError
941 984
942 _marker = [] 985 _marker = []
986
943 def get(self, nodeid, propname, default=_marker, cache=1): 987 def get(self, nodeid, propname, default=_marker, cache=1):
944 """Get the value of a property on an existing node of this class. 988 """Get the value of a property on an existing node of this class.
945 989
946 'nodeid' must be the id of an existing node of this class or an 990 'nodeid' must be the id of an existing node of this class or an
947 IndexError is raised. 'propname' must be the name of a property 991 IndexError is raised. 'propname' must be the name of a property
1059 raise ValueError('Journalling is disabled for this class') 1103 raise ValueError('Journalling is disabled for this class')
1060 1104
1061 perm = self.db.security.hasPermission 1105 perm = self.db.security.hasPermission
1062 journal = [] 1106 journal = []
1063 1107
1064 uid=self.db.getuid() # id of the person requesting the history 1108 uid = self.db.getuid() # id of the person requesting the history
1065 1109
1066 # Roles of the user and the configured obsolete_history_roles 1110 # Roles of the user and the configured obsolete_history_roles
1067 hr = set(iter_roles(self.db.config.OBSOLETE_HISTORY_ROLES)) 1111 hr = set(iter_roles(self.db.config.OBSOLETE_HISTORY_ROLES))
1068 ur = set(self.db.user.get_roles(uid)) 1112 ur = set(self.db.user.get_roles(uid))
1069 allow_obsolete = bool(hr & ur) 1113 allow_obsolete = bool(hr & ur)
1073 # property is quiet 1117 # property is quiet
1074 # property is not (viewable or editable) 1118 # property is not (viewable or editable)
1075 # property is obsolete and not allow_obsolete 1119 # property is obsolete and not allow_obsolete
1076 id, evt_date, user, action, args = j 1120 id, evt_date, user, action, args = j
1077 if logger.isEnabledFor(logging.DEBUG): 1121 if logger.isEnabledFor(logging.DEBUG):
1078 j_repr = "%s"%(j,) 1122 j_repr = "%s" % (j,)
1079 else: 1123 else:
1080 j_repr='' 1124 j_repr = ''
1081 if args and type(args) == type({}): 1125 if args and isinstance(args, type({})):
1082 for key in list(args.keys()): 1126 for key in list(args.keys()):
1083 if key not in self.properties : 1127 if key not in self.properties:
1084 if enforceperm and not allow_obsolete: 1128 if enforceperm and not allow_obsolete:
1085 del args[key] 1129 del args[key]
1086 continue 1130 continue
1087 if skipquiet and self.properties[key].quiet: 1131 if skipquiet and self.properties[key].quiet:
1088 logger.debug("skipping quiet property" 1132 logger.debug("skipping quiet property"
1089 " %s::%s in %s", 1133 " %s::%s in %s",
1090 self.classname, key, j_repr) 1134 self.classname, key, j_repr)
1091 del args[key] 1135 del args[key]
1092 continue 1136 continue
1093 if enforceperm and not ( perm("View", 1137 if enforceperm and not (perm("View",
1094 uid, 1138 uid,
1095 self.classname, 1139 self.classname,
1096 property=key ) or perm("Edit", 1140 property=key) or
1097 uid, 1141 perm("Edit",
1098 self.classname, 1142 uid,
1099 property=key )): 1143 self.classname,
1144 property=key)):
1100 logger.debug("skipping unaccessible property " 1145 logger.debug("skipping unaccessible property "
1101 "%s::%s seen by user%s in %s", 1146 "%s::%s seen by user%s in %s",
1102 self.classname, key, uid, j_repr) 1147 self.classname, key, uid, j_repr)
1103 del args[key] 1148 del args[key]
1104 continue 1149 continue
1105 if not args: 1150 if not args:
1106 logger.debug("Omitting journal entry for %s%s" 1151 logger.debug("Omitting journal entry for %s%s"
1107 " all props removed in: %s", 1152 " all props removed in: %s",
1108 self.classname, nodeid, j_repr) 1153 self.classname, nodeid, j_repr)
1109 continue 1154 continue
1110 journal.append(j) 1155 journal.append(j)
1111 elif action in ['link', 'unlink' ] and type(args) == type(()): 1156 elif action in ['link', 'unlink'] and isinstance(args, type(())):
1112 # definitions: 1157 # definitions:
1113 # myself - object whose history is being filtered 1158 # myself - object whose history is being filtered
1114 # linkee - object/class whose property is changing to 1159 # linkee - object/class whose property is changing to
1115 # include/remove myself 1160 # include/remove myself
1116 # link property - property of the linkee class that is changing 1161 # link property - property of the linkee class that is changing
1138 if not enforceperm or allow_obsolete: 1183 if not enforceperm or allow_obsolete:
1139 journal.append(j) 1184 journal.append(j)
1140 continue 1185 continue
1141 # obsolete linked-to item 1186 # obsolete linked-to item
1142 try: 1187 try:
1143 k = cls.get (linkid, key) 1188 cls.get(linkid, key) # does linkid exist
1144 except IndexError: 1189 except IndexError:
1145 if not enforceperm or allow_obsolete: 1190 if not enforceperm or allow_obsolete:
1146 journal.append(j) 1191 journal.append(j)
1147 continue 1192 continue
1148 # is the updated property quiet? 1193 # is the updated property quiet?
1151 "%s %sed %s%s", 1196 "%s %sed %s%s",
1152 j_repr, action, self.classname, nodeid) 1197 j_repr, action, self.classname, nodeid)
1153 continue 1198 continue
1154 # can user view the property in linkee class 1199 # can user view the property in linkee class
1155 if enforceperm and not (perm("View", 1200 if enforceperm and not (perm("View",
1156 uid, 1201 uid,
1157 linkcl, 1202 linkcl,
1158 property=key) or perm("Edit", 1203 property=key) or
1159 uid, 1204 perm("Edit",
1160 linkcl, 1205 uid,
1161 property=key)): 1206 linkcl,
1207 property=key)):
1162 logger.debug("skipping unaccessible property: " 1208 logger.debug("skipping unaccessible property: "
1163 "%s with uid %s %sed %s%s", 1209 "%s with uid %s %sed %s%s",
1164 j_repr, uid, action, 1210 j_repr, uid, action,
1165 self.classname, nodeid) 1211 self.classname, nodeid)
1166 continue 1212 continue
1167 # check access to linkee object 1213 # check access to linkee object
1168 if enforceperm and not (perm("View", 1214 if enforceperm and not (perm("View",
1169 uid, 1215 uid,
1170 cls.classname, 1216 cls.classname,
1171 itemid=linkid) or perm("Edit", 1217 itemid=linkid) or
1172 uid, 1218 perm("Edit",
1173 cls.classname, 1219 uid,
1174 itemid=linkid)): 1220 cls.classname,
1221 itemid=linkid)):
1175 logger.debug("skipping unaccessible object: " 1222 logger.debug("skipping unaccessible object: "
1176 "%s uid %s %sed %s%s", 1223 "%s uid %s %sed %s%s",
1177 j_repr, uid, action, 1224 j_repr, uid, action,
1178 self.classname, nodeid) 1225 self.classname, nodeid)
1179 continue 1226 continue
1236 4. first property from the sorted property name list 1283 4. first property from the sorted property name list
1237 """ 1284 """
1238 if hasattr(self, '_labelprop'): 1285 if hasattr(self, '_labelprop'):
1239 return self._labelprop 1286 return self._labelprop
1240 k = self.getkey() 1287 k = self.getkey()
1241 if k: 1288 if k:
1242 return k 1289 return k
1243 props = self.getprops() 1290 props = self.getprops()
1244 if 'name' in props: 1291 if 'name' in props:
1245 return 'name' 1292 return 'name'
1246 elif 'title' in props: 1293 elif 'title' in props:
1293 1340
1294 db.issue.find(messages={'1':1,'3':1}, files={'7':1}) 1341 db.issue.find(messages={'1':1,'3':1}, files={'7':1})
1295 """ 1342 """
1296 raise NotImplementedError 1343 raise NotImplementedError
1297 1344
1298 def _filter(self, search_matches, filterspec, sort=(None,None), 1345 def _filter(self, search_matches, filterspec, sort=(None, None),
1299 group=(None,None), retired=False, exact_match_spec={}): 1346 group=(None, None), retired=False, exact_match_spec={}):
1300 """For some backends this implements the non-transitive 1347 """For some backends this implements the non-transitive
1301 search, for more information see the filter method. 1348 search, for more information see the filter method.
1302 """ 1349 """
1303 raise NotImplementedError 1350 raise NotImplementedError
1304 1351
1315 for key, v in spec.items(): 1362 for key, v in spec.items():
1316 keys = key.split('.') 1363 keys = key.split('.')
1317 p = proptree 1364 p = proptree
1318 mlseen = False 1365 mlseen = False
1319 for k in keys: 1366 for k in keys:
1320 if isinstance (p.propclass, Multilink): 1367 if isinstance(p.propclass, Multilink):
1321 mlseen = True 1368 mlseen = True
1322 isnull = v == '-1' or v is None 1369 isnull = v == '-1' or v is None
1323 islist = isinstance(v, type([])) 1370 islist = isinstance(v, type([]))
1324 nullin = islist and ('-1' in v or None in v) 1371 nullin = islist and ('-1' in v or None in v)
1325 r = retr and not mlseen and not isnull and not nullin 1372 r = retr and not mlseen and not isnull and not nullin
1338 for s in sortattr: 1385 for s in sortattr:
1339 keys = s[1].split('.') 1386 keys = s[1].split('.')
1340 p = proptree 1387 p = proptree
1341 mlseen = False 1388 mlseen = False
1342 for k in keys: 1389 for k in keys:
1343 if isinstance (p.propclass, Multilink): 1390 if isinstance(p.propclass, Multilink):
1344 mlseen = True 1391 mlseen = True
1345 r = retr and not mlseen 1392 r = retr and not mlseen
1346 p = p.append(k, need_for='sort', retr=r) 1393 p = p.append(k, need_for='sort', retr=r)
1347 if isinstance (p.propclass, Multilink): 1394 if isinstance(p.propclass, Multilink):
1348 multilinks[p] = True 1395 multilinks[p] = True
1349 if p.cls: 1396 if p.cls:
1350 p = p.append(p.cls.orderprop(), need_for='sort') 1397 p = p.append(p.cls.orderprop(), need_for='sort')
1351 if p.sort_direction: # if an orderprop is also specified explicitly 1398 if p.sort_direction: # if orderprop is also specified explicitly
1352 continue 1399 continue
1353 p.sort_direction = s[0] 1400 p.sort_direction = s[0]
1354 proptree.sortattr.append (p) 1401 proptree.sortattr.append(p)
1355 for p in multilinks.keys(): 1402 for p in multilinks.keys():
1356 sattr = {} 1403 sattr = {}
1357 for c in p: 1404 for c in p:
1358 if c.sort_direction: 1405 if c.sort_direction:
1359 sattr [c] = True 1406 sattr[c] = True
1360 for sa in proptree.sortattr: 1407 for sa in proptree.sortattr:
1361 if sa in sattr: 1408 if sa in sattr:
1362 p.sortattr.append (sa) 1409 p.sortattr.append(sa)
1363 return proptree 1410 return proptree
1364 1411
1365 def get_transitive_prop(self, propname_path, default = None): 1412 def get_transitive_prop(self, propname_path, default=None):
1366 """Expand a transitive property (individual property names 1413 """Expand a transitive property (individual property names
1367 separated by '.' into a new property at the end of the path. If 1414 separated by '.' into a new property at the end of the path. If
1368 one of the names does not refer to a valid property, we return 1415 one of the names does not refer to a valid property, we return
1369 None. 1416 None.
1370 Example propname_path (for class issue): "messages.author" 1417 Example propname_path (for class issue): "messages.author"
1393 srt = [srt] 1440 srt = [srt]
1394 for s in srt: 1441 for s in srt:
1395 if s[1] and s[1] not in seen: 1442 if s[1] and s[1] not in seen:
1396 sortattr.append((s[0] or '+', s[1])) 1443 sortattr.append((s[0] or '+', s[1]))
1397 seen[s[1]] = True 1444 seen[s[1]] = True
1398 if 'id' not in seen : 1445 if 'id' not in seen:
1399 sortattr.append(('+', 'id')) 1446 sortattr.append(('+', 'id'))
1400 return sortattr 1447 return sortattr
1401 1448
1402 def filter(self, search_matches, filterspec, sort=[], group=[], 1449 def filter(self, search_matches, filterspec, sort=[], group=[],
1403 retired=False, exact_match_spec={}, limit=None, offset=None): 1450 retired=False, exact_match_spec={}, limit=None, offset=None):
1456 using _filter implemented in a backend class. A more efficient 1503 using _filter implemented in a backend class. A more efficient
1457 version can be implemented in the individual backends -- e.g., 1504 version can be implemented in the individual backends -- e.g.,
1458 an SQL backend will want to create a single SQL statement and 1505 an SQL backend will want to create a single SQL statement and
1459 override the filter method instead of implementing _filter. 1506 override the filter method instead of implementing _filter.
1460 """ 1507 """
1461 sortattr = self._sortattr(sort = sort, group = group) 1508 sortattr = self._sortattr(sort=sort, group=group)
1462 proptree = self._proptree(filterspec, exact_match_spec, sortattr) 1509 proptree = self._proptree(filterspec, exact_match_spec, sortattr)
1463 proptree.search(search_matches, retired=retired) 1510 proptree.search(search_matches, retired=retired)
1464 if offset is not None or limit is not None: 1511 if offset is not None or limit is not None:
1465 items = proptree.sort() 1512 items = proptree.sort()
1466 if limit and offset: 1513 if limit and offset:
1469 return items[offset:] 1516 return items[offset:]
1470 else: 1517 else:
1471 return items[:limit] 1518 return items[:limit]
1472 return proptree.sort() 1519 return proptree.sort()
1473 1520
1474
1475 # non-optimized filter_iter, a backend may chose to implement a 1521 # non-optimized filter_iter, a backend may chose to implement a
1476 # better version that provides a real iterator that pre-fills the 1522 # better version that provides a real iterator that pre-fills the
1477 # cache for each id returned. Note that the filter_iter doesn't 1523 # cache for each id returned. Note that the filter_iter doesn't
1478 # promise to correctly sort by multilink (which isn't sane to do 1524 # promise to correctly sort by multilink (which isn't sane to do
1479 # anyway). 1525 # anyway).
1494 If the "protected" flag is true, we include protected properties - 1540 If the "protected" flag is true, we include protected properties -
1495 those which may not be modified. 1541 those which may not be modified.
1496 """ 1542 """
1497 raise NotImplementedError 1543 raise NotImplementedError
1498 1544
1499 def get_required_props(self, propnames = []): 1545 def get_required_props(self, propnames=[]):
1500 """Return a dict of property names mapping to property objects. 1546 """Return a dict of property names mapping to property objects.
1501 All properties that have the "required" flag set will be 1547 All properties that have the "required" flag set will be
1502 returned in addition to all properties in the propnames 1548 returned in addition to all properties in the propnames
1503 parameter. 1549 parameter.
1504 """ 1550 """
1505 props = self.getprops(protected = False) 1551 props = self.getprops(protected=False)
1506 pdict = dict([(p, props[p]) for p in propnames]) 1552 pdict = dict([(p, props[p]) for p in propnames])
1507 pdict.update([(k, v) for k, v in props.items() if v.required]) 1553 pdict.update([(k, v) for k, v in props.items() if v.required])
1508 return pdict 1554 return pdict
1509 1555
1510 def addprop(self, **properties): 1556 def addprop(self, **properties):
1522 raise NotImplementedError 1568 raise NotImplementedError
1523 1569
1524 # 1570 #
1525 # Detector interface 1571 # Detector interface
1526 # 1572 #
1527 def audit(self, event, detector, priority = 100): 1573 def audit(self, event, detector, priority=100):
1528 """Register an auditor detector""" 1574 """Register an auditor detector"""
1529 self.auditors[event].append((priority, detector.__name__, detector)) 1575 self.auditors[event].append((priority, detector.__name__, detector))
1530 1576
1531 def fireAuditors(self, event, nodeid, newvalues): 1577 def fireAuditors(self, event, nodeid, newvalues):
1532 """Fire all registered auditors""" 1578 """Fire all registered auditors"""
1533 for prio, name, audit in self.auditors[event]: 1579 for _prio, _name, audit in self.auditors[event]:
1534 try: 1580 try:
1535 audit(self.db, self, nodeid, newvalues) 1581 audit(self.db, self, nodeid, newvalues)
1536 except (EnvironmentError, ArithmeticError) as e: 1582 except (EnvironmentError, ArithmeticError) as e:
1537 tb = traceback.format_exc() 1583 tb = traceback.format_exc()
1538 html = ("<h1>Traceback</h1>" + str(tb).replace('\n', '<br>'). 1584 html = ("<h1>Traceback</h1>" + str(tb).replace('\n', '<br>').
1540 txt = 'Caught exception %s: %s\n%s' % (str(type(e)), e, tb) 1586 txt = 'Caught exception %s: %s\n%s' % (str(type(e)), e, tb)
1541 exc_info = sys.exc_info() 1587 exc_info = sys.exc_info()
1542 subject = "Error: %s" % exc_info[1] 1588 subject = "Error: %s" % exc_info[1]
1543 raise DetectorError(subject, html, txt) 1589 raise DetectorError(subject, html, txt)
1544 1590
1545 def react(self, event, detector, priority = 100): 1591 def react(self, event, detector, priority=100):
1546 """Register a reactor detector""" 1592 """Register a reactor detector"""
1547 self.reactors[event].append((priority, detector.__name__, detector)) 1593 self.reactors[event].append((priority, detector.__name__, detector))
1548 1594
1549 def fireReactors(self, event, nodeid, oldvalues): 1595 def fireReactors(self, event, nodeid, oldvalues):
1550 """Fire all registered reactors""" 1596 """Fire all registered reactors"""
1551 for prio, name, react in self.reactors[event]: 1597 for _prio, _name, react in self.reactors[event]:
1552 try: 1598 try:
1553 react(self.db, self, nodeid, oldvalues) 1599 react(self.db, self, nodeid, oldvalues)
1554 except (EnvironmentError, ArithmeticError) as e: 1600 except (EnvironmentError, ArithmeticError) as e:
1555 tb = traceback.format_exc() 1601 tb = traceback.format_exc()
1556 html = ("<h1>Traceback</h1>" + str(tb).replace('\n', '<br>'). 1602 html = ("<h1>Traceback</h1>" + str(tb).replace('\n', '<br>').
1579 properties = self.getprops() 1625 properties = self.getprops()
1580 a = [] 1626 a = []
1581 for l in entries: 1627 for l in entries:
1582 # first element in sorted list is the (numeric) id 1628 # first element in sorted list is the (numeric) id
1583 # in python2.4 and up we would use sorted with a key... 1629 # in python2.4 and up we would use sorted with a key...
1584 a.append ((int (l [0].strip ("'")), l)) 1630 a.append((int(l[0].strip("'")), l))
1585 a.sort () 1631 a.sort()
1586
1587 1632
1588 last = 0 1633 last = 0
1589 r = [] 1634 r = []
1590 for n, l in a: 1635 for n, l in a:
1591 nodeid, jdate, user, action, params = map(eval_import, l) 1636 nodeid, jdate, user, action, params = map(eval_import, l)
1637 1682
1638 For convenience reasons we take a list. 1683 For convenience reasons we take a list.
1639 In standard schemas only a user has a roles property but 1684 In standard schemas only a user has a roles property but
1640 this may be different in customized schemas. 1685 this may be different in customized schemas.
1641 ''' 1686 '''
1642 roles = dict.fromkeys ([r.strip().lower() for r in roles]) 1687 roles = dict.fromkeys([r.strip().lower() for r in roles])
1643 for role in self.get_roles(nodeid): 1688 for role in self.get_roles(nodeid):
1644 if role in roles: 1689 if role in roles:
1645 return True 1690 return True
1646 return False 1691 return False
1647 1692
1648 1693
1649 class HyperdbValueError(ValueError): 1694 class HyperdbValueError(ValueError):
1650 """ Error converting a raw value into a Hyperdb value """ 1695 """ Error converting a raw value into a Hyperdb value """
1651 pass 1696 pass
1697
1652 1698
1653 def convertLinkValue(db, propname, prop, value, idre=re.compile(r'^\d+$')): 1699 def convertLinkValue(db, propname, prop, value, idre=re.compile(r'^\d+$')):
1654 """ Convert the link value (may be id or key value) to an id value. """ 1700 """ Convert the link value (may be id or key value) to an id value. """
1655 linkcl = db.classes[prop.classname] 1701 linkcl = db.classes[prop.classname]
1656 if not idre or not idre.match(value): 1702 if not idre or not idre.match(value):
1657 if linkcl.getkey(): 1703 if linkcl.getkey():
1658 try: 1704 try:
1659 value = linkcl.lookup(value) 1705 value = linkcl.lookup(value)
1660 except KeyError as message: 1706 except KeyError:
1661 raise HyperdbValueError(_('property %s: %r is not a %s.')%( 1707 raise HyperdbValueError(_('property %s: %r is not a %s.') % (
1662 propname, value, prop.classname)) 1708 propname, value, prop.classname))
1663 else: 1709 else:
1664 raise HyperdbValueError(_('you may only enter ID values '\ 1710 raise HyperdbValueError(_('you may only enter ID values '
1665 'for property %s')%propname) 1711 'for property %s') % propname)
1666 return value 1712 return value
1713
1667 1714
1668 def fixNewlines(text): 1715 def fixNewlines(text):
1669 """ Homogenise line endings. 1716 """ Homogenise line endings.
1670 1717
1671 Different web clients send different line ending values, but 1718 Different web clients send different line ending values, but
1675 if text is not None: 1722 if text is not None:
1676 text = text.replace('\r\n', '\n') 1723 text = text.replace('\r\n', '\n')
1677 return text.replace('\r', '\n') 1724 return text.replace('\r', '\n')
1678 return text 1725 return text
1679 1726
1727
1680 def rawToHyperdb(db, klass, itemid, propname, value, **kw): 1728 def rawToHyperdb(db, klass, itemid, propname, value, **kw):
1681 """ Convert the raw (user-input) value to a hyperdb-storable value. The 1729 """ Convert the raw (user-input) value to a hyperdb-storable value. The
1682 value is for the "propname" property on itemid (may be None for a 1730 value is for the "propname" property on itemid (may be None for a
1683 new item) of "klass" in "db". 1731 new item) of "klass" in "db".
1684 1732
1689 properties = klass.getprops() 1737 properties = klass.getprops()
1690 1738
1691 # ensure it's a valid property name 1739 # ensure it's a valid property name
1692 propname = propname.strip() 1740 propname = propname.strip()
1693 try: 1741 try:
1694 proptype = properties[propname] 1742 proptype = properties[propname]
1695 except KeyError: 1743 except KeyError:
1696 raise HyperdbValueError(_('%r is not a property of %s')%(propname, 1744 raise HyperdbValueError(_('%r is not a property of %s') % (
1697 klass.classname)) 1745 propname, klass.classname))
1698 1746
1699 # if we got a string, strip it now 1747 # if we got a string, strip it now
1700 if isinstance(value, type('')): 1748 if isinstance(value, type('')):
1701 value = value.strip() 1749 value = value.strip()
1702 1750
1703 # convert the input value to a real property value 1751 # convert the input value to a real property value
1704 value = proptype.from_raw(value, db=db, klass=klass, 1752 value = proptype.from_raw(value, db=db, klass=klass,
1705 propname=propname, itemid=itemid, **kw) 1753 propname=propname, itemid=itemid, **kw)
1706 1754
1707 return value 1755 return value
1756
1708 1757
1709 class FileClass: 1758 class FileClass:
1710 """ A class that requires the "content" property and stores it on 1759 """ A class that requires the "content" property and stores it on
1711 disk. 1760 disk.
1712 """ 1761 """
1766 index_content = index_content.decode('utf-8', errors='ignore') 1815 index_content = index_content.decode('utf-8', errors='ignore')
1767 # indexer will only index text mime type. It will skip 1816 # indexer will only index text mime type. It will skip
1768 # other types. So if mime type of file is correct, we 1817 # other types. So if mime type of file is correct, we
1769 # call add_text on content. 1818 # call add_text on content.
1770 self.db.indexer.add_text((self.classname, nodeid, 'content'), 1819 self.db.indexer.add_text((self.classname, nodeid, 'content'),
1771 index_content, mime_type) 1820 index_content, mime_type)
1821
1772 1822
1773 class Node: 1823 class Node:
1774 """ A convenience wrapper for the given node 1824 """ A convenience wrapper for the given node
1775 """ 1825 """
1776 def __init__(self, cl, nodeid, cache=1): 1826 def __init__(self, cl, nodeid, cache=1):
1777 self.__dict__['cl'] = cl 1827 self.__dict__['cl'] = cl
1778 self.__dict__['nodeid'] = nodeid 1828 self.__dict__['nodeid'] = nodeid
1829
1779 def keys(self, protected=1): 1830 def keys(self, protected=1):
1780 return list(self.cl.getprops(protected=protected).keys()) 1831 return list(self.cl.getprops(protected=protected).keys())
1832
1781 def values(self, protected=1): 1833 def values(self, protected=1):
1782 l = [] 1834 l = []
1783 for name in self.cl.getprops(protected=protected).keys(): 1835 for name in self.cl.getprops(protected=protected).keys():
1784 l.append(self.cl.get(self.nodeid, name)) 1836 l.append(self.cl.get(self.nodeid, name))
1785 return l 1837 return l
1838
1786 def items(self, protected=1): 1839 def items(self, protected=1):
1787 l = [] 1840 l = []
1788 for name in self.cl.getprops(protected=protected).keys(): 1841 for name in self.cl.getprops(protected=protected).keys():
1789 l.append((name, self.cl.get(self.nodeid, name))) 1842 l.append((name, self.cl.get(self.nodeid, name)))
1790 return l 1843 return l
1844
1791 def has_key(self, name): 1845 def has_key(self, name):
1792 return name in self.cl.getprops() 1846 return name in self.cl.getprops()
1847
1793 def get(self, name, default=None): 1848 def get(self, name, default=None):
1794 if name in self: 1849 if name in self:
1795 return self[name] 1850 return self[name]
1796 else: 1851 else:
1797 return default 1852 return default
1853
1798 def __getattr__(self, name): 1854 def __getattr__(self, name):
1799 if name in self.__dict__: 1855 if name in self.__dict__:
1800 return self.__dict__[name] 1856 return self.__dict__[name]
1801 try: 1857 try:
1802 return self.cl.get(self.nodeid, name) 1858 return self.cl.get(self.nodeid, name)
1803 except KeyError as value: 1859 except KeyError as value:
1804 # we trap this but re-raise it as AttributeError - all other 1860 # we trap this but re-raise it as AttributeError - all other
1805 # exceptions should pass through untrapped 1861 # exceptions should pass through untrapped
1806 raise AttributeError(str(value)) 1862 raise AttributeError(str(value))
1863
1807 def __getitem__(self, name): 1864 def __getitem__(self, name):
1808 return self.cl.get(self.nodeid, name) 1865 return self.cl.get(self.nodeid, name)
1866
1809 def __setattr__(self, name, value): 1867 def __setattr__(self, name, value):
1810 try: 1868 try:
1811 return self.cl.set(self.nodeid, **{name: value}) 1869 return self.cl.set(self.nodeid, **{name: value})
1812 except KeyError as value: 1870 except KeyError as value:
1813 # we trap this but re-raise it as AttributeError - all other 1871 # we trap this but re-raise it as AttributeError - all other
1814 # exceptions should pass through untrapped 1872 # exceptions should pass through untrapped
1815 raise AttributeError(str(value)) 1873 raise AttributeError(str(value))
1874
1816 def __setitem__(self, name, value): 1875 def __setitem__(self, name, value):
1817 self.cl.set(self.nodeid, **{name: value}) 1876 self.cl.set(self.nodeid, **{name: value})
1877
1818 def history(self, enforceperm=True, skipquiet=True): 1878 def history(self, enforceperm=True, skipquiet=True):
1819 return self.cl.history(self.nodeid, 1879 return self.cl.history(self.nodeid,
1820 enforceperm=enforceperm, 1880 enforceperm=enforceperm,
1821 skipquiet=skipquiet ) 1881 skipquiet=skipquiet)
1882
1822 def retire(self): 1883 def retire(self):
1823 return self.cl.retire(self.nodeid) 1884 return self.cl.retire(self.nodeid)
1824 1885
1825 1886
1826 def Choice(name, db, *options): 1887 def Choice(name, db, *options):

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