17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
17 # with CubicWeb. If not, see <http://www.gnu.org/licenses/>. |
18 """Base class for entity objects manipulated in clients""" |
18 """Base class for entity objects manipulated in clients""" |
19 |
19 |
20 __docformat__ = "restructuredtext en" |
20 __docformat__ = "restructuredtext en" |
21 |
21 |
|
22 from copy import copy |
22 from warnings import warn |
23 from warnings import warn |
23 |
24 |
24 from logilab.common import interface |
25 from logilab.common import interface |
25 from logilab.common.decorators import cached |
26 from logilab.common.decorators import cached |
26 from logilab.common.deprecation import deprecated |
27 from logilab.common.deprecation import deprecated |
49 if card in '+*': |
50 if card in '+*': |
50 return card |
51 return card |
51 return '1' |
52 return '1' |
52 |
53 |
53 |
54 |
54 class Entity(AppObject, dict): |
55 class Entity(AppObject): |
55 """an entity instance has e_schema automagically set on |
56 """an entity instance has e_schema automagically set on |
56 the class and instances has access to their issuing cursor. |
57 the class and instances has access to their issuing cursor. |
57 |
58 |
58 A property is set for each attribute and relation on each entity's type |
59 A property is set for each attribute and relation on each entity's type |
59 class. Becare that among attributes, 'eid' is *NEITHER* stored in the |
60 class. Becare that among attributes, 'eid' is *NEITHER* stored in the |
285 {'x': created.eid}, build_descr=False) |
286 {'x': created.eid}, build_descr=False) |
286 return created |
287 return created |
287 |
288 |
288 def __init__(self, req, rset=None, row=None, col=0): |
289 def __init__(self, req, rset=None, row=None, col=0): |
289 AppObject.__init__(self, req, rset=rset, row=row, col=col) |
290 AppObject.__init__(self, req, rset=rset, row=row, col=col) |
290 dict.__init__(self) |
|
291 self._cw_related_cache = {} |
291 self._cw_related_cache = {} |
292 if rset is not None: |
292 if rset is not None: |
293 self.eid = rset[row][col] |
293 self.eid = rset[row][col] |
294 else: |
294 else: |
295 self.eid = None |
295 self.eid = None |
296 self._cw_is_saved = True |
296 self._cw_is_saved = True |
|
297 self.cw_attr_cache = {} |
297 |
298 |
298 def __repr__(self): |
299 def __repr__(self): |
299 return '<Entity %s %s %s at %s>' % ( |
300 return '<Entity %s %s %s at %s>' % ( |
300 self.e_schema, self.eid, self.keys(), id(self)) |
301 self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self)) |
301 |
302 |
302 def __json_encode__(self): |
303 def __json_encode__(self): |
303 """custom json dumps hook to dump the entity's eid |
304 """custom json dumps hook to dump the entity's eid |
304 which is not part of dict structure itself |
305 which is not part of dict structure itself |
305 """ |
306 """ |
313 def __hash__(self): |
314 def __hash__(self): |
314 return id(self) |
315 return id(self) |
315 |
316 |
316 def __cmp__(self, other): |
317 def __cmp__(self, other): |
317 raise NotImplementedError('comparison not implemented for %s' % self.__class__) |
318 raise NotImplementedError('comparison not implemented for %s' % self.__class__) |
|
319 |
|
320 def __contains__(self, key): |
|
321 return key in self.cw_attr_cache |
|
322 |
|
323 def __iter__(self): |
|
324 return iter(self.cw_attr_cache) |
318 |
325 |
319 def __getitem__(self, key): |
326 def __getitem__(self, key): |
320 if key == 'eid': |
327 if key == 'eid': |
321 warn('[3.7] entity["eid"] is deprecated, use entity.eid instead', |
328 warn('[3.7] entity["eid"] is deprecated, use entity.eid instead', |
322 DeprecationWarning, stacklevel=2) |
329 DeprecationWarning, stacklevel=2) |
323 return self.eid |
330 return self.eid |
324 return super(Entity, self).__getitem__(key) |
331 return self.cw_attr_cache[key] |
325 |
332 |
326 def __setitem__(self, attr, value): |
333 def __setitem__(self, attr, value): |
327 """override __setitem__ to update self.edited_attributes. |
334 """override __setitem__ to update self.edited_attributes. |
328 |
335 |
329 Typically, a before_[update|add]_hook could do:: |
336 Typically, a before_[update|add]_hook could do:: |
337 if attr == 'eid': |
344 if attr == 'eid': |
338 warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead', |
345 warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead', |
339 DeprecationWarning, stacklevel=2) |
346 DeprecationWarning, stacklevel=2) |
340 self.eid = value |
347 self.eid = value |
341 else: |
348 else: |
342 super(Entity, self).__setitem__(attr, value) |
349 self.cw_attr_cache[attr] = value |
343 # don't add attribute into skip_security if already in edited |
350 # don't add attribute into skip_security if already in edited |
344 # attributes, else we may accidentaly skip a desired security check |
351 # attributes, else we may accidentaly skip a desired security check |
345 if hasattr(self, 'edited_attributes') and \ |
352 if hasattr(self, 'edited_attributes') and \ |
346 attr not in self.edited_attributes: |
353 attr not in self.edited_attributes: |
347 self.edited_attributes.add(attr) |
354 self.edited_attributes.add(attr) |
361 elif not has_load_left and edited: |
368 elif not has_load_left and edited: |
362 # cleanup, this may cause undesired changes |
369 # cleanup, this may cause undesired changes |
363 del self.entity['load_left'] |
370 del self.entity['load_left'] |
364 |
371 |
365 """ |
372 """ |
366 super(Entity, self).__delitem__(attr) |
373 del self.cw_attr_cache[attr] |
367 if hasattr(self, 'edited_attributes'): |
374 if hasattr(self, 'edited_attributes'): |
368 self.edited_attributes.remove(attr) |
375 self.edited_attributes.remove(attr) |
369 |
376 |
|
377 def get(self, key, default=None): |
|
378 return self.cw_attr_cache.get(key, default) |
|
379 |
370 def setdefault(self, attr, default): |
380 def setdefault(self, attr, default): |
371 """override setdefault to update self.edited_attributes""" |
381 """override setdefault to update self.edited_attributes""" |
372 super(Entity, self).setdefault(attr, default) |
382 self.cw_attr_cache.setdefault(attr, default) |
373 # don't add attribute into skip_security if already in edited |
383 # don't add attribute into skip_security if already in edited |
374 # attributes, else we may accidentaly skip a desired security check |
384 # attributes, else we may accidentaly skip a desired security check |
375 if hasattr(self, 'edited_attributes') and \ |
385 if hasattr(self, 'edited_attributes') and \ |
376 attr not in self.edited_attributes: |
386 attr not in self.edited_attributes: |
377 self.edited_attributes.add(attr) |
387 self.edited_attributes.add(attr) |
380 def pop(self, attr, default=_marker): |
390 def pop(self, attr, default=_marker): |
381 """override pop to update self.edited_attributes on cleanup of |
391 """override pop to update self.edited_attributes on cleanup of |
382 undesired changes introduced in the entity's dict. See `__delitem__` |
392 undesired changes introduced in the entity's dict. See `__delitem__` |
383 """ |
393 """ |
384 if default is _marker: |
394 if default is _marker: |
385 value = super(Entity, self).pop(attr) |
395 value = self.cw_attr_cache.pop(attr) |
386 else: |
396 else: |
387 value = super(Entity, self).pop(attr, default) |
397 value = self.cw_attr_cache.pop(attr, default) |
388 if hasattr(self, 'edited_attributes') and attr in self.edited_attributes: |
398 if hasattr(self, 'edited_attributes') and attr in self.edited_attributes: |
389 self.edited_attributes.remove(attr) |
399 self.edited_attributes.remove(attr) |
390 return value |
400 return value |
391 |
401 |
392 def update(self, values): |
402 def update(self, values): |
554 data = soup2xhtml(data, self._cw.encoding) |
564 data = soup2xhtml(data, self._cw.encoding) |
555 return data |
565 return data |
556 |
566 |
557 # entity cloning ########################################################## |
567 # entity cloning ########################################################## |
558 |
568 |
|
569 def cw_copy(self): |
|
570 thecopy = copy(self) |
|
571 thecopy.cw_attr_cache = copy(self.cw_attr_cache) |
|
572 thecopy._cw_related_cache = {} |
|
573 return thecopy |
|
574 |
559 def copy_relations(self, ceid): # XXX cw_copy_relations |
575 def copy_relations(self, ceid): # XXX cw_copy_relations |
560 """copy relations of the object with the given eid on this |
576 """copy relations of the object with the given eid on this |
561 object (this method is called on the newly created copy, and |
577 object (this method is called on the newly created copy, and |
562 ceid designates the original entity). |
578 ceid designates the original entity). |
563 |
579 |
666 V = varmaker.next() |
682 V = varmaker.next() |
667 rql = ['WHERE %s eid %%(x)s' % V] |
683 rql = ['WHERE %s eid %%(x)s' % V] |
668 selected = [] |
684 selected = [] |
669 for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)): |
685 for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)): |
670 # if attribute already in entity, nothing to do |
686 # if attribute already in entity, nothing to do |
671 if self.has_key(attr): |
687 if self.cw_attr_cache.has_key(attr): |
672 continue |
688 continue |
673 # case where attribute must be completed, but is not yet in entity |
689 # case where attribute must be completed, but is not yet in entity |
674 var = varmaker.next() |
690 var = varmaker.next() |
675 rql.append('%s %s %s' % (V, attr, var)) |
691 rql.append('%s %s %s' % (V, attr, var)) |
676 selected.append((attr, var)) |
692 selected.append((attr, var)) |
950 you should override this method to clear them as well. |
966 you should override this method to clear them as well. |
951 """ |
967 """ |
952 # clear attributes cache |
968 # clear attributes cache |
953 haseid = 'eid' in self |
969 haseid = 'eid' in self |
954 self._cw_completed = False |
970 self._cw_completed = False |
955 self.clear() |
971 self.cw_attr_cache.clear() |
956 # clear relations cache |
972 # clear relations cache |
957 self.cw_clear_relation_cache() |
973 self.cw_clear_relation_cache() |
958 # rest path unique cache |
974 # rest path unique cache |
959 try: |
975 try: |
960 del self.__unique |
976 del self.__unique |
1018 don't use dict api in such case since we don't want attribute to be |
1034 don't use dict api in such case since we don't want attribute to be |
1019 added to skip_security_attributes. |
1035 added to skip_security_attributes. |
1020 |
1036 |
1021 This method is for internal use, you should not use it. |
1037 This method is for internal use, you should not use it. |
1022 """ |
1038 """ |
1023 super(Entity, self).__setitem__(attr, value) |
1039 self.cw_attr_cache[attr] = value |
1024 |
1040 |
1025 def _cw_clear_local_perm_cache(self, action): |
1041 def _cw_clear_local_perm_cache(self, action): |
1026 for rqlexpr in self.e_schema.get_rqlexprs(action): |
1042 for rqlexpr in self.e_schema.get_rqlexprs(action): |
1027 self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None) |
1043 self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None) |
1028 |
1044 |
1035 return self.__cw_skip_security_attributes |
1051 return self.__cw_skip_security_attributes |
1036 |
1052 |
1037 def _cw_set_defaults(self): |
1053 def _cw_set_defaults(self): |
1038 """set default values according to the schema""" |
1054 """set default values according to the schema""" |
1039 for attr, value in self.e_schema.defaults(): |
1055 for attr, value in self.e_schema.defaults(): |
1040 if not self.has_key(attr): |
1056 if not self.cw_attr_cache.has_key(attr): |
1041 self[str(attr)] = value |
1057 self[str(attr)] = value |
1042 |
1058 |
1043 def _cw_check(self, creation=False): |
1059 def _cw_check(self, creation=False): |
1044 """check this entity against its schema. Only final relation |
1060 """check this entity against its schema. Only final relation |
1045 are checked here, constraint on actual relations are checked in hooks |
1061 are checked here, constraint on actual relations are checked in hooks |