entity.py
changeset 5726 c3b99606644d
parent 5573 8dfb1726526b
child 5728 12d3da7b3bcf
equal deleted inserted replaced
5725:b5d595b66c35 5726:c3b99606644d
    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))
   725 
   741 
   726         :type name: str
   742         :type name: str
   727         :param name: name of the attribute to get
   743         :param name: name of the attribute to get
   728         """
   744         """
   729         try:
   745         try:
   730             value = self[name]
   746             value = self.cw_attr_cache[name]
   731         except KeyError:
   747         except KeyError:
   732             if not self.cw_is_saved():
   748             if not self.cw_is_saved():
   733                 return None
   749                 return None
   734             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   750             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   735             try:
   751             try:
   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