entity.py
branchstable
changeset 5994 97c55baefa0c
parent 5980 6dc04e75c8e1
child 6085 8a059eefac75
equal deleted inserted replaced
5976:00b1b6b906cf 5994:97c55baefa0c
    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.compat import all
       
    26 from logilab.common.decorators import cached
    26 from logilab.common.decorators import cached
       
    27 from logilab.common.deprecation import deprecated
    27 from logilab.mtconverter import TransformData, TransformError, xml_escape
    28 from logilab.mtconverter import TransformData, TransformError, xml_escape
    28 
    29 
    29 from rql.utils import rqlvar_maker
    30 from rql.utils import rqlvar_maker
    30 
    31 
    31 from cubicweb import Unauthorized, typed_eid
    32 from cubicweb import Unauthorized, typed_eid
    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
   104                     mixins.append(mixin)
   105                     mixins.append(mixin)
   105                 for iface in getattr(mixin, '__implements__', ()):
   106                 for iface in getattr(mixin, '__implements__', ()):
   106                     if not interface.implements(cls, iface):
   107                     if not interface.implements(cls, iface):
   107                         interface.extend(cls, iface)
   108                         interface.extend(cls, iface)
   108             if role == 'subject':
   109             if role == 'subject':
   109                 setattr(cls, rschema.type, SubjectRelation(rschema))
   110                 attr = rschema.type
   110             else:
   111             else:
   111                 attr = 'reverse_%s' % rschema.type
   112                 attr = 'reverse_%s' % rschema.type
   112                 setattr(cls, attr, ObjectRelation(rschema))
   113             setattr(cls, attr, Relation(rschema, role))
   113         if mixins:
   114         if mixins:
   114             # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
   115             # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
   115             # due to class dumping, cls is the generated top level class with actual
   116             # due to class dumping, cls is the generated top level class with actual
   116             # user class as (only) parent. Since we want to be able to override mixins
   117             # user class as (only) parent. Since we want to be able to override mixins
   117             # method from this user class, we have to take care to insert mixins after that
   118             # method from this user class, we have to take care to insert mixins after that
   121             # with some cases of entity classes inheritance.
   122             # with some cases of entity classes inheritance.
   122             mixins.insert(0, cls.__bases__[0])
   123             mixins.insert(0, cls.__bases__[0])
   123             mixins += cls.__bases__[1:]
   124             mixins += cls.__bases__[1:]
   124             cls.__bases__ = tuple(mixins)
   125             cls.__bases__ = tuple(mixins)
   125             cls.info('plugged %s mixins on %s', mixins, cls)
   126             cls.info('plugged %s mixins on %s', mixins, cls)
       
   127 
       
   128     fetch_attrs = ('modification_date',)
       
   129     @classmethod
       
   130     def fetch_order(cls, attr, var):
       
   131         """class method used to control sort order when multiple entities of
       
   132         this type are fetched
       
   133         """
       
   134         return cls.fetch_unrelated_order(attr, var)
       
   135 
       
   136     @classmethod
       
   137     def fetch_unrelated_order(cls, attr, var):
       
   138         """class method used to control sort order when multiple entities of
       
   139         this type are fetched to use in edition (eg propose them to create a
       
   140         new relation on an edited entity).
       
   141         """
       
   142         if attr == 'modification_date':
       
   143             return '%s DESC' % var
       
   144         return None
   126 
   145 
   127     @classmethod
   146     @classmethod
   128     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   147     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   129                   settype=True, ordermethod='fetch_order'):
   148                   settype=True, ordermethod='fetch_order'):
   130         """return a rql to fetch all entities of the class type"""
   149         """return a rql to fetch all entities of the class type"""
   267                     {'x': created.eid}, build_descr=False)
   286                     {'x': created.eid}, build_descr=False)
   268         return created
   287         return created
   269 
   288 
   270     def __init__(self, req, rset=None, row=None, col=0):
   289     def __init__(self, req, rset=None, row=None, col=0):
   271         AppObject.__init__(self, req, rset=rset, row=row, col=col)
   290         AppObject.__init__(self, req, rset=rset, row=row, col=col)
   272         dict.__init__(self)
   291         self._cw_related_cache = {}
   273         self._related_cache = {}
       
   274         if rset is not None:
   292         if rset is not None:
   275             self.eid = rset[row][col]
   293             self.eid = rset[row][col]
   276         else:
   294         else:
   277             self.eid = None
   295             self.eid = None
   278         self._is_saved = True
   296         self._cw_is_saved = True
       
   297         self.cw_attr_cache = {}
   279 
   298 
   280     def __repr__(self):
   299     def __repr__(self):
   281         return '<Entity %s %s %s at %s>' % (
   300         return '<Entity %s %s %s at %s>' % (
   282             self.e_schema, self.eid, self.keys(), id(self))
   301             self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
   283 
   302 
   284     def __json_encode__(self):
   303     def __json_encode__(self):
   285         """custom json dumps hook to dump the entity's eid
   304         """custom json dumps hook to dump the entity's eid
   286         which is not part of dict structure itself
   305         which is not part of dict structure itself
   287         """
   306         """
   295     def __hash__(self):
   314     def __hash__(self):
   296         return id(self)
   315         return id(self)
   297 
   316 
   298     def __cmp__(self, other):
   317     def __cmp__(self, other):
   299         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)
   300 
   325 
   301     def __getitem__(self, key):
   326     def __getitem__(self, key):
   302         if key == 'eid':
   327         if key == 'eid':
   303             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
   328             warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
   304                  DeprecationWarning, stacklevel=2)
   329                  DeprecationWarning, stacklevel=2)
   305             return self.eid
   330             return self.eid
   306         return super(Entity, self).__getitem__(key)
   331         return self.cw_attr_cache[key]
   307 
   332 
   308     def __setitem__(self, attr, value):
   333     def __setitem__(self, attr, value):
   309         """override __setitem__ to update self.edited_attributes.
   334         """override __setitem__ to update self.edited_attributes.
   310 
   335 
   311         Typically, a before_[update|add]_hook could do::
   336         Typically, a before_[update|add]_hook could do::
   319         if attr == 'eid':
   344         if attr == 'eid':
   320             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',
   321                  DeprecationWarning, stacklevel=2)
   346                  DeprecationWarning, stacklevel=2)
   322             self.eid = value
   347             self.eid = value
   323         else:
   348         else:
   324             super(Entity, self).__setitem__(attr, value)
   349             self.cw_attr_cache[attr] = value
   325             # don't add attribute into skip_security if already in edited
   350             # don't add attribute into skip_security if already in edited
   326             # attributes, else we may accidentaly skip a desired security check
   351             # attributes, else we may accidentaly skip a desired security check
   327             if hasattr(self, 'edited_attributes') and \
   352             if hasattr(self, 'edited_attributes') and \
   328                    attr not in self.edited_attributes:
   353                    attr not in self.edited_attributes:
   329                 self.edited_attributes.add(attr)
   354                 self.edited_attributes.add(attr)
   330                 self.skip_security_attributes.add(attr)
   355                 self._cw_skip_security_attributes.add(attr)
   331 
   356 
   332     def __delitem__(self, attr):
   357     def __delitem__(self, attr):
   333         """override __delitem__ to update self.edited_attributes on cleanup of
   358         """override __delitem__ to update self.edited_attributes on cleanup of
   334         undesired changes introduced in the entity's dict. For example, see the
   359         undesired changes introduced in the entity's dict. For example, see the
   335         code snippet below from the `forge` cube:
   360         code snippet below from the `forge` cube:
   343             elif not has_load_left and edited:
   368             elif not has_load_left and edited:
   344                 # cleanup, this may cause undesired changes
   369                 # cleanup, this may cause undesired changes
   345                 del self.entity['load_left']
   370                 del self.entity['load_left']
   346 
   371 
   347         """
   372         """
   348         super(Entity, self).__delitem__(attr)
   373         del self.cw_attr_cache[attr]
   349         if hasattr(self, 'edited_attributes'):
   374         if hasattr(self, 'edited_attributes'):
   350             self.edited_attributes.remove(attr)
   375             self.edited_attributes.remove(attr)
   351 
   376 
       
   377     def clear(self):
       
   378         self.cw_attr_cache.clear()
       
   379 
       
   380     def get(self, key, default=None):
       
   381         return self.cw_attr_cache.get(key, default)
       
   382 
   352     def setdefault(self, attr, default):
   383     def setdefault(self, attr, default):
   353         """override setdefault to update self.edited_attributes"""
   384         """override setdefault to update self.edited_attributes"""
   354         super(Entity, self).setdefault(attr, default)
   385         value = self.cw_attr_cache.setdefault(attr, default)
   355         # don't add attribute into skip_security if already in edited
   386         # don't add attribute into skip_security if already in edited
   356         # attributes, else we may accidentaly skip a desired security check
   387         # attributes, else we may accidentaly skip a desired security check
   357         if hasattr(self, 'edited_attributes') and \
   388         if hasattr(self, 'edited_attributes') and \
   358                attr not in self.edited_attributes:
   389                attr not in self.edited_attributes:
   359             self.edited_attributes.add(attr)
   390             self.edited_attributes.add(attr)
   360             self.skip_security_attributes.add(attr)
   391             self._cw_skip_security_attributes.add(attr)
       
   392         return value
   361 
   393 
   362     def pop(self, attr, default=_marker):
   394     def pop(self, attr, default=_marker):
   363         """override pop to update self.edited_attributes on cleanup of
   395         """override pop to update self.edited_attributes on cleanup of
   364         undesired changes introduced in the entity's dict. See `__delitem__`
   396         undesired changes introduced in the entity's dict. See `__delitem__`
   365         """
   397         """
   366         if default is _marker:
   398         if default is _marker:
   367             value = super(Entity, self).pop(attr)
   399             value = self.cw_attr_cache.pop(attr)
   368         else:
   400         else:
   369             value = super(Entity, self).pop(attr, default)
   401             value = self.cw_attr_cache.pop(attr, default)
   370         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
   402         if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
   371             self.edited_attributes.remove(attr)
   403             self.edited_attributes.remove(attr)
   372         return value
   404         return value
   373 
   405 
   374     def update(self, values):
   406     def update(self, values):
   375         """override update to update self.edited_attributes. See `__setitem__`
   407         """override update to update self.edited_attributes. See `__setitem__`
   376         """
   408         """
   377         for attr, value in values.items():
   409         for attr, value in values.items():
   378             self[attr] = value # use self.__setitem__ implementation
   410             self[attr] = value # use self.__setitem__ implementation
   379 
   411 
   380     def rql_set_value(self, attr, value):
   412     def cw_adapt_to(self, interface):
   381         """call by rql execution plan when some attribute is modified
   413         """return an adapter the entity to the given interface name.
   382 
   414 
   383         don't use dict api in such case since we don't want attribute to be
   415         return None if it can not be adapted.
   384         added to skip_security_attributes.
   416         """
   385         """
   417         try:
   386         super(Entity, self).__setitem__(attr, value)
   418             cache = self._cw_adapters_cache
   387 
   419         except AttributeError:
   388     def pre_add_hook(self):
   420             self._cw_adapters_cache = cache = {}
   389         """hook called by the repository before doing anything to add the entity
   421         try:
   390         (before_add entity hooks have not been called yet). This give the
   422             return cache[interface]
   391         occasion to do weird stuff such as autocast (File -> Image for instance).
   423         except KeyError:
   392 
   424             adapter = self._cw.vreg['adapters'].select_or_none(
   393         This method must return the actual entity to be added.
   425                 interface, self._cw, entity=self)
   394         """
   426             cache[interface] = adapter
   395         return self
   427             return adapter
   396 
   428 
   397     def set_eid(self, eid):
   429     def has_eid(self): # XXX cw_has_eid
   398         self.eid = eid
       
   399 
       
   400     def has_eid(self):
       
   401         """return True if the entity has an attributed eid (False
   430         """return True if the entity has an attributed eid (False
   402         meaning that the entity has to be created
   431         meaning that the entity has to be created
   403         """
   432         """
   404         try:
   433         try:
   405             typed_eid(self.eid)
   434             typed_eid(self.eid)
   406             return True
   435             return True
   407         except (ValueError, TypeError):
   436         except (ValueError, TypeError):
   408             return False
   437             return False
   409 
   438 
   410     def is_saved(self):
   439     def cw_is_saved(self):
   411         """during entity creation, there is some time during which the entity
   440         """during entity creation, there is some time during which the entity
   412         has an eid attributed though it's not saved (eg during before_add_entity
   441         has an eid attributed though it's not saved (eg during
   413         hooks). You can use this method to ensure the entity has an eid *and* is
   442         'before_add_entity' hooks). You can use this method to ensure the entity
   414         saved in its source.
   443         has an eid *and* is saved in its source.
   415         """
   444         """
   416         return self.has_eid() and self._is_saved
   445         return self.has_eid() and self._cw_is_saved
   417 
   446 
   418     @cached
   447     @cached
   419     def metainformation(self):
   448     def cw_metainformation(self):
   420         res = dict(zip(('type', 'source', 'extid'), self._cw.describe(self.eid)))
   449         res = dict(zip(('type', 'source', 'extid'), self._cw.describe(self.eid)))
   421         res['source'] = self._cw.source_defs()[res['source']]
   450         res['source'] = self._cw.source_defs()[res['source']]
   422         return res
   451         return res
   423 
   452 
   424     def clear_local_perm_cache(self, action):
   453     def cw_check_perm(self, action):
   425         for rqlexpr in self.e_schema.get_rqlexprs(action):
       
   426             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
       
   427 
       
   428     def check_perm(self, action):
       
   429         self.e_schema.check_perm(self._cw, action, eid=self.eid)
   454         self.e_schema.check_perm(self._cw, action, eid=self.eid)
   430 
   455 
   431     def has_perm(self, action):
   456     def cw_has_perm(self, action):
   432         return self.e_schema.has_perm(self._cw, action, eid=self.eid)
   457         return self.e_schema.has_perm(self._cw, action, eid=self.eid)
   433 
   458 
   434     def view(self, __vid, __registry='views', w=None, **kwargs):
   459     def view(self, __vid, __registry='views', w=None, **kwargs): # XXX cw_view
   435         """shortcut to apply a view on this entity"""
   460         """shortcut to apply a view on this entity"""
   436         view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
   461         view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
   437                                                 row=self.cw_row, col=self.cw_col,
   462                                                 row=self.cw_row, col=self.cw_col,
   438                                                 **kwargs)
   463                                                 **kwargs)
   439         return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
   464         return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
   440 
   465 
   441     def absolute_url(self, *args, **kwargs):
   466     def absolute_url(self, *args, **kwargs): # XXX cw_url
   442         """return an absolute url to view this entity"""
   467         """return an absolute url to view this entity"""
   443         # use *args since we don't want first argument to be "anonymous" to
   468         # use *args since we don't want first argument to be "anonymous" to
   444         # avoid potential clash with kwargs
   469         # avoid potential clash with kwargs
   445         if args:
   470         if args:
   446             assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
   471             assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
   449             method = None
   474             method = None
   450         # in linksearch mode, we don't want external urls else selecting
   475         # in linksearch mode, we don't want external urls else selecting
   451         # the object for use in the relation is tricky
   476         # the object for use in the relation is tricky
   452         # XXX search_state is web specific
   477         # XXX search_state is web specific
   453         if getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
   478         if getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
   454             kwargs['base_url'] = self.metainformation()['source'].get('base-url')
   479             kwargs['base_url'] = self.cw_metainformation()['source'].get('base-url')
   455         if method in (None, 'view'):
   480         if method in (None, 'view'):
   456             try:
   481             try:
   457                 kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
   482                 kwargs['_restpath'] = self.rest_path(kwargs.get('base_url'))
   458             except TypeError:
   483             except TypeError:
   459                 warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
   484                 warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
   461                 kwargs['_restpath'] = self.rest_path()
   486                 kwargs['_restpath'] = self.rest_path()
   462         else:
   487         else:
   463             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
   488             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
   464         return self._cw.build_url(method, **kwargs)
   489         return self._cw.build_url(method, **kwargs)
   465 
   490 
   466     def rest_path(self, use_ext_eid=False):
   491     def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
   467         """returns a REST-like (relative) path for this entity"""
   492         """returns a REST-like (relative) path for this entity"""
   468         mainattr, needcheck = self._rest_attr_info()
   493         mainattr, needcheck = self._rest_attr_info()
   469         etype = str(self.e_schema)
   494         etype = str(self.e_schema)
   470         path = etype.lower()
   495         path = etype.lower()
   471         if mainattr != 'eid':
   496         if mainattr != 'eid':
   484                 if nbresults != 1: # ambiguity?
   509                 if nbresults != 1: # ambiguity?
   485                     mainattr = 'eid'
   510                     mainattr = 'eid'
   486                     path += '/eid'
   511                     path += '/eid'
   487         if mainattr == 'eid':
   512         if mainattr == 'eid':
   488             if use_ext_eid:
   513             if use_ext_eid:
   489                 value = self.metainformation()['extid']
   514                 value = self.cw_metainformation()['extid']
   490             else:
   515             else:
   491                 value = self.eid
   516                 value = self.eid
   492         return '%s/%s' % (path, self._cw.url_quote(value))
   517         return '%s/%s' % (path, self._cw.url_quote(value))
   493 
   518 
   494     def attr_metadata(self, attr, metadata):
   519     def cw_attr_metadata(self, attr, metadata):
   495         """return a metadata for an attribute (None if unspecified)"""
   520         """return a metadata for an attribute (None if unspecified)"""
   496         value = getattr(self, '%s_%s' % (attr, metadata), None)
   521         value = getattr(self, '%s_%s' % (attr, metadata), None)
   497         if value is None and metadata == 'encoding':
   522         if value is None and metadata == 'encoding':
   498             value = self._cw.vreg.property_value('ui.encoding')
   523             value = self._cw.vreg.property_value('ui.encoding')
   499         return value
   524         return value
   500 
   525 
   501     def printable_value(self, attr, value=_marker, attrtype=None,
   526     def printable_value(self, attr, value=_marker, attrtype=None,
   502                         format='text/html', displaytime=True):
   527                         format='text/html', displaytime=True): # XXX cw_printable_value
   503         """return a displayable value (i.e. unicode string) which may contains
   528         """return a displayable value (i.e. unicode string) which may contains
   504         html tags
   529         html tags
   505         """
   530         """
   506         attr = str(attr)
   531         attr = str(attr)
   507         if value is _marker:
   532         if value is _marker:
   516         if attrtype == 'String':
   541         if attrtype == 'String':
   517             # internalinalized *and* formatted string such as schema
   542             # internalinalized *and* formatted string such as schema
   518             # description...
   543             # description...
   519             if props.internationalizable:
   544             if props.internationalizable:
   520                 value = self._cw._(value)
   545                 value = self._cw._(value)
   521             attrformat = self.attr_metadata(attr, 'format')
   546             attrformat = self.cw_attr_metadata(attr, 'format')
   522             if attrformat:
   547             if attrformat:
   523                 return self.mtc_transform(value, attrformat, format,
   548                 return self._cw_mtc_transform(value, attrformat, format,
   524                                           self._cw.encoding)
   549                                               self._cw.encoding)
   525         elif attrtype == 'Bytes':
   550         elif attrtype == 'Bytes':
   526             attrformat = self.attr_metadata(attr, 'format')
   551             attrformat = self.cw_attr_metadata(attr, 'format')
   527             if attrformat:
   552             if attrformat:
   528                 encoding = self.attr_metadata(attr, 'encoding')
   553                 encoding = self.cw_attr_metadata(attr, 'encoding')
   529                 return self.mtc_transform(value.getvalue(), attrformat, format,
   554                 return self._cw_mtc_transform(value.getvalue(), attrformat, format,
   530                                           encoding)
   555                                               encoding)
   531             return u''
   556             return u''
   532         value = printable_value(self._cw, attrtype, value, props,
   557         value = printable_value(self._cw, attrtype, value, props,
   533                                 displaytime=displaytime)
   558                                 displaytime=displaytime)
   534         if format == 'text/html':
   559         if format == 'text/html':
   535             value = xml_escape(value)
   560             value = xml_escape(value)
   536         return value
   561         return value
   537 
   562 
   538     def mtc_transform(self, data, format, target_format, encoding,
   563     def _cw_mtc_transform(self, data, format, target_format, encoding,
   539                       _engine=ENGINE):
   564                           _engine=ENGINE):
   540         trdata = TransformData(data, format, encoding, appobject=self)
   565         trdata = TransformData(data, format, encoding, appobject=self)
   541         data = _engine.convert(trdata, target_format).decode()
   566         data = _engine.convert(trdata, target_format).decode()
   542         if format == 'text/html':
   567         if format == 'text/html':
   543             data = soup2xhtml(data, self._cw.encoding)
   568             data = soup2xhtml(data, self._cw.encoding)
   544         return data
   569         return data
   545 
   570 
   546     # entity cloning ##########################################################
   571     # entity cloning ##########################################################
   547 
   572 
   548     def copy_relations(self, ceid):
   573     def cw_copy(self):
       
   574         thecopy = copy(self)
       
   575         thecopy.cw_attr_cache = copy(self.cw_attr_cache)
       
   576         thecopy._cw_related_cache = {}
       
   577         return thecopy
       
   578 
       
   579     def copy_relations(self, ceid): # XXX cw_copy_relations
   549         """copy relations of the object with the given eid on this
   580         """copy relations of the object with the given eid on this
   550         object (this method is called on the newly created copy, and
   581         object (this method is called on the newly created copy, and
   551         ceid designates the original entity).
   582         ceid designates the original entity).
   552 
   583 
   553         By default meta and composite relations are skipped.
   584         By default meta and composite relations are skipped.
   572             if rdef.cardinality[1] in '?1':
   603             if rdef.cardinality[1] in '?1':
   573                 continue
   604                 continue
   574             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
   605             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
   575                 rschema.type, rschema.type)
   606                 rschema.type, rschema.type)
   576             execute(rql, {'x': self.eid, 'y': ceid})
   607             execute(rql, {'x': self.eid, 'y': ceid})
   577             self.clear_related_cache(rschema.type, 'subject')
   608             self.cw_clear_relation_cache(rschema.type, 'subject')
   578         for rschema in self.e_schema.object_relations():
   609         for rschema in self.e_schema.object_relations():
   579             if rschema.meta:
   610             if rschema.meta:
   580                 continue
   611                 continue
   581             # skip already defined relations
   612             # skip already defined relations
   582             if self.related(rschema.type, 'object'):
   613             if self.related(rschema.type, 'object'):
   590             if rdef.cardinality[0] in '?1':
   621             if rdef.cardinality[0] in '?1':
   591                 continue
   622                 continue
   592             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
   623             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
   593                 rschema.type, rschema.type)
   624                 rschema.type, rschema.type)
   594             execute(rql, {'x': self.eid, 'y': ceid})
   625             execute(rql, {'x': self.eid, 'y': ceid})
   595             self.clear_related_cache(rschema.type, 'object')
   626             self.cw_clear_relation_cache(rschema.type, 'object')
   596 
   627 
   597     # data fetching methods ###################################################
   628     # data fetching methods ###################################################
   598 
   629 
   599     @cached
   630     @cached
   600     def as_rset(self):
   631     def as_rset(self): # XXX .cw_as_rset
   601         """returns a resultset containing `self` information"""
   632         """returns a resultset containing `self` information"""
   602         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
   633         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
   603                          {'x': self.eid}, [(self.__regid__,)])
   634                          {'x': self.eid}, [(self.__regid__,)])
   604         rset.req = self._cw
   635         rset.req = self._cw
   605         return rset
   636         return rset
   606 
   637 
   607     def to_complete_relations(self):
   638     def _cw_to_complete_relations(self):
   608         """by default complete final relations to when calling .complete()"""
   639         """by default complete final relations to when calling .complete()"""
   609         for rschema in self.e_schema.subject_relations():
   640         for rschema in self.e_schema.subject_relations():
   610             if rschema.final:
   641             if rschema.final:
   611                 continue
   642                 continue
   612             targets = rschema.objects(self.e_schema)
   643             targets = rschema.objects(self.e_schema)
   613             if len(targets) > 1:
       
   614                 # ambigous relations, the querier doesn't handle
       
   615                 # outer join correctly in this case
       
   616                 continue
       
   617             if rschema.inlined:
   644             if rschema.inlined:
   618                 matching_groups = self._cw.user.matching_groups
   645                 matching_groups = self._cw.user.matching_groups
   619                 rdef = rschema.rdef(self.e_schema, targets[0])
   646                 if all(matching_groups(e.get_groups('read')) and
   620                 if matching_groups(rdef.get_groups('read')) and \
   647                        rschema.rdef(self.e_schema, e).get_groups('read')
   621                    all(matching_groups(e.get_groups('read')) for e in targets):
   648                        for e in targets):
   622                     yield rschema, 'subject'
   649                     yield rschema, 'subject'
   623 
   650 
   624     def to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
   651     def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
   625         for rschema, attrschema in self.e_schema.attribute_definitions():
   652         for rschema, attrschema in self.e_schema.attribute_definitions():
   626             # skip binary data by default
   653             # skip binary data by default
   627             if skip_bytes and attrschema.type == 'Bytes':
   654             if skip_bytes and attrschema.type == 'Bytes':
   628                 continue
   655                 continue
   629             attr = rschema.type
   656             attr = rschema.type
   636                 self[attr] = None
   663                 self[attr] = None
   637                 continue
   664                 continue
   638             yield attr
   665             yield attr
   639 
   666 
   640     _cw_completed = False
   667     _cw_completed = False
   641     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True):
   668     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
   642         """complete this entity by adding missing attributes (i.e. query the
   669         """complete this entity by adding missing attributes (i.e. query the
   643         repository to fill the entity)
   670         repository to fill the entity)
   644 
   671 
   645         :type skip_bytes: bool
   672         :type skip_bytes: bool
   646         :param skip_bytes:
   673         :param skip_bytes:
   653             self._cw_completed = True
   680             self._cw_completed = True
   654         varmaker = rqlvar_maker()
   681         varmaker = rqlvar_maker()
   655         V = varmaker.next()
   682         V = varmaker.next()
   656         rql = ['WHERE %s eid %%(x)s' % V]
   683         rql = ['WHERE %s eid %%(x)s' % V]
   657         selected = []
   684         selected = []
   658         for attr in (attributes or self.to_complete_attributes(skip_bytes, skip_pwd)):
   685         for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
   659             # if attribute already in entity, nothing to do
   686             # if attribute already in entity, nothing to do
   660             if self.has_key(attr):
   687             if self.cw_attr_cache.has_key(attr):
   661                 continue
   688                 continue
   662             # 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
   663             var = varmaker.next()
   690             var = varmaker.next()
   664             rql.append('%s %s %s' % (V, attr, var))
   691             rql.append('%s %s %s' % (V, attr, var))
   665             selected.append((attr, var))
   692             selected.append((attr, var))
   666         # +1 since this doen't include the main variable
   693         # +1 since this doen't include the main variable
   667         lastattr = len(selected) + 1
   694         lastattr = len(selected) + 1
   668         if attributes is None:
   695         if attributes is None:
   669             # fetch additional relations (restricted to 0..1 relations)
   696             # fetch additional relations (restricted to 0..1 relations)
   670             for rschema, role in self.to_complete_relations():
   697             for rschema, role in self._cw_to_complete_relations():
   671                 rtype = rschema.type
   698                 rtype = rschema.type
   672                 if self.relation_cached(rtype, role):
   699                 if self.cw_relation_cached(rtype, role):
   673                     continue
   700                     continue
       
   701                 # at this point we suppose that:
       
   702                 # * this is a inlined relation
       
   703                 # * entity (self) is the subject
       
   704                 # * user has read perm on the relation and on the target entity
       
   705                 assert rschema.inlined
       
   706                 assert role == 'subject'
   674                 var = varmaker.next()
   707                 var = varmaker.next()
   675                 targettype = rschema.targets(self.e_schema, role)[0]
   708                 # keep outer join anyway, we don't want .complete to crash on
   676                 rdef = rschema.role_rdef(self.e_schema, targettype, role)
   709                 # missing mandatory relation (see #1058267)
   677                 card = rdef.role_cardinality(role)
   710                 rql.append('%s %s %s?' % (V, rtype, var))
   678                 assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
       
   679                                                       role, card)
       
   680                 if role == 'subject':
       
   681                     if card == '1':
       
   682                         rql.append('%s %s %s' % (V, rtype, var))
       
   683                     else:
       
   684                         rql.append('%s %s %s?' % (V, rtype, var))
       
   685                 else:
       
   686                     if card == '1':
       
   687                         rql.append('%s %s %s' % (var, rtype, V))
       
   688                     else:
       
   689                         rql.append('%s? %s %s' % (var, rtype, V))
       
   690                 selected.append(((rtype, role), var))
   711                 selected.append(((rtype, role), var))
   691         if selected:
   712         if selected:
   692             # select V, we need it as the left most selected variable
   713             # select V, we need it as the left most selected variable
   693             # if some outer join are included to fetch inlined relations
   714             # if some outer join are included to fetch inlined relations
   694             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   715             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
   704                 if value is None:
   725                 if value is None:
   705                     rrset = ResultSet([], rql, {'x': self.eid})
   726                     rrset = ResultSet([], rql, {'x': self.eid})
   706                     rrset.req = self._cw
   727                     rrset.req = self._cw
   707                 else:
   728                 else:
   708                     rrset = self._cw.eid_rset(value)
   729                     rrset = self._cw.eid_rset(value)
   709                 self.set_related_cache(rtype, role, rrset)
   730                 self.cw_set_relation_cache(rtype, role, rrset)
   710 
   731 
   711     def get_value(self, name):
   732     def cw_attr_value(self, name):
   712         """get value for the attribute relation <name>, query the repository
   733         """get value for the attribute relation <name>, query the repository
   713         to get the value if necessary.
   734         to get the value if necessary.
   714 
   735 
   715         :type name: str
   736         :type name: str
   716         :param name: name of the attribute to get
   737         :param name: name of the attribute to get
   717         """
   738         """
   718         try:
   739         try:
   719             value = self[name]
   740             value = self.cw_attr_cache[name]
   720         except KeyError:
   741         except KeyError:
   721             if not self.is_saved():
   742             if not self.cw_is_saved():
   722                 return None
   743                 return None
   723             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   744             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
   724             try:
   745             try:
   725                 rset = self._cw.execute(rql, {'x': self.eid})
   746                 rset = self._cw.execute(rql, {'x': self.eid})
   726             except Unauthorized:
   747             except Unauthorized:
   738                         self[name] = value = self._cw._('unaccessible')
   759                         self[name] = value = self._cw._('unaccessible')
   739                     else:
   760                     else:
   740                         self[name] = value = None
   761                         self[name] = value = None
   741         return value
   762         return value
   742 
   763 
   743     def related(self, rtype, role='subject', limit=None, entities=False):
   764     def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
   744         """returns a resultset of related entities
   765         """returns a resultset of related entities
   745 
   766 
   746         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   767         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   747         :param limit: resultset's maximum size
   768         :param limit: resultset's maximum size
   748         :param entities: if True, the entites are returned; if False, a result set is returned
   769         :param entities: if True, the entites are returned; if False, a result set is returned
   749         """
   770         """
   750         try:
   771         try:
   751             return self.related_cache(rtype, role, entities, limit)
   772             return self._cw_relation_cache(rtype, role, entities, limit)
   752         except KeyError:
   773         except KeyError:
   753             pass
   774             pass
   754         if not self.has_eid():
   775         if not self.has_eid():
   755             if entities:
   776             if entities:
   756                 return []
   777                 return []
   757             return self.empty_rset()
   778             return self.empty_rset()
   758         rql = self.related_rql(rtype, role)
   779         rql = self.cw_related_rql(rtype, role)
   759         rset = self._cw.execute(rql, {'x': self.eid})
   780         rset = self._cw.execute(rql, {'x': self.eid})
   760         self.set_related_cache(rtype, role, rset)
   781         self.cw_set_relation_cache(rtype, role, rset)
   761         return self.related(rtype, role, limit, entities)
   782         return self.related(rtype, role, limit, entities)
   762 
   783 
   763     def related_rql(self, rtype, role='subject', targettypes=None):
   784     def cw_related_rql(self, rtype, role='subject', targettypes=None):
   764         rschema = self._cw.vreg.schema[rtype]
   785         rschema = self._cw.vreg.schema[rtype]
   765         if role == 'subject':
   786         if role == 'subject':
   766             restriction = 'E eid %%(x)s, E %s X' % rtype
   787             restriction = 'E eid %%(x)s, E %s X' % rtype
   767             if targettypes is None:
   788             if targettypes is None:
   768                 targettypes = rschema.objects(self.e_schema)
   789                 targettypes = rschema.objects(self.e_schema)
   807                       tuple(args)
   828                       tuple(args)
   808         return rql
   829         return rql
   809 
   830 
   810     # generic vocabulary methods ##############################################
   831     # generic vocabulary methods ##############################################
   811 
   832 
   812     def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   833     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   813                       vocabconstraints=True):
   834                       vocabconstraints=True):
   814         """build a rql to fetch `targettype` entities unrelated to this entity
   835         """build a rql to fetch `targettype` entities unrelated to this entity
   815         using (rtype, role) relation.
   836         using (rtype, role) relation.
   816 
   837 
   817         Consider relation permissions so that returned entities may be actually
   838         Consider relation permissions so that returned entities may be actually
   869                                  select.solutions, args, existant)
   890                                  select.solutions, args, existant)
   870             rql = rqlst.as_string()
   891             rql = rqlst.as_string()
   871         return rql, args
   892         return rql, args
   872 
   893 
   873     def unrelated(self, rtype, targettype, role='subject', limit=None,
   894     def unrelated(self, rtype, targettype, role='subject', limit=None,
   874                   ordermethod=None):
   895                   ordermethod=None): # XXX .cw_unrelated
   875         """return a result set of target type objects that may be related
   896         """return a result set of target type objects that may be related
   876         by a given relation, with self as subject or object
   897         by a given relation, with self as subject or object
   877         """
   898         """
   878         try:
   899         try:
   879             rql, args = self.unrelated_rql(rtype, targettype, role, ordermethod)
   900             rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
   880         except Unauthorized:
   901         except Unauthorized:
   881             return self._cw.empty_rset()
   902             return self._cw.empty_rset()
   882         if limit is not None:
   903         if limit is not None:
   883             before, after = rql.split(' WHERE ', 1)
   904             before, after = rql.split(' WHERE ', 1)
   884             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
   905             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
   885         return self._cw.execute(rql, args)
   906         return self._cw.execute(rql, args)
   886 
   907 
   887     # relations cache handling ################################################
   908     # relations cache handling #################################################
   888 
   909 
   889     def relation_cached(self, rtype, role):
   910     def cw_relation_cached(self, rtype, role):
   890         """return true if the given relation is already cached on the instance
   911         """return None if the given relation isn't already cached on the
   891         """
   912         instance, else the content of the cache (a 2-uple (rset, entities)).
   892         return self._related_cache.get('%s_%s' % (rtype, role))
   913         """
   893 
   914         return self._cw_related_cache.get('%s_%s' % (rtype, role))
   894     def related_cache(self, rtype, role, entities=True, limit=None):
   915 
       
   916     def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
   895         """return values for the given relation if it's cached on the instance,
   917         """return values for the given relation if it's cached on the instance,
   896         else raise `KeyError`
   918         else raise `KeyError`
   897         """
   919         """
   898         res = self._related_cache['%s_%s' % (rtype, role)][entities]
   920         res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
   899         if limit is not None and limit < len(res):
   921         if limit is not None and limit < len(res):
   900             if entities:
   922             if entities:
   901                 res = res[:limit]
   923                 res = res[:limit]
   902             else:
   924             else:
   903                 res = res.limit(limit)
   925                 res = res.limit(limit)
   904         return res
   926         return res
   905 
   927 
   906     def set_related_cache(self, rtype, role, rset, col=0):
   928     def cw_set_relation_cache(self, rtype, role, rset):
   907         """set cached values for the given relation"""
   929         """set cached values for the given relation"""
   908         if rset:
   930         if rset:
   909             related = list(rset.entities(col))
   931             related = list(rset.entities(0))
   910             rschema = self._cw.vreg.schema.rschema(rtype)
   932             rschema = self._cw.vreg.schema.rschema(rtype)
   911             if role == 'subject':
   933             if role == 'subject':
   912                 rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
   934                 rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
   913                 target = 'object'
   935                 target = 'object'
   914             else:
   936             else:
   915                 rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
   937                 rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
   916                 target = 'subject'
   938                 target = 'subject'
   917             if rcard in '?1':
   939             if rcard in '?1':
   918                 for rentity in related:
   940                 for rentity in related:
   919                     rentity._related_cache['%s_%s' % (rtype, target)] = (
   941                     rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
   920                         self.as_rset(), (self,))
   942                         self.as_rset(), (self,))
   921         else:
   943         else:
   922             related = ()
   944             related = ()
   923         self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
   945         self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
   924 
   946 
   925     def clear_related_cache(self, rtype=None, role=None):
   947     def cw_clear_relation_cache(self, rtype=None, role=None):
   926         """clear cached values for the given relation or the entire cache if
   948         """clear cached values for the given relation or the entire cache if
   927         no relation is given
   949         no relation is given
   928         """
   950         """
   929         if rtype is None:
   951         if rtype is None:
   930             self._related_cache = {}
   952             self._cw_related_cache = {}
       
   953             self._cw_adapters_cache = {}
   931         else:
   954         else:
   932             assert role
   955             assert role
   933             self._related_cache.pop('%s_%s' % (rtype, role), None)
   956             self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
   934 
   957 
   935     def clear_all_caches(self):
   958     def clear_all_caches(self): # XXX cw_clear_all_caches
   936         """flush all caches on this entity. Further attributes/relations access
   959         """flush all caches on this entity. Further attributes/relations access
   937         will triggers new database queries to get back values.
   960         will triggers new database queries to get back values.
   938 
   961 
   939         If you use custom caches on your entity class (take care to @cached!),
   962         If you use custom caches on your entity class (take care to @cached!),
   940         you should override this method to clear them as well.
   963         you should override this method to clear them as well.
   941         """
   964         """
   942         # clear attributes cache
   965         # clear attributes cache
   943         haseid = 'eid' in self
   966         haseid = 'eid' in self
   944         self._cw_completed = False
   967         self._cw_completed = False
   945         self.clear()
   968         self.cw_attr_cache.clear()
   946         # clear relations cache
   969         # clear relations cache
   947         for rschema, _, role in self.e_schema.relation_definitions():
   970         self.cw_clear_relation_cache()
   948             self.clear_related_cache(rschema.type, role)
       
   949         # rest path unique cache
   971         # rest path unique cache
   950         try:
   972         try:
   951             del self.__unique
   973             del self.__unique
   952         except AttributeError:
   974         except AttributeError:
   953             pass
   975             pass
   954 
   976 
   955     # raw edition utilities ###################################################
   977     # raw edition utilities ###################################################
   956 
   978 
   957     def set_attributes(self, **kwargs):
   979     def set_attributes(self, **kwargs): # XXX cw_set_attributes
   958         _check_cw_unsafe(kwargs)
   980         _check_cw_unsafe(kwargs)
   959         assert kwargs
   981         assert kwargs
   960         assert self._is_saved, "should not call set_attributes while entity "\
   982         assert self.cw_is_saved(), "should not call set_attributes while entity "\
   961                "hasn't been saved yet"
   983                "hasn't been saved yet"
   962         relations = []
   984         relations = []
   963         for key in kwargs:
   985         for key in kwargs:
   964             relations.append('X %s %%(%s)s' % (key, key))
   986             relations.append('X %s %%(%s)s' % (key, key))
   965         # and now update the database
   987         # and now update the database
   970         # update current local object _after_ the rql query to avoid
   992         # update current local object _after_ the rql query to avoid
   971         # interferences between the query execution itself and the
   993         # interferences between the query execution itself and the
   972         # edited_attributes / skip_security_attributes machinery
   994         # edited_attributes / skip_security_attributes machinery
   973         self.update(kwargs)
   995         self.update(kwargs)
   974 
   996 
   975     def set_relations(self, **kwargs):
   997     def set_relations(self, **kwargs): # XXX cw_set_relations
   976         """add relations to the given object. To set a relation where this entity
   998         """add relations to the given object. To set a relation where this entity
   977         is the object of the relation, use 'reverse_'<relation> as argument name.
   999         is the object of the relation, use 'reverse_'<relation> as argument name.
   978 
  1000 
   979         Values may be an entity, a list of entities, or None (meaning that all
  1001         Values may be an entity, a list of entities, or None (meaning that all
   980         relations of the given type from or to this object should be deleted).
  1002         relations of the given type from or to this object should be deleted).
   994                 values = (values,)
  1016                 values = (values,)
   995             self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
  1017             self._cw.execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
   996                 restr, ','.join(str(r.eid) for r in values)),
  1018                 restr, ','.join(str(r.eid) for r in values)),
   997                              {'x': self.eid})
  1019                              {'x': self.eid})
   998 
  1020 
   999     def delete(self, **kwargs):
  1021     def cw_delete(self, **kwargs):
  1000         assert self.has_eid(), self.eid
  1022         assert self.has_eid(), self.eid
  1001         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
  1023         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
  1002                          {'x': self.eid}, **kwargs)
  1024                          {'x': self.eid}, **kwargs)
  1003 
  1025 
  1004     # server side utilities ###################################################
  1026     # server side utilities ###################################################
  1005 
  1027 
       
  1028     def _cw_rql_set_value(self, attr, value):
       
  1029         """call by rql execution plan when some attribute is modified
       
  1030 
       
  1031         don't use dict api in such case since we don't want attribute to be
       
  1032         added to skip_security_attributes.
       
  1033 
       
  1034         This method is for internal use, you should not use it.
       
  1035         """
       
  1036         self.cw_attr_cache[attr] = value
       
  1037 
       
  1038     def _cw_clear_local_perm_cache(self, action):
       
  1039         for rqlexpr in self.e_schema.get_rqlexprs(action):
       
  1040             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
       
  1041 
  1006     @property
  1042     @property
  1007     def skip_security_attributes(self):
  1043     def _cw_skip_security_attributes(self):
  1008         try:
  1044         try:
  1009             return self._skip_security_attributes
  1045             return self.__cw_skip_security_attributes
  1010         except:
  1046         except:
  1011             self._skip_security_attributes = set()
  1047             self.__cw_skip_security_attributes = set()
  1012             return self._skip_security_attributes
  1048             return self.__cw_skip_security_attributes
  1013 
  1049 
  1014     def set_defaults(self):
  1050     def _cw_set_defaults(self):
  1015         """set default values according to the schema"""
  1051         """set default values according to the schema"""
  1016         for attr, value in self.e_schema.defaults():
  1052         for attr, value in self.e_schema.defaults():
  1017             if not self.has_key(attr):
  1053             if not self.cw_attr_cache.has_key(attr):
  1018                 self[str(attr)] = value
  1054                 self[str(attr)] = value
  1019 
  1055 
  1020     def check(self, creation=False):
  1056     def _cw_check(self, creation=False):
  1021         """check this entity against its schema. Only final relation
  1057         """check this entity against its schema. Only final relation
  1022         are checked here, constraint on actual relations are checked in hooks
  1058         are checked here, constraint on actual relations are checked in hooks
  1023         """
  1059         """
  1024         # necessary since eid is handled specifically and yams require it to be
  1060         # necessary since eid is handled specifically and yams require it to be
  1025         # in the dictionary
  1061         # in the dictionary
  1038         else:
  1074         else:
  1039             relations = None
  1075             relations = None
  1040         self.e_schema.check(self, creation=creation, _=_,
  1076         self.e_schema.check(self, creation=creation, _=_,
  1041                             relations=relations)
  1077                             relations=relations)
  1042 
  1078 
  1043     def fti_containers(self, _done=None):
  1079     @deprecated('[3.9] use entity.cw_attr_value(attr)')
  1044         if _done is None:
  1080     def get_value(self, name):
  1045             _done = set()
  1081         return self.cw_attr_value(name)
  1046         _done.add(self.eid)
  1082 
  1047         containers = tuple(self.e_schema.fulltext_containers())
  1083     @deprecated('[3.9] use entity.cw_delete()')
  1048         if containers:
  1084     def delete(self, **kwargs):
  1049             for rschema, target in containers:
  1085         return self.cw_delete(**kwargs)
  1050                 if target == 'object':
  1086 
  1051                     targets = getattr(self, rschema.type)
  1087     @deprecated('[3.9] use entity.cw_attr_metadata(attr, metadata)')
  1052                 else:
  1088     def attr_metadata(self, attr, metadata):
  1053                     targets = getattr(self, 'reverse_%s' % rschema)
  1089         return self.cw_attr_metadata(attr, metadata)
  1054                 for entity in targets:
  1090 
  1055                     if entity.eid in _done:
  1091     @deprecated('[3.9] use entity.cw_has_perm(action)')
  1056                         continue
  1092     def has_perm(self, action):
  1057                     for container in entity.fti_containers(_done):
  1093         return self.cw_has_perm(action)
  1058                         yield container
  1094 
  1059                         yielded = True
  1095     @deprecated('[3.9] use entity.cw_set_relation_cache(rtype, role, rset)')
  1060         else:
  1096     def set_related_cache(self, rtype, role, rset):
  1061             yield self
  1097         self.cw_set_relation_cache(rtype, role, rset)
  1062 
  1098 
  1063     def get_words(self):
  1099     @deprecated('[3.9] use entity.cw_clear_relation_cache(rtype, role, rset)')
  1064         """used by the full text indexer to get words to index
  1100     def clear_related_cache(self, rtype=None, role=None):
  1065 
  1101         self.cw_clear_relation_cache(rtype, role)
  1066         this method should only be used on the repository side since it depends
  1102 
  1067         on the logilab.database package
  1103     @deprecated('[3.9] use entity.cw_related_rql(rtype, [role, [targettypes]])')
  1068 
  1104     def related_rql(self, rtype, role='subject', targettypes=None):
  1069         :rtype: list
  1105         return self.cw_related_rql(rtype, role, targettypes)
  1070         :return: the list of indexable word of this entity
       
  1071         """
       
  1072         from logilab.database.fti import tokenize
       
  1073         # take care to cases where we're modyfying the schema
       
  1074         pending = self._cw.transaction_data.setdefault('pendingrdefs', set())
       
  1075         words = []
       
  1076         for rschema in self.e_schema.indexable_attributes():
       
  1077             if (self.e_schema, rschema) in pending:
       
  1078                 continue
       
  1079             try:
       
  1080                 value = self.printable_value(rschema, format='text/plain')
       
  1081             except TransformError:
       
  1082                 continue
       
  1083             except:
       
  1084                 self.exception("can't add value of %s to text index for entity %s",
       
  1085                                rschema, self.eid)
       
  1086                 continue
       
  1087             if value:
       
  1088                 words += tokenize(value)
       
  1089         for rschema, role in self.e_schema.fulltext_relations():
       
  1090             if role == 'subject':
       
  1091                 for entity in getattr(self, rschema.type):
       
  1092                     words += entity.get_words()
       
  1093             else: # if role == 'object':
       
  1094                 for entity in getattr(self, 'reverse_%s' % rschema.type):
       
  1095                     words += entity.get_words()
       
  1096         return words
       
  1097 
  1106 
  1098 
  1107 
  1099 # attribute and relation descriptors ##########################################
  1108 # attribute and relation descriptors ##########################################
  1100 
  1109 
  1101 class Attribute(object):
  1110 class Attribute(object):
  1106         self._attrname = attrname
  1115         self._attrname = attrname
  1107 
  1116 
  1108     def __get__(self, eobj, eclass):
  1117     def __get__(self, eobj, eclass):
  1109         if eobj is None:
  1118         if eobj is None:
  1110             return self
  1119             return self
  1111         return eobj.get_value(self._attrname)
  1120         return eobj.cw_attr_value(self._attrname)
  1112 
  1121 
  1113     def __set__(self, eobj, value):
  1122     def __set__(self, eobj, value):
  1114         eobj[self._attrname] = value
  1123         eobj[self._attrname] = value
  1115 
  1124 
       
  1125 
  1116 class Relation(object):
  1126 class Relation(object):
  1117     """descriptor that controls schema relation access"""
  1127     """descriptor that controls schema relation access"""
  1118     _role = None # for pylint
  1128 
  1119 
  1129     def __init__(self, rschema, role):
  1120     def __init__(self, rschema):
       
  1121         self._rschema = rschema
       
  1122         self._rtype = rschema.type
  1130         self._rtype = rschema.type
       
  1131         self._role = role
  1123 
  1132 
  1124     def __get__(self, eobj, eclass):
  1133     def __get__(self, eobj, eclass):
  1125         if eobj is None:
  1134         if eobj is None:
  1126             raise AttributeError('%s cannot be only be accessed from instances'
  1135             raise AttributeError('%s cannot be only be accessed from instances'
  1127                                  % self._rtype)
  1136                                  % self._rtype)
  1129 
  1138 
  1130     def __set__(self, eobj, value):
  1139     def __set__(self, eobj, value):
  1131         raise NotImplementedError
  1140         raise NotImplementedError
  1132 
  1141 
  1133 
  1142 
  1134 class SubjectRelation(Relation):
       
  1135     """descriptor that controls schema relation access"""
       
  1136     _role = 'subject'
       
  1137 
       
  1138 class ObjectRelation(Relation):
       
  1139     """descriptor that controls schema relation access"""
       
  1140     _role = 'object'
       
  1141 
       
  1142 from logging import getLogger
  1143 from logging import getLogger
  1143 from cubicweb import set_log_methods
  1144 from cubicweb import set_log_methods
  1144 set_log_methods(Entity, getLogger('cubicweb.entity'))
  1145 set_log_methods(Entity, getLogger('cubicweb.entity'))