entity.py
branchtls-sprint
changeset 1474 716f0742ee7f
parent 1471 c889c3bcf5ec
child 1498 2c6eec0b46b9
equal deleted inserted replaced
1473:717dea3362c0 1474:716f0742ee7f
    60             else:
    60             else:
    61                 raise ValueError(tag)
    61                 raise ValueError(tag)
    62 
    62 
    63 except ImportError:
    63 except ImportError:
    64     AutomaticEntityForm = None
    64     AutomaticEntityForm = None
    65     
    65 
    66     def dispatch_rtags(*args):
    66     def dispatch_rtags(*args):
    67         pass
    67         pass
    68 
    68 
    69 def _get_etype(bases, classdict):
    69 def _get_etype(bases, classdict):
    70     try:
    70     try:
    72     except KeyError:
    72     except KeyError:
    73         for base in bases:
    73         for base in bases:
    74             etype = getattr(base, 'id', None)
    74             etype = getattr(base, 'id', None)
    75             if etype and etype != 'Any':
    75             if etype and etype != 'Any':
    76                 return etype
    76                 return etype
    77             
    77 
    78 def _get_defs(attr, name, bases, classdict):
    78 def _get_defs(attr, name, bases, classdict):
    79     try:
    79     try:
    80         yield name, classdict.pop(attr)
    80         yield name, classdict.pop(attr)
    81     except KeyError:
    81     except KeyError:
    82         for base in bases:
    82         for base in bases:
    84                 value = getattr(base, attr)
    84                 value = getattr(base, attr)
    85                 delattr(base, attr)
    85                 delattr(base, attr)
    86                 yield base.__name__, value
    86                 yield base.__name__, value
    87             except AttributeError:
    87             except AttributeError:
    88                 continue
    88                 continue
    89             
    89 
    90 class metaentity(type):
    90 class metaentity(type):
    91     """this metaclass sets the relation tags on the entity class
    91     """this metaclass sets the relation tags on the entity class
    92     and deals with the `widgets` attribute
    92     and deals with the `widgets` attribute
    93     """
    93     """
    94     def __new__(mcs, name, bases, classdict):
    94     def __new__(mcs, name, bases, classdict):
   136 
   136 
   137 
   137 
   138 class Entity(AppRsetObject, dict):
   138 class Entity(AppRsetObject, dict):
   139     """an entity instance has e_schema automagically set on
   139     """an entity instance has e_schema automagically set on
   140     the class and instances has access to their issuing cursor.
   140     the class and instances has access to their issuing cursor.
   141     
   141 
   142     A property is set for each attribute and relation on each entity's type
   142     A property is set for each attribute and relation on each entity's type
   143     class. Becare that among attributes, 'eid' is *NEITHER* stored in the
   143     class. Becare that among attributes, 'eid' is *NEITHER* stored in the
   144     dict containment (which acts as a cache for other attributes dynamically
   144     dict containment (which acts as a cache for other attributes dynamically
   145     fetched)
   145     fetched)
   146 
   146 
   149 
   149 
   150     :type rest_var: str
   150     :type rest_var: str
   151     :cvar rest_var: indicates which attribute should be used to build REST urls
   151     :cvar rest_var: indicates which attribute should be used to build REST urls
   152                     If None is specified, the first non-meta attribute will
   152                     If None is specified, the first non-meta attribute will
   153                     be used
   153                     be used
   154                     
   154 
   155     :type skip_copy_for: list
   155     :type skip_copy_for: list
   156     :cvar skip_copy_for: a list of relations that should be skipped when copying
   156     :cvar skip_copy_for: a list of relations that should be skipped when copying
   157                          this kind of entity. Note that some relations such
   157                          this kind of entity. Note that some relations such
   158                          as composite relations or relations that have '?1' as object
   158                          as composite relations or relations that have '?1' as object
   159                          cardinality
   159                          cardinality
   160     """
   160     """
   161     __metaclass__ = metaentity
   161     __metaclass__ = metaentity
   162     __registry__ = 'etypes'
   162     __registry__ = 'etypes'
   163     __select__ = yes()
   163     __select__ = yes()
   164 
   164 
   165     # class attributes that must be set in class definition 
   165     # class attributes that must be set in class definition
   166     id = None
   166     id = None
   167     rest_attr = None
   167     rest_attr = None
   168     fetch_attrs = None
   168     fetch_attrs = None
   169     skip_copy_for = ()
   169     skip_copy_for = ()
   170     # class attributes set automatically at registration time
   170     # class attributes set automatically at registration time
   171     e_schema = None
   171     e_schema = None
   172     
   172 
   173     @classmethod
   173     @classmethod
   174     def registered(cls, registry):
   174     def registered(cls, registry):
   175         """build class using descriptor at registration time"""
   175         """build class using descriptor at registration time"""
   176         assert cls.id is not None
   176         assert cls.id is not None
   177         super(Entity, cls).registered(registry)
   177         super(Entity, cls).registered(registry)
   178         if cls.id != 'Any':
   178         if cls.id != 'Any':
   179             cls.__initialize__()
   179             cls.__initialize__()
   180         return cls
   180         return cls
   181                 
   181 
   182     MODE_TAGS = set(('link', 'create'))
   182     MODE_TAGS = set(('link', 'create'))
   183     CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
   183     CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
   184     @classmethod
   184     @classmethod
   185     def __initialize__(cls):
   185     def __initialize__(cls):
   186         """initialize a specific entity class by adding descriptors to access
   186         """initialize a specific entity class by adding descriptors to access
   208                 attr = 'reverse_%s' % rschema.type
   208                 attr = 'reverse_%s' % rschema.type
   209                 setattr(cls, attr, ObjectRelation(rschema))
   209                 setattr(cls, attr, ObjectRelation(rschema))
   210         if mixins:
   210         if mixins:
   211             cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object])
   211             cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object])
   212             cls.debug('plugged %s mixins on %s', mixins, etype)
   212             cls.debug('plugged %s mixins on %s', mixins, etype)
   213     
   213 
   214     @classmethod
   214     @classmethod
   215     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   215     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   216                   settype=True, ordermethod='fetch_order'):
   216                   settype=True, ordermethod='fetch_order'):
   217         """return a rql to fetch all entities of the class type"""
   217         """return a rql to fetch all entities of the class type"""
   218         restrictions = restriction or []
   218         restrictions = restriction or []
   229         rql = 'Any %s' % ','.join(selection)
   229         rql = 'Any %s' % ','.join(selection)
   230         if orderby:
   230         if orderby:
   231             rql +=  ' ORDERBY %s' % ','.join(orderby)
   231             rql +=  ' ORDERBY %s' % ','.join(orderby)
   232         rql += ' WHERE %s' % ', '.join(restrictions)
   232         rql += ' WHERE %s' % ', '.join(restrictions)
   233         return rql
   233         return rql
   234     
   234 
   235     @classmethod
   235     @classmethod
   236     def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
   236     def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
   237                             selection, orderby, restrictions, user,
   237                             selection, orderby, restrictions, user,
   238                             ordermethod='fetch_order', visited=None):
   238                             ordermethod='fetch_order', visited=None):
   239         eschema = cls.e_schema
   239         eschema = cls.e_schema
   291         if rset is not None:
   291         if rset is not None:
   292             self.eid = rset[row][col]
   292             self.eid = rset[row][col]
   293         else:
   293         else:
   294             self.eid = None
   294             self.eid = None
   295         self._is_saved = True
   295         self._is_saved = True
   296         
   296 
   297     def __repr__(self):
   297     def __repr__(self):
   298         return '<Entity %s %s %s at %s>' % (
   298         return '<Entity %s %s %s at %s>' % (
   299             self.e_schema, self.eid, self.keys(), id(self))
   299             self.e_schema, self.eid, self.keys(), id(self))
   300 
   300 
   301     def __nonzero__(self):
   301     def __nonzero__(self):
   306 
   306 
   307     def pre_add_hook(self):
   307     def pre_add_hook(self):
   308         """hook called by the repository before doing anything to add the entity
   308         """hook called by the repository before doing anything to add the entity
   309         (before_add entity hooks have not been called yet). This give the
   309         (before_add entity hooks have not been called yet). This give the
   310         occasion to do weird stuff such as autocast (File -> Image for instance).
   310         occasion to do weird stuff such as autocast (File -> Image for instance).
   311         
   311 
   312         This method must return the actual entity to be added.
   312         This method must return the actual entity to be added.
   313         """
   313         """
   314         return self
   314         return self
   315     
   315 
   316     def set_eid(self, eid):
   316     def set_eid(self, eid):
   317         self.eid = self['eid'] = eid
   317         self.eid = self['eid'] = eid
   318 
   318 
   319     def has_eid(self):
   319     def has_eid(self):
   320         """return True if the entity has an attributed eid (False
   320         """return True if the entity has an attributed eid (False
   331         has an eid attributed though it's not saved (eg during before_add_entity
   331         has an eid attributed though it's not saved (eg during before_add_entity
   332         hooks). You can use this method to ensure the entity has an eid *and* is
   332         hooks). You can use this method to ensure the entity has an eid *and* is
   333         saved in its source.
   333         saved in its source.
   334         """
   334         """
   335         return self.has_eid() and self._is_saved
   335         return self.has_eid() and self._is_saved
   336     
   336 
   337     @cached
   337     @cached
   338     def metainformation(self):
   338     def metainformation(self):
   339         res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid)))
   339         res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid)))
   340         res['source'] = self.req.source_defs()[res['source']]
   340         res['source'] = self.req.source_defs()[res['source']]
   341         return res
   341         return res
   347     def check_perm(self, action):
   347     def check_perm(self, action):
   348         self.e_schema.check_perm(self.req, action, self.eid)
   348         self.e_schema.check_perm(self.req, action, self.eid)
   349 
   349 
   350     def has_perm(self, action):
   350     def has_perm(self, action):
   351         return self.e_schema.has_perm(self.req, action, self.eid)
   351         return self.e_schema.has_perm(self.req, action, self.eid)
   352         
   352 
   353     def view(self, vid, __registry='views', **kwargs):
   353     def view(self, vid, __registry='views', **kwargs):
   354         """shortcut to apply a view on this entity"""
   354         """shortcut to apply a view on this entity"""
   355         return self.vreg.render(__registry, vid, self.req, rset=self.rset,
   355         return self.vreg.render(__registry, vid, self.req, rset=self.rset,
   356                                 row=self.row, col=self.col, **kwargs)
   356                                 row=self.row, col=self.col, **kwargs)
   357 
   357 
   450     def mtc_transform(self, data, format, target_format, encoding,
   450     def mtc_transform(self, data, format, target_format, encoding,
   451                       _engine=ENGINE):
   451                       _engine=ENGINE):
   452         trdata = TransformData(data, format, encoding, appobject=self)
   452         trdata = TransformData(data, format, encoding, appobject=self)
   453         data = _engine.convert(trdata, target_format).decode()
   453         data = _engine.convert(trdata, target_format).decode()
   454         if format == 'text/html':
   454         if format == 'text/html':
   455             data = soup2xhtml(data, self.req.encoding)                
   455             data = soup2xhtml(data, self.req.encoding)
   456         return data
   456         return data
   457     
   457 
   458     # entity cloning ##########################################################
   458     # entity cloning ##########################################################
   459 
   459 
   460     def copy_relations(self, ceid):
   460     def copy_relations(self, ceid):
   461         """copy relations of the object with the given eid on this object
   461         """copy relations of the object with the given eid on this object
   462 
   462 
   515     def as_rset(self):
   515     def as_rset(self):
   516         """returns a resultset containing `self` information"""
   516         """returns a resultset containing `self` information"""
   517         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
   517         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
   518                          {'x': self.eid}, [(self.id,)])
   518                          {'x': self.eid}, [(self.id,)])
   519         return self.req.decorate_rset(rset)
   519         return self.req.decorate_rset(rset)
   520                        
   520 
   521     def to_complete_relations(self):
   521     def to_complete_relations(self):
   522         """by default complete final relations to when calling .complete()"""
   522         """by default complete final relations to when calling .complete()"""
   523         for rschema in self.e_schema.subject_relations():
   523         for rschema in self.e_schema.subject_relations():
   524             if rschema.is_final():
   524             if rschema.is_final():
   525                 continue
   525                 continue
   531                 matching_groups = self.req.user.matching_groups
   531                 matching_groups = self.req.user.matching_groups
   532                 if matching_groups(rschema.get_groups('read')) and \
   532                 if matching_groups(rschema.get_groups('read')) and \
   533                    all(matching_groups(es.get_groups('read'))
   533                    all(matching_groups(es.get_groups('read'))
   534                        for es in rschema.objects(self.e_schema)):
   534                        for es in rschema.objects(self.e_schema)):
   535                     yield rschema, 'subject'
   535                     yield rschema, 'subject'
   536                     
   536 
   537     def to_complete_attributes(self, skip_bytes=True):
   537     def to_complete_attributes(self, skip_bytes=True):
   538         for rschema, attrschema in self.e_schema.attribute_definitions():
   538         for rschema, attrschema in self.e_schema.attribute_definitions():
   539             # skip binary data by default
   539             # skip binary data by default
   540             if skip_bytes and attrschema.type == 'Bytes':
   540             if skip_bytes and attrschema.type == 'Bytes':
   541                 continue
   541                 continue
   546             if not self.req.user.matching_groups(rschema.get_groups('read')) \
   546             if not self.req.user.matching_groups(rschema.get_groups('read')) \
   547                    or attrschema.type == 'Password':
   547                    or attrschema.type == 'Password':
   548                 self[attr] = None
   548                 self[attr] = None
   549                 continue
   549                 continue
   550             yield attr
   550             yield attr
   551             
   551 
   552     def complete(self, attributes=None, skip_bytes=True):
   552     def complete(self, attributes=None, skip_bytes=True):
   553         """complete this entity by adding missing attributes (i.e. query the
   553         """complete this entity by adding missing attributes (i.e. query the
   554         repository to fill the entity)
   554         repository to fill the entity)
   555 
   555 
   556         :type skip_bytes: bool
   556         :type skip_bytes: bool
   616                     rrset = ResultSet([], rql, {'x': self.eid})
   616                     rrset = ResultSet([], rql, {'x': self.eid})
   617                     self.req.decorate_rset(rrset)
   617                     self.req.decorate_rset(rrset)
   618                 else:
   618                 else:
   619                     rrset = self.req.eid_rset(value)
   619                     rrset = self.req.eid_rset(value)
   620                 self.set_related_cache(rtype, x, rrset)
   620                 self.set_related_cache(rtype, x, rrset)
   621                 
   621 
   622     def get_value(self, name):
   622     def get_value(self, name):
   623         """get value for the attribute relation <name>, query the repository
   623         """get value for the attribute relation <name>, query the repository
   624         to get the value if necessary.
   624         to get the value if necessary.
   625 
   625 
   626         :type name: str
   626         :type name: str
   652                         self[name] = value = None
   652                         self[name] = value = None
   653         return value
   653         return value
   654 
   654 
   655     def related(self, rtype, role='subject', limit=None, entities=False):
   655     def related(self, rtype, role='subject', limit=None, entities=False):
   656         """returns a resultset of related entities
   656         """returns a resultset of related entities
   657         
   657 
   658         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   658         :param role: is the role played by 'self' in the relation ('subject' or 'object')
   659         :param limit: resultset's maximum size
   659         :param limit: resultset's maximum size
   660         :param entities: if True, the entites are returned; if False, a result set is returned
   660         :param entities: if True, the entites are returned; if False, a result set is returned
   661         """
   661         """
   662         try:
   662         try:
   698                                        rql.split(' WHERE ', 1)[1])
   698                                        rql.split(' WHERE ', 1)[1])
   699         elif not ' ORDERBY ' in rql:
   699         elif not ' ORDERBY ' in rql:
   700             args = tuple(rql.split(' WHERE ', 1))
   700             args = tuple(rql.split(' WHERE ', 1))
   701             rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args
   701             rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args
   702         return rql
   702         return rql
   703     
   703 
   704     # generic vocabulary methods ##############################################
   704     # generic vocabulary methods ##############################################
   705 
   705 
   706     @obsolete('see new form api')
   706     @obsolete('see new form api')
   707     def vocabulary(self, rtype, role='subject', limit=None):
   707     def vocabulary(self, rtype, role='subject', limit=None):
   708         """vocabulary functions must return a list of couples
   708         """vocabulary functions must return a list of couples
   709         (label, eid) that will typically be used to fill the
   709         (label, eid) that will typically be used to fill the
   710         edition view's combobox.
   710         edition view's combobox.
   711         
   711 
   712         If `eid` is None in one of these couples, it should be
   712         If `eid` is None in one of these couples, it should be
   713         interpreted as a separator in case vocabulary results are grouped
   713         interpreted as a separator in case vocabulary results are grouped
   714         """
   714         """
   715         from cubicweb.web.form import EntityFieldsForm
   715         from cubicweb.web.form import EntityFieldsForm
   716         from logilab.common.testlib import mock_object
   716         from logilab.common.testlib import mock_object
   717         form = EntityFieldsForm(self.req, entity=self)
   717         form = EntityFieldsForm(self.req, entity=self)
   718         field = mock_object(name=rtype, role=role)
   718         field = mock_object(name=rtype, role=role)
   719         return form.form_field_vocabulary(field, limit)
   719         return form.form_field_vocabulary(field, limit)
   720             
   720 
   721     def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   721     def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   722                       vocabconstraints=True):
   722                       vocabconstraints=True):
   723         """build a rql to fetch `targettype` entities unrelated to this entity
   723         """build a rql to fetch `targettype` entities unrelated to this entity
   724         using (rtype, role) relation
   724         using (rtype, role) relation
   725         """
   725         """
   751         # ensure we have an order defined
   751         # ensure we have an order defined
   752         if not ' ORDERBY ' in rql:
   752         if not ' ORDERBY ' in rql:
   753             before, after = rql.split(' WHERE ', 1)
   753             before, after = rql.split(' WHERE ', 1)
   754             rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
   754             rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
   755         return rql
   755         return rql
   756     
   756 
   757     def unrelated(self, rtype, targettype, role='subject', limit=None,
   757     def unrelated(self, rtype, targettype, role='subject', limit=None,
   758                   ordermethod=None):
   758                   ordermethod=None):
   759         """return a result set of target type objects that may be related
   759         """return a result set of target type objects that may be related
   760         by a given relation, with self as subject or object
   760         by a given relation, with self as subject or object
   761         """
   761         """
   764             before, after = rql.split(' WHERE ', 1)
   764             before, after = rql.split(' WHERE ', 1)
   765             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
   765             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
   766         if self.has_eid():
   766         if self.has_eid():
   767             return self.req.execute(rql, {'x': self.eid})
   767             return self.req.execute(rql, {'x': self.eid})
   768         return self.req.execute(rql)
   768         return self.req.execute(rql)
   769         
   769 
   770     # relations cache handling ################################################
   770     # relations cache handling ################################################
   771     
   771 
   772     def relation_cached(self, rtype, role):
   772     def relation_cached(self, rtype, role):
   773         """return true if the given relation is already cached on the instance
   773         """return true if the given relation is already cached on the instance
   774         """
   774         """
   775         return '%s_%s' % (rtype, role) in self._related_cache
   775         return '%s_%s' % (rtype, role) in self._related_cache
   776     
   776 
   777     def related_cache(self, rtype, role, entities=True, limit=None):
   777     def related_cache(self, rtype, role, entities=True, limit=None):
   778         """return values for the given relation if it's cached on the instance,
   778         """return values for the given relation if it's cached on the instance,
   779         else raise `KeyError`
   779         else raise `KeyError`
   780         """
   780         """
   781         res = self._related_cache['%s_%s' % (rtype, role)][entities]
   781         res = self._related_cache['%s_%s' % (rtype, role)][entities]
   783             if entities:
   783             if entities:
   784                 res = res[:limit]
   784                 res = res[:limit]
   785             else:
   785             else:
   786                 res = res.limit(limit)
   786                 res = res.limit(limit)
   787         return res
   787         return res
   788     
   788 
   789     def set_related_cache(self, rtype, role, rset, col=0):
   789     def set_related_cache(self, rtype, role, rset, col=0):
   790         """set cached values for the given relation"""
   790         """set cached values for the given relation"""
   791         if rset:
   791         if rset:
   792             related = list(rset.entities(col))
   792             related = list(rset.entities(col))
   793             rschema = self.schema.rschema(rtype)
   793             rschema = self.schema.rschema(rtype)
   803                 for rentity in related:
   803                 for rentity in related:
   804                     rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self])
   804                     rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self])
   805         else:
   805         else:
   806             related = []
   806             related = []
   807         self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
   807         self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
   808         
   808 
   809     def clear_related_cache(self, rtype=None, role=None):
   809     def clear_related_cache(self, rtype=None, role=None):
   810         """clear cached values for the given relation or the entire cache if
   810         """clear cached values for the given relation or the entire cache if
   811         no relation is given
   811         no relation is given
   812         """
   812         """
   813         if rtype is None:
   813         if rtype is None:
   814             self._related_cache = {}
   814             self._related_cache = {}
   815         else:
   815         else:
   816             assert role
   816             assert role
   817             self._related_cache.pop('%s_%s' % (rtype, role), None)
   817             self._related_cache.pop('%s_%s' % (rtype, role), None)
   818         
   818 
   819     # raw edition utilities ###################################################
   819     # raw edition utilities ###################################################
   820     
   820 
   821     def set_attributes(self, **kwargs):
   821     def set_attributes(self, **kwargs):
   822         assert kwargs
   822         assert kwargs
   823         relations = []
   823         relations = []
   824         for key in kwargs:
   824         for key in kwargs:
   825             relations.append('X %s %%(%s)s' % (key, key))
   825             relations.append('X %s %%(%s)s' % (key, key))
   827         self.update(kwargs)
   827         self.update(kwargs)
   828         # and now update the database
   828         # and now update the database
   829         kwargs['x'] = self.eid
   829         kwargs['x'] = self.eid
   830         self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
   830         self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
   831                          kwargs, 'x')
   831                          kwargs, 'x')
   832             
   832 
   833     def delete(self):
   833     def delete(self):
   834         assert self.has_eid(), self.eid
   834         assert self.has_eid(), self.eid
   835         self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
   835         self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
   836                          {'x': self.eid})
   836                          {'x': self.eid})
   837     
   837 
   838     # server side utilities ###################################################
   838     # server side utilities ###################################################
   839         
   839 
   840     def set_defaults(self):
   840     def set_defaults(self):
   841         """set default values according to the schema"""
   841         """set default values according to the schema"""
   842         self._default_set = set()
   842         self._default_set = set()
   843         for attr, value in self.e_schema.defaults():
   843         for attr, value in self.e_schema.defaults():
   844             if not self.has_key(attr):
   844             if not self.has_key(attr):
   877                         yielded = True
   877                         yielded = True
   878             if not yielded:
   878             if not yielded:
   879                 yield self
   879                 yield self
   880         else:
   880         else:
   881             yield self
   881             yield self
   882                     
   882 
   883     def get_words(self):
   883     def get_words(self):
   884         """used by the full text indexer to get words to index
   884         """used by the full text indexer to get words to index
   885 
   885 
   886         this method should only be used on the repository side since it depends
   886         this method should only be used on the repository side since it depends
   887         on the indexer package
   887         on the indexer package
   888         
   888 
   889         :rtype: list
   889         :rtype: list
   890         :return: the list of indexable word of this entity
   890         :return: the list of indexable word of this entity
   891         """
   891         """
   892         from indexer.query_objects import tokenize
   892         from indexer.query_objects import tokenize
   893         words = []
   893         words = []
   900                 self.exception("can't add value of %s to text index for entity %s",
   900                 self.exception("can't add value of %s to text index for entity %s",
   901                                rschema, self.eid)
   901                                rschema, self.eid)
   902                 continue
   902                 continue
   903             if value:
   903             if value:
   904                 words += tokenize(value)
   904                 words += tokenize(value)
   905         
   905 
   906         for rschema, role in self.e_schema.fulltext_relations():
   906         for rschema, role in self.e_schema.fulltext_relations():
   907             if role == 'subject':
   907             if role == 'subject':
   908                 for entity in getattr(self, rschema.type):
   908                 for entity in getattr(self, rschema.type):
   909                     words += entity.get_words()
   909                     words += entity.get_words()
   910             else: # if role == 'object':
   910             else: # if role == 'object':
   945     def __get__(self, eobj, eclass):
   945     def __get__(self, eobj, eclass):
   946         if eobj is None:
   946         if eobj is None:
   947             raise AttributeError('%s cannot be only be accessed from instances'
   947             raise AttributeError('%s cannot be only be accessed from instances'
   948                                  % self._rtype)
   948                                  % self._rtype)
   949         return eobj.related(self._rtype, self._role, entities=True)
   949         return eobj.related(self._rtype, self._role, entities=True)
   950     
   950 
   951     def __set__(self, eobj, value):
   951     def __set__(self, eobj, value):
   952         raise NotImplementedError
   952         raise NotImplementedError
   953 
   953 
   954 
   954 
   955 class SubjectRelation(Relation):
   955 class SubjectRelation(Relation):
   956     """descriptor that controls schema relation access"""
   956     """descriptor that controls schema relation access"""
   957     _role = 'subject'
   957     _role = 'subject'
   958     
   958 
   959 class ObjectRelation(Relation):
   959 class ObjectRelation(Relation):
   960     """descriptor that controls schema relation access"""
   960     """descriptor that controls schema relation access"""
   961     _role = 'object'
   961     _role = 'object'
   962 
   962 
   963 from logging import getLogger
   963 from logging import getLogger