entity.py
changeset 6142 8bc6eac1fac1
parent 6124 c5900230809b
child 6279 42079f752a9c
equal deleted inserted replaced
6141:b8287e54b528 6142:8bc6eac1fac1
    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
       
    23 from warnings import warn
    22 from warnings import warn
    24 
    23 
    25 from logilab.common import interface
    24 from logilab.common import interface
    26 from logilab.common.decorators import cached
    25 from logilab.common.decorators import cached
    27 from logilab.common.deprecation import deprecated
    26 from logilab.common.deprecation import deprecated
   310 
   309 
   311     def __repr__(self):
   310     def __repr__(self):
   312         return '<Entity %s %s %s at %s>' % (
   311         return '<Entity %s %s %s at %s>' % (
   313             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
   312             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
   314 
   313 
       
   314     def __cmp__(self, other):
       
   315         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
       
   316 
   315     def __json_encode__(self):
   317     def __json_encode__(self):
   316         """custom json dumps hook to dump the entity's eid
   318         """custom json dumps hook to dump the entity's eid
   317         which is not part of dict structure itself
   319         which is not part of dict structure itself
   318         """
   320         """
   319         dumpable = dict(self)
   321         dumpable = dict(self)
   320         dumpable['eid'] = self.eid
   322         dumpable['eid'] = self.eid
   321         return dumpable
   323         return dumpable
   322 
       
   323     def __nonzero__(self):
       
   324         return True
       
   325 
       
   326     def __hash__(self):
       
   327         return id(self)
       
   328 
       
   329     def __cmp__(self, other):
       
   330         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
       
   331 
       
   332     def __contains__(self, key):
       
   333         return key in self.cw_attr_cache
       
   334 
       
   335     def __iter__(self):
       
   336         return iter(self.cw_attr_cache)
       
   337 
       
   338     def __getitem__(self, key):
       
   339         if key == 'eid':
       
   340             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
       
   341                  DeprecationWarning, stacklevel=2)
       
   342             return self.eid
       
   343         return self.cw_attr_cache[key]
       
   344 
       
   345     def __setitem__(self, attr, value):
       
   346         """override __setitem__ to update self.edited_attributes.
       
   347 
       
   348         Typically, a before_[update|add]_hook could do::
       
   349 
       
   350             entity['generated_attr'] = generated_value
       
   351 
       
   352         and this way, edited_attributes will be updated accordingly. Also, add
       
   353         the attribute to skip_security since we don't want to check security
       
   354         for such attributes set by hooks.
       
   355         """
       
   356         if attr == 'eid':
       
   357             warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
       
   358                  DeprecationWarning, stacklevel=2)
       
   359             self.eid = value
       
   360         else:
       
   361             self.cw_attr_cache[attr] = value
       
   362             # don't add attribute into skip_security if already in edited
       
   363             # attributes, else we may accidentaly skip a desired security check
       
   364             if hasattr(self, 'edited_attributes') and \
       
   365                    attr not in self.edited_attributes:
       
   366                 self.edited_attributes.add(attr)
       
   367                 self._cw_skip_security_attributes.add(attr)
       
   368 
       
   369     def __delitem__(self, attr):
       
   370         """override __delitem__ to update self.edited_attributes on cleanup of
       
   371         undesired changes introduced in the entity's dict. For example, see the
       
   372         code snippet below from the `forge` cube:
       
   373 
       
   374         .. sourcecode:: python
       
   375 
       
   376             edited = self.entity.edited_attributes
       
   377             has_load_left = 'load_left' in edited
       
   378             if 'load' in edited and self.entity.load_left is None:
       
   379                 self.entity.load_left = self.entity['load']
       
   380             elif not has_load_left and edited:
       
   381                 # cleanup, this may cause undesired changes
       
   382                 del self.entity['load_left']
       
   383 
       
   384         """
       
   385         del self.cw_attr_cache[attr]
       
   386         if hasattr(self, 'edited_attributes'):
       
   387             self.edited_attributes.remove(attr)
       
   388 
       
   389     def clear(self):
       
   390         self.cw_attr_cache.clear()
       
   391 
       
   392     def get(self, key, default=None):
       
   393         return self.cw_attr_cache.get(key, default)
       
   394 
       
   395     def setdefault(self, attr, default):
       
   396         """override setdefault to update self.edited_attributes"""
       
   397         value = self.cw_attr_cache.setdefault(attr, default)
       
   398         # don't add attribute into skip_security if already in edited
       
   399         # attributes, else we may accidentaly skip a desired security check
       
   400         if hasattr(self, 'edited_attributes') and \
       
   401                attr not in self.edited_attributes:
       
   402             self.edited_attributes.add(attr)
       
   403             self._cw_skip_security_attributes.add(attr)
       
   404         return value
       
   405 
       
   406     def pop(self, attr, default=_marker):
       
   407         """override pop to update self.edited_attributes on cleanup of
       
   408         undesired changes introduced in the entity's dict. See `__delitem__`
       
   409         """
       
   410         if default is _marker:
       
   411             value = self.cw_attr_cache.pop(attr)
       
   412         else:
       
   413             value = self.cw_attr_cache.pop(attr, default)
       
   414         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
       
   415             self.edited_attributes.remove(attr)
       
   416         return value
       
   417 
       
   418     def update(self, values):
       
   419         """override update to update self.edited_attributes. See `__setitem__`
       
   420         """
       
   421         for attr, value in values.items():
       
   422             self[attr] = value # use self.__setitem__ implementation
       
   423 
   324 
   424     def cw_adapt_to(self, interface):
   325     def cw_adapt_to(self, interface):
   425         """return an adapter the entity to the given interface name.
   326         """return an adapter the entity to the given interface name.
   426 
   327 
   427         return None if it can not be adapted.
   328         return None if it can not be adapted.
   588             data = soup2xhtml(data, self._cw.encoding)
   489             data = soup2xhtml(data, self._cw.encoding)
   589         return data
   490         return data
   590 
   491 
   591     # entity cloning ##########################################################
   492     # entity cloning ##########################################################
   592 
   493 
   593     def cw_copy(self):
       
   594         thecopy = copy(self)
       
   595         thecopy.cw_attr_cache = copy(self.cw_attr_cache)
       
   596         thecopy._cw_related_cache = {}
       
   597         return thecopy
       
   598 
       
   599     def copy_relations(self, ceid): # XXX cw_copy_relations
   494     def copy_relations(self, ceid): # XXX cw_copy_relations
   600         """copy relations of the object with the given eid on this
   495         """copy relations of the object with the given eid on this
   601         object (this method is called on the newly created copy, and
   496         object (this method is called on the newly created copy, and
   602         ceid designates the original entity).
   497         ceid designates the original entity).
   603 
   498 
   678                 continue
   573                 continue
   679             # password retreival is blocked at the repository server level
   574             # password retreival is blocked at the repository server level
   680             rdef = rschema.rdef(self.e_schema, attrschema)
   575             rdef = rschema.rdef(self.e_schema, attrschema)
   681             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
   576             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
   682                    or (attrschema.type == 'Password' and skip_pwd):
   577                    or (attrschema.type == 'Password' and skip_pwd):
   683                 self[attr] = None
   578                 self.cw_attr_cache[attr] = None
   684                 continue
   579                 continue
   685             yield attr
   580             yield attr
   686 
   581 
   687     _cw_completed = False
   582     _cw_completed = False
   688     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
   583     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
   737             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   632             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   738                                     ','.join(rql))
   633                                     ','.join(rql))
   739             rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
   634             rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
   740             # handle attributes
   635             # handle attributes
   741             for i in xrange(1, lastattr):
   636             for i in xrange(1, lastattr):
   742                 self[str(selected[i-1][0])] = rset[i]
   637                 self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
   743             # handle relations
   638             # handle relations
   744             for i in xrange(lastattr, len(rset)):
   639             for i in xrange(lastattr, len(rset)):
   745                 rtype, role = selected[i-1][0]
   640                 rtype, role = selected[i-1][0]
   746                 value = rset[i]
   641                 value = rset[i]
   747                 if value is None:
   642                 if value is None:
   757 
   652 
   758         :type name: str
   653         :type name: str
   759         :param name: name of the attribute to get
   654         :param name: name of the attribute to get
   760         """
   655         """
   761         try:
   656         try:
   762             value = self.cw_attr_cache[name]
   657             return self.cw_attr_cache[name]
   763         except KeyError:
   658         except KeyError:
   764             if not self.cw_is_saved():
   659             if not self.cw_is_saved():
   765                 return None
   660                 return None
   766             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   661             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   767             try:
   662             try:
   768                 rset = self._cw.execute(rql, {'x': self.eid})
   663                 rset = self._cw.execute(rql, {'x': self.eid})
   769             except Unauthorized:
   664             except Unauthorized:
   770                 self[name] = value = None
   665                 self.cw_attr_cache[name] = value = None
   771             else:
   666             else:
   772                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
   667                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
   773                 try:
   668                 try:
   774                     self[name] = value = rset.rows[0][0]
   669                     self.cw_attr_cache[name] = value = rset.rows[0][0]
   775                 except IndexError:
   670                 except IndexError:
   776                     # probably a multisource error
   671                     # probably a multisource error
   777                     self.critical("can't get value for attribute %s of entity with eid %s",
   672                     self.critical("can't get value for attribute %s of entity with eid %s",
   778                                   name, self.eid)
   673                                   name, self.eid)
   779                     if self.e_schema.destination(name) == 'String':
   674                     if self.e_schema.destination(name) == 'String':
   780                         # XXX (syt) imo emtpy string is better
   675                         self.cw_attr_cache[name] = value = self._cw._('unaccessible')
   781                         self[name] = value = self._cw._('unaccessible')
       
   782                     else:
   676                     else:
   783                         self[name] = value = None
   677                         self.cw_attr_cache[name] = value = None
   784         return value
   678             return value
   785 
   679 
   786     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
   680     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
   787         """returns a resultset of related entities
   681         """returns a resultset of related entities
   788 
   682 
   789         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   683         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   983 
   877 
   984         If you use custom caches on your entity class (take care to @cached!),
   878         If you use custom caches on your entity class (take care to @cached!),
   985         you should override this method to clear them as well.
   879         you should override this method to clear them as well.
   986         """
   880         """
   987         # clear attributes cache
   881         # clear attributes cache
   988         haseid = 'eid' in self
       
   989         self._cw_completed = False
   882         self._cw_completed = False
   990         self.cw_attr_cache.clear()
   883         self.cw_attr_cache.clear()
   991         # clear relations cache
   884         # clear relations cache
   992         self.cw_clear_relation_cache()
   885         self.cw_clear_relation_cache()
   993         # rest path unique cache
   886         # rest path unique cache
  1010         kwargs['x'] = self.eid
   903         kwargs['x'] = self.eid
  1011         self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
   904         self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
  1012                          kwargs)
   905                          kwargs)
  1013         kwargs.pop('x')
   906         kwargs.pop('x')
  1014         # update current local object _after_ the rql query to avoid
   907         # update current local object _after_ the rql query to avoid
  1015         # interferences between the query execution itself and the
   908         # interferences between the query execution itself and the cw_edited /
  1016         # edited_attributes / skip_security_attributes machinery
   909         # skip_security machinery
  1017         self.update(kwargs)
   910         self.cw_attr_cache.update(kwargs)
  1018 
   911 
  1019     def set_relations(self, **kwargs): # XXX cw_set_relations
   912     def set_relations(self, **kwargs): # XXX cw_set_relations
  1020         """add relations to the given object. To set a relation where this entity
   913         """add relations to the given object. To set a relation where this entity
  1021         is the object of the relation, use 'reverse_'<relation> as argument name.
   914         is the object of the relation, use 'reverse_'<relation> as argument name.
  1022 
   915 
  1043     def cw_delete(self, **kwargs):
   936     def cw_delete(self, **kwargs):
  1044         assert self.has_eid(), self.eid
   937         assert self.has_eid(), self.eid
  1045         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
   938         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
  1046                          {'x': self.eid}, **kwargs)
   939                          {'x': self.eid}, **kwargs)
  1047 
   940 
  1048     # server side utilities ###################################################
   941     # server side utilities ####################################################
  1049 
       
  1050     def _cw_rql_set_value(self, attr, value):
       
  1051         """call by rql execution plan when some attribute is modified
       
  1052 
       
  1053         don't use dict api in such case since we don't want attribute to be
       
  1054         added to skip_security_attributes.
       
  1055 
       
  1056         This method is for internal use, you should not use it.
       
  1057         """
       
  1058         self.cw_attr_cache[attr] = value
       
  1059 
   942 
  1060     def _cw_clear_local_perm_cache(self, action):
   943     def _cw_clear_local_perm_cache(self, action):
  1061         for rqlexpr in self.e_schema.get_rqlexprs(action):
   944         for rqlexpr in self.e_schema.get_rqlexprs(action):
  1062             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
   945             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
  1063 
   946 
  1064     @property
   947     # deprecated stuff #########################################################
  1065     def _cw_skip_security_attributes(self):
       
  1066         try:
       
  1067             return self.__cw_skip_security_attributes
       
  1068         except:
       
  1069             self.__cw_skip_security_attributes = set()
       
  1070             return self.__cw_skip_security_attributes
       
  1071 
       
  1072     def _cw_set_defaults(self):
       
  1073         """set default values according to the schema"""
       
  1074         for attr, value in self.e_schema.defaults():
       
  1075             if not self.cw_attr_cache.has_key(attr):
       
  1076                 self[str(attr)] = value
       
  1077 
       
  1078     def _cw_check(self, creation=False):
       
  1079         """check this entity against its schema. Only final relation
       
  1080         are checked here, constraint on actual relations are checked in hooks
       
  1081         """
       
  1082         # necessary since eid is handled specifically and yams require it to be
       
  1083         # in the dictionary
       
  1084         if self._cw is None:
       
  1085             _ = unicode
       
  1086         else:
       
  1087             _ = self._cw._
       
  1088         if creation:
       
  1089             # on creations, we want to check all relations, especially
       
  1090             # required attributes
       
  1091             relations = [rschema for rschema in self.e_schema.subject_relations()
       
  1092                          if rschema.final and rschema.type != 'eid']
       
  1093         elif hasattr(self, 'edited_attributes'):
       
  1094             relations = [self._cw.vreg.schema.rschema(rtype)
       
  1095                          for rtype in self.edited_attributes]
       
  1096         else:
       
  1097             relations = None
       
  1098         self.e_schema.check(self, creation=creation, _=_,
       
  1099                             relations=relations)
       
  1100 
   948 
  1101     @deprecated('[3.9] use entity.cw_attr_value(attr)')
   949     @deprecated('[3.9] use entity.cw_attr_value(attr)')
  1102     def get_value(self, name):
   950     def get_value(self, name):
  1103         return self.cw_attr_value(name)
   951         return self.cw_attr_value(name)
  1104 
   952 
  1123         self.cw_clear_relation_cache(rtype, role)
   971         self.cw_clear_relation_cache(rtype, role)
  1124 
   972 
  1125     @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
   973     @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
  1126     def related_rql(self, rtype, role='subject', targettypes=None):
   974     def related_rql(self, rtype, role='subject', targettypes=None):
  1127         return self.cw_related_rql(rtype, role, targettypes)
   975         return self.cw_related_rql(rtype, role, targettypes)
       
   976 
       
   977     @property
       
   978     @deprecated('[3.10] use entity.cw_edited')
       
   979     def edited_attributes(self):
       
   980         return self.cw_edited
       
   981 
       
   982     @property
       
   983     @deprecated('[3.10] use entity.cw_edited.skip_security')
       
   984     def skip_security_attributes(self):
       
   985         return self.cw_edited.skip_security
       
   986 
       
   987     @property
       
   988     @deprecated('[3.10] use entity.cw_edited.skip_security')
       
   989     def _cw_skip_security_attributes(self):
       
   990         return self.cw_edited.skip_security
       
   991 
       
   992     @property
       
   993     @deprecated('[3.10] use entity.cw_edited.skip_security')
       
   994     def querier_pending_relations(self):
       
   995         return self.cw_edited.querier_pending_relations
       
   996 
       
   997     @deprecated('[3.10] use key in entity.cw_attr_cache')
       
   998     def __contains__(self, key):
       
   999         return key in self.cw_attr_cache
       
  1000 
       
  1001     @deprecated('[3.10] iter on entity.cw_attr_cache')
       
  1002     def __iter__(self):
       
  1003         return iter(self.cw_attr_cache)
       
  1004 
       
  1005     @deprecated('[3.10] use entity.cw_attr_cache[attr]')
       
  1006     def __getitem__(self, key):
       
  1007         if key == 'eid':
       
  1008             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
       
  1009                  DeprecationWarning, stacklevel=2)
       
  1010             return self.eid
       
  1011         return self.cw_attr_cache[key]
       
  1012 
       
  1013     @deprecated('[3.10] use entity.cw_attr_cache.get(attr[, default])')
       
  1014     def get(self, key, default=None):
       
  1015         return self.cw_attr_cache.get(key, default)
       
  1016 
       
  1017     @deprecated('[3.10] use entity.cw_attr_cache.clear()')
       
  1018     def clear(self):
       
  1019         self.cw_attr_cache.clear()
       
  1020         # XXX clear cw_edited ?
       
  1021 
       
  1022     @deprecated('[3.10] use entity.cw_edited[attr] = value or entity.cw_attr_cache[attr] = value')
       
  1023     def __setitem__(self, attr, value):
       
  1024         """override __setitem__ to update self.cw_edited.
       
  1025 
       
  1026         Typically, a before_[update|add]_hook could do::
       
  1027 
       
  1028             entity['generated_attr'] = generated_value
       
  1029 
       
  1030         and this way, cw_edited will be updated accordingly. Also, add
       
  1031         the attribute to skip_security since we don't want to check security
       
  1032         for such attributes set by hooks.
       
  1033         """
       
  1034         if attr == 'eid':
       
  1035             warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
       
  1036                  DeprecationWarning, stacklevel=2)
       
  1037             self.eid = value
       
  1038         else:
       
  1039             try:
       
  1040                 self.cw_edited[attr] = value
       
  1041             except AttributeError:
       
  1042                 self.cw_attr_cache[attr] = value
       
  1043 
       
  1044     @deprecated('[3.10] use del entity.cw_edited[attr]')
       
  1045     def __delitem__(self, attr):
       
  1046         """override __delitem__ to update self.cw_edited on cleanup of
       
  1047         undesired changes introduced in the entity's dict. For example, see the
       
  1048         code snippet below from the `forge` cube:
       
  1049 
       
  1050         .. sourcecode:: python
       
  1051 
       
  1052             edited = self.entity.cw_edited
       
  1053             has_load_left = 'load_left' in edited
       
  1054             if 'load' in edited and self.entity.load_left is None:
       
  1055                 self.entity.load_left = self.entity['load']
       
  1056             elif not has_load_left and edited:
       
  1057                 # cleanup, this may cause undesired changes
       
  1058                 del self.entity['load_left']
       
  1059         """
       
  1060         del self.cw_edited[attr]
       
  1061 
       
  1062     @deprecated('[3.10] use entity.cw_edited.setdefault(attr, default)')
       
  1063     def setdefault(self, attr, default):
       
  1064         """override setdefault to update self.cw_edited"""
       
  1065         return self.cw_edited.setdefault(attr, default)
       
  1066 
       
  1067     @deprecated('[3.10] use entity.cw_edited.pop(attr[, default])')
       
  1068     def pop(self, attr, *args):
       
  1069         """override pop to update self.cw_edited on cleanup of
       
  1070         undesired changes introduced in the entity's dict. See `__delitem__`
       
  1071         """
       
  1072         return self.cw_edited.pop(attr, *args)
       
  1073 
       
  1074     @deprecated('[3.10] use entity.cw_edited.update(values)')
       
  1075     def update(self, values):
       
  1076         """override update to update self.cw_edited. See `__setitem__`
       
  1077         """
       
  1078         self.cw_edited.update(values)
  1128 
  1079 
  1129 
  1080 
  1130 # attribute and relation descriptors ##########################################
  1081 # attribute and relation descriptors ##########################################
  1131 
  1082 
  1132 class Attribute(object):
  1083 class Attribute(object):
  1139     def __get__(self, eobj, eclass):
  1090     def __get__(self, eobj, eclass):
  1140         if eobj is None:
  1091         if eobj is None:
  1141             return self
  1092             return self
  1142         return eobj.cw_attr_value(self._attrname)
  1093         return eobj.cw_attr_value(self._attrname)
  1143 
  1094 
       
  1095     @deprecated('[3.10] use entity.cw_attr_cache[attr] = value')
  1144     def __set__(self, eobj, value):
  1096     def __set__(self, eobj, value):
  1145         eobj[self._attrname] = value
  1097         eobj.cw_attr_cache[self._attrname] = value
  1146 
  1098 
  1147 
  1099 
  1148 class Relation(object):
  1100 class Relation(object):
  1149     """descriptor that controls schema relation access"""
  1101     """descriptor that controls schema relation access"""
  1150 
  1102