entity.py
branchtls-sprint
changeset 702 c1d2f9bfb9ff
parent 692 800592b8d39b
child 707 21a59b468f1a
child 712 ce49e3885453
equal deleted inserted replaced
701:cd5e855de490 702:c1d2f9bfb9ff
       
     1 """Base class for entity objects manipulated in clients
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 from logilab.common import interface
       
    10 from logilab.common.compat import all
       
    11 from logilab.common.decorators import cached
       
    12 from logilab.mtconverter import TransformData, TransformError
       
    13 
       
    14 from rql.utils import rqlvar_maker
       
    15 
       
    16 from cubicweb import Unauthorized
       
    17 from cubicweb.vregistry import autoselectors
       
    18 from cubicweb.rset import ResultSet
       
    19 from cubicweb.selectors import yes
       
    20 from cubicweb.common.appobject import AppRsetObject
       
    21 from cubicweb.common.registerers import id_registerer
       
    22 from cubicweb.common.uilib import printable_value, html_escape, soup2xhtml
       
    23 from cubicweb.common.mixins import MI_REL_TRIGGERS
       
    24 from cubicweb.common.mttransforms import ENGINE
       
    25 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint, bw_normalize_etype
       
    26 
       
    27 _marker = object()
       
    28 
       
    29 def greater_card(rschema, subjtypes, objtypes, index):
       
    30     for subjtype in subjtypes:
       
    31         for objtype in objtypes:
       
    32             card = rschema.rproperty(subjtype, objtype, 'cardinality')[index]
       
    33             if card in '+*':
       
    34                 return card
       
    35     return '1'
       
    36 
       
    37 
       
    38 class RelationTags(object):
       
    39     
       
    40     MODE_TAGS = frozenset(('link', 'create'))
       
    41     CATEGORY_TAGS = frozenset(('primary', 'secondary', 'generic', 'generated',
       
    42                                'inlineview'))
       
    43 
       
    44     def __init__(self, eclass, tagdefs):
       
    45         self.eclass = eclass
       
    46         self._tagdefs = {}
       
    47         for relation, tags in tagdefs.iteritems():
       
    48             # tags must become a set
       
    49             if isinstance(tags, basestring):
       
    50                 tags = set((tags,))
       
    51             elif not isinstance(tags, set):
       
    52                 tags = set(tags)
       
    53             # relation must become a 3-uple (rtype, targettype, role)
       
    54             if isinstance(relation, basestring):
       
    55                 self._tagdefs[(relation, '*', 'subject')] = tags
       
    56                 self._tagdefs[(relation, '*', 'object')] = tags
       
    57             elif len(relation) == 1: # useful ?
       
    58                 self._tagdefs[(relation[0], '*', 'subject')] = tags
       
    59                 self._tagdefs[(relation[0], '*', 'object')] = tags
       
    60             elif len(relation) == 2:
       
    61                 rtype, ttype = relation
       
    62                 ttype = bw_normalize_etype(ttype) # XXX bw compat
       
    63                 self._tagdefs[rtype, ttype, 'subject'] = tags
       
    64                 self._tagdefs[rtype, ttype, 'object'] = tags
       
    65             elif len(relation) == 3:
       
    66                 relation = list(relation)  # XXX bw compat
       
    67                 relation[1] = bw_normalize_etype(relation[1])
       
    68                 self._tagdefs[tuple(relation)] = tags
       
    69             else:
       
    70                 raise ValueError('bad rtag definition (%r)' % (relation,))
       
    71         
       
    72 
       
    73     def __initialize__(self):
       
    74         # eclass.[*]schema are only set when registering
       
    75         self.schema = self.eclass.schema
       
    76         eschema = self.eschema = self.eclass.e_schema
       
    77         rtags = self._tagdefs
       
    78         # expand wildcards in rtags and add automatic tags
       
    79         for rschema, tschemas, role in sorted(eschema.relation_definitions(True)):
       
    80             rtype = rschema.type
       
    81             star_tags = rtags.pop((rtype, '*', role), set())
       
    82             for tschema in tschemas:
       
    83                 tags = rtags.setdefault((rtype, tschema.type, role), set(star_tags))
       
    84                 if role == 'subject':
       
    85                     X, Y = eschema, tschema
       
    86                     card = rschema.rproperty(X, Y, 'cardinality')[0]
       
    87                     composed = rschema.rproperty(X, Y, 'composite') == 'object'
       
    88                 else:
       
    89                     X, Y = tschema, eschema
       
    90                     card = rschema.rproperty(X, Y, 'cardinality')[1]
       
    91                     composed = rschema.rproperty(X, Y, 'composite') == 'subject'
       
    92                 # set default category tags if needed
       
    93                 if not tags & self.CATEGORY_TAGS:
       
    94                     if card in '1+':
       
    95                         if not rschema.is_final() and composed:
       
    96                             category = 'generated'
       
    97                         elif rschema.is_final() and (
       
    98                             rschema.type.endswith('_format')
       
    99                             or rschema.type.endswith('_encoding')):
       
   100                             category = 'generated'
       
   101                         else:
       
   102                             category = 'primary'
       
   103                     elif rschema.is_final():
       
   104                         if (rschema.type.endswith('_format')
       
   105                             or rschema.type.endswith('_encoding')):
       
   106                             category = 'generated'
       
   107                         else:
       
   108                             category = 'secondary'
       
   109                     else: 
       
   110                         category = 'generic'
       
   111                     tags.add(category)
       
   112                 if not tags & self.MODE_TAGS:
       
   113                     if card in '?1':
       
   114                         # by default, suppose link mode if cardinality doesn't allow
       
   115                         # more than one relation
       
   116                         mode = 'link'
       
   117                     elif rschema.rproperty(X, Y, 'composite') == role:
       
   118                         # if self is composed of the target type, create mode
       
   119                         mode = 'create'
       
   120                     else:
       
   121                         # link mode by default
       
   122                         mode = 'link'
       
   123                     tags.add(mode)
       
   124 
       
   125     def _default_target(self, rschema, role='subject'):
       
   126         eschema = self.eschema
       
   127         if role == 'subject':
       
   128             return eschema.subject_relation(rschema).objects(eschema)[0]
       
   129         else:
       
   130             return eschema.object_relation(rschema).subjects(eschema)[0]
       
   131 
       
   132     # dict compat
       
   133     def __getitem__(self, key):
       
   134         if isinstance(key, basestring):
       
   135             key = (key,)
       
   136         return self.get_tags(*key)
       
   137 
       
   138     __contains__ = __getitem__
       
   139     
       
   140     def get_tags(self, rtype, targettype=None, role='subject'):
       
   141         rschema = self.schema.rschema(rtype)
       
   142         if targettype is None:
       
   143             tschema = self._default_target(rschema, role)
       
   144         else:
       
   145             tschema = self.schema.eschema(targettype)
       
   146         return self._tagdefs[(rtype, tschema.type, role)]
       
   147 
       
   148     __call__ = get_tags
       
   149     
       
   150     def get_mode(self, rtype, targettype=None, role='subject'):
       
   151         # XXX: should we make an assertion on rtype not being final ?
       
   152         # assert not rschema.is_final()
       
   153         tags = self.get_tags(rtype, targettype, role)
       
   154         # do not change the intersection order !
       
   155         modes = tags & self.MODE_TAGS
       
   156         assert len(modes) == 1
       
   157         return modes.pop()
       
   158 
       
   159     def get_category(self, rtype, targettype=None, role='subject'):
       
   160         tags = self.get_tags(rtype, targettype, role)
       
   161         categories = tags & self.CATEGORY_TAGS
       
   162         assert len(categories) == 1
       
   163         return categories.pop()
       
   164 
       
   165     def is_inlined(self, rtype, targettype=None, role='subject'):
       
   166         # return set(('primary', 'secondary')) & self.get_tags(rtype, targettype)
       
   167         return 'inlineview' in self.get_tags(rtype, targettype, role)
       
   168 
       
   169 
       
   170 class metaentity(autoselectors):
       
   171     """this metaclass sets the relation tags on the entity class
       
   172     and deals with the `widgets` attribute
       
   173     """
       
   174     def __new__(mcs, name, bases, classdict):
       
   175         # collect baseclass' rtags
       
   176         tagdefs = {}
       
   177         widgets = {}
       
   178         for base in bases:
       
   179             tagdefs.update(getattr(base, '__rtags__', {}))
       
   180             widgets.update(getattr(base, 'widgets', {}))
       
   181         # update with the class' own rtgas
       
   182         tagdefs.update(classdict.get('__rtags__', {}))
       
   183         widgets.update(classdict.get('widgets', {}))
       
   184         # XXX decide whether or not it's a good idea to replace __rtags__
       
   185         #     good point: transparent support for inheritance levels >= 2
       
   186         #     bad point: we loose the information of which tags are specific
       
   187         #                to this entity class
       
   188         classdict['__rtags__'] = tagdefs
       
   189         classdict['widgets'] = widgets
       
   190         eclass = super(metaentity, mcs).__new__(mcs, name, bases, classdict)
       
   191         # adds the "rtags" attribute
       
   192         eclass.rtags = RelationTags(eclass, tagdefs)
       
   193         return eclass
       
   194 
       
   195 
       
   196 class Entity(AppRsetObject, dict):
       
   197     """an entity instance has e_schema automagically set on
       
   198     the class and instances has access to their issuing cursor.
       
   199     
       
   200     A property is set for each attribute and relation on each entity's type
       
   201     class. Becare that among attributes, 'eid' is *NEITHER* stored in the
       
   202     dict containment (which acts as a cache for other attributes dynamically
       
   203     fetched)
       
   204 
       
   205     :type e_schema: `cubicweb.schema.EntitySchema`
       
   206     :ivar e_schema: the entity's schema
       
   207 
       
   208     :type rest_var: str
       
   209     :cvar rest_var: indicates which attribute should be used to build REST urls
       
   210                     If None is specified, the first non-meta attribute will
       
   211                     be used
       
   212                     
       
   213     :type skip_copy_for: list
       
   214     :cvar skip_copy_for: a list of relations that should be skipped when copying
       
   215                          this kind of entity. Note that some relations such
       
   216                          as composite relations or relations that have '?1' as object
       
   217                          cardinality
       
   218     """
       
   219     __metaclass__ = metaentity
       
   220     __registry__ = 'etypes'
       
   221     __registerer__ = id_registerer
       
   222     __selectors__ = (yes,)
       
   223     widgets = {}
       
   224     id = None
       
   225     e_schema = None
       
   226     eid = None
       
   227     rest_attr = None
       
   228     skip_copy_for = ()
       
   229 
       
   230     @classmethod
       
   231     def registered(cls, registry):
       
   232         """build class using descriptor at registration time"""
       
   233         assert cls.id is not None
       
   234         super(Entity, cls).registered(registry)
       
   235         if cls.id != 'Any':
       
   236             cls.__initialize__()
       
   237         return cls
       
   238                 
       
   239     MODE_TAGS = set(('link', 'create'))
       
   240     CATEGORY_TAGS = set(('primary', 'secondary', 'generic', 'generated')) # , 'metadata'))
       
   241     @classmethod
       
   242     def __initialize__(cls):
       
   243         """initialize a specific entity class by adding descriptors to access
       
   244         entity type's attributes and relations
       
   245         """
       
   246         etype = cls.id
       
   247         assert etype != 'Any', etype
       
   248         cls.e_schema = eschema = cls.schema.eschema(etype)
       
   249         for rschema, _ in eschema.attribute_definitions():
       
   250             if rschema.type == 'eid':
       
   251                 continue
       
   252             setattr(cls, rschema.type, Attribute(rschema.type))
       
   253         mixins = []
       
   254         for rschema, _, x in eschema.relation_definitions():
       
   255             if (rschema, x) in MI_REL_TRIGGERS:
       
   256                 mixin = MI_REL_TRIGGERS[(rschema, x)]
       
   257                 if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ?
       
   258                     mixins.append(mixin)
       
   259                 for iface in getattr(mixin, '__implements__', ()):
       
   260                     if not interface.implements(cls, iface):
       
   261                         interface.extend(cls, iface)
       
   262             if x == 'subject':
       
   263                 setattr(cls, rschema.type, SubjectRelation(rschema))
       
   264             else:
       
   265                 attr = 'reverse_%s' % rschema.type
       
   266                 setattr(cls, attr, ObjectRelation(rschema))
       
   267         if mixins:
       
   268             cls.__bases__ = tuple(mixins + [p for p in cls.__bases__ if not p is object])
       
   269             cls.debug('plugged %s mixins on %s', mixins, etype)
       
   270         cls.rtags.__initialize__()
       
   271     
       
   272     @classmethod
       
   273     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
       
   274                   settype=True, ordermethod='fetch_order'):
       
   275         """return a rql to fetch all entities of the class type"""
       
   276         restrictions = restriction or []
       
   277         if settype:
       
   278             restrictions.append('%s is %s' % (mainvar, cls.id))
       
   279         if fetchattrs is None:
       
   280             fetchattrs = cls.fetch_attrs
       
   281         selection = [mainvar]
       
   282         orderby = []
       
   283         # start from 26 to avoid possible conflicts with X
       
   284         varmaker = rqlvar_maker(index=26)
       
   285         cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
       
   286                                 orderby, restrictions, user, ordermethod)
       
   287         rql = 'Any %s' % ','.join(selection)
       
   288         if orderby:
       
   289             rql +=  ' ORDERBY %s' % ','.join(orderby)
       
   290         rql += ' WHERE %s' % ', '.join(restrictions)
       
   291         return rql
       
   292     
       
   293     @classmethod
       
   294     def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
       
   295                             selection, orderby, restrictions, user,
       
   296                             ordermethod='fetch_order', visited=None):
       
   297         eschema = cls.e_schema
       
   298         if visited is None:
       
   299             visited = set((eschema.type,))
       
   300         elif eschema.type in visited:
       
   301             # avoid infinite recursion
       
   302             return
       
   303         else:
       
   304             visited.add(eschema.type)
       
   305         _fetchattrs = []
       
   306         for attr in fetchattrs:
       
   307             try:
       
   308                 rschema = eschema.subject_relation(attr)
       
   309             except KeyError:
       
   310                 cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
       
   311                             attr, cls.id)
       
   312                 continue
       
   313             if not user.matching_groups(rschema.get_groups('read')):
       
   314                 continue
       
   315             var = varmaker.next()
       
   316             selection.append(var)
       
   317             restriction = '%s %s %s' % (mainvar, attr, var)
       
   318             restrictions.append(restriction)
       
   319             if not rschema.is_final():
       
   320                 # XXX this does not handle several destination types
       
   321                 desttype = rschema.objects(eschema.type)[0]
       
   322                 card = rschema.rproperty(eschema, desttype, 'cardinality')[0]
       
   323                 if card not in '?1':
       
   324                     selection.pop()
       
   325                     restrictions.pop()
       
   326                     continue
       
   327                 if card == '?':
       
   328                     restrictions[-1] += '?' # left outer join if not mandatory
       
   329                 destcls = cls.vreg.etype_class(desttype)
       
   330                 destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
       
   331                                             selection, orderby, restrictions,
       
   332                                             user, ordermethod, visited=visited)
       
   333             orderterm = getattr(cls, ordermethod)(attr, var)
       
   334             if orderterm:
       
   335                 orderby.append(orderterm)
       
   336         return selection, orderby, restrictions
       
   337 
       
   338     def __init__(self, req, rset, row=None, col=0):
       
   339         AppRsetObject.__init__(self, req, rset)
       
   340         dict.__init__(self)
       
   341         self.row, self.col = row, col
       
   342         self._related_cache = {}
       
   343         if rset is not None:
       
   344             self.eid = rset[row][col]
       
   345         else:
       
   346             self.eid = None
       
   347         self._is_saved = True
       
   348         
       
   349     def __repr__(self):
       
   350         return '<Entity %s %s %s at %s>' % (
       
   351             self.e_schema, self.eid, self.keys(), id(self))
       
   352 
       
   353     def __nonzero__(self):
       
   354         return True
       
   355 
       
   356     def __hash__(self):
       
   357         return id(self)
       
   358 
       
   359     def pre_add_hook(self):
       
   360         """hook called by the repository before doing anything to add the entity
       
   361         (before_add entity hooks have not been called yet). This give the
       
   362         occasion to do weird stuff such as autocast (File -> Image for instance).
       
   363         
       
   364         This method must return the actual entity to be added.
       
   365         """
       
   366         return self
       
   367     
       
   368     def set_eid(self, eid):
       
   369         self.eid = self['eid'] = eid
       
   370 
       
   371     def has_eid(self):
       
   372         """return True if the entity has an attributed eid (False
       
   373         meaning that the entity has to be created
       
   374         """
       
   375         try:
       
   376             int(self.eid)
       
   377             return True
       
   378         except (ValueError, TypeError):
       
   379             return False
       
   380 
       
   381     def is_saved(self):
       
   382         """during entity creation, there is some time during which the entity
       
   383         has an eid attributed though it's not saved (eg during before_add_entity
       
   384         hooks). You can use this method to ensure the entity has an eid *and* is
       
   385         saved in its source.
       
   386         """
       
   387         return self.has_eid() and self._is_saved
       
   388     
       
   389     @cached
       
   390     def metainformation(self):
       
   391         res = dict(zip(('type', 'source', 'extid'), self.req.describe(self.eid)))
       
   392         res['source'] = self.req.source_defs()[res['source']]
       
   393         return res
       
   394 
       
   395     def clear_local_perm_cache(self, action):
       
   396         for rqlexpr in self.e_schema.get_rqlexprs(action):
       
   397             self.req.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
       
   398 
       
   399     def check_perm(self, action):
       
   400         self.e_schema.check_perm(self.req, action, self.eid)
       
   401 
       
   402     def has_perm(self, action):
       
   403         return self.e_schema.has_perm(self.req, action, self.eid)
       
   404         
       
   405     def view(self, vid, __registry='views', **kwargs):
       
   406         """shortcut to apply a view on this entity"""
       
   407         return self.vreg.render(__registry, vid, self.req, rset=self.rset,
       
   408                                 row=self.row, col=self.col, **kwargs)
       
   409 
       
   410     def absolute_url(self, method=None, **kwargs):
       
   411         """return an absolute url to view this entity"""
       
   412         # in linksearch mode, we don't want external urls else selecting
       
   413         # the object for use in the relation is tricky
       
   414         # XXX search_state is web specific
       
   415         if getattr(self.req, 'search_state', ('normal',))[0] == 'normal':
       
   416             kwargs['base_url'] = self.metainformation()['source'].get('base-url')
       
   417         if method is None or method == 'view':
       
   418             kwargs['_restpath'] = self.rest_path()
       
   419         else:
       
   420             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
       
   421         return self.build_url(method, **kwargs)
       
   422 
       
   423     def rest_path(self):
       
   424         """returns a REST-like (relative) path for this entity"""
       
   425         mainattr, needcheck = self._rest_attr_info()
       
   426         etype = str(self.e_schema)
       
   427         if mainattr == 'eid':
       
   428             value = self.eid
       
   429         else:
       
   430             value = getattr(self, mainattr)
       
   431             if value is None:
       
   432                 return '%s/eid/%s' % (etype.lower(), self.eid)
       
   433         if needcheck:
       
   434             # make sure url is not ambiguous
       
   435             rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (etype, mainattr)
       
   436             if value is not None:
       
   437                 nbresults = self.req.execute(rql, {'value' : value})[0][0]
       
   438                 # may an assertion that nbresults is not 0 would be a good idea
       
   439                 if nbresults != 1: # no ambiguity
       
   440                     return '%s/eid/%s' % (etype.lower(), self.eid)
       
   441         return '%s/%s' % (etype.lower(), self.req.url_quote(value))
       
   442 
       
   443     @classmethod
       
   444     def _rest_attr_info(cls):
       
   445         mainattr, needcheck = 'eid', True
       
   446         if cls.rest_attr:
       
   447             mainattr = cls.rest_attr
       
   448             needcheck = not cls.e_schema.has_unique_values(mainattr)
       
   449         else:
       
   450             for rschema in cls.e_schema.subject_relations():
       
   451                 if rschema.is_final() and rschema != 'eid' and cls.e_schema.has_unique_values(rschema):
       
   452                     mainattr = str(rschema)
       
   453                     needcheck = False
       
   454                     break
       
   455         if mainattr == 'eid':
       
   456             needcheck = False
       
   457         return mainattr, needcheck
       
   458 
       
   459     @cached
       
   460     def formatted_attrs(self):
       
   461         """returns the list of attributes which have some format information
       
   462         (i.e. rich text strings)
       
   463         """
       
   464         attrs = []
       
   465         for rschema, attrschema in self.e_schema.attribute_definitions():
       
   466             if attrschema.type == 'String' and self.has_format(rschema):
       
   467                 attrs.append(rschema.type)
       
   468         return attrs
       
   469         
       
   470     def format(self, attr):
       
   471         """return the mime type format for an attribute (if specified)"""
       
   472         return getattr(self, '%s_format' % attr, None)
       
   473     
       
   474     def text_encoding(self, attr):
       
   475         """return the text encoding for an attribute, default to site encoding
       
   476         """
       
   477         encoding = getattr(self, '%s_encoding' % attr, None)
       
   478         return encoding or self.vreg.property_value('ui.encoding')
       
   479 
       
   480     def has_format(self, attr):
       
   481         """return true if this entity's schema has a format field for the given
       
   482         attribute
       
   483         """
       
   484         return self.e_schema.has_subject_relation('%s_format' % attr)
       
   485     
       
   486     def has_text_encoding(self, attr):
       
   487         """return true if this entity's schema has ab encoding field for the
       
   488         given attribute
       
   489         """
       
   490         return self.e_schema.has_subject_relation('%s_encoding' % attr)
       
   491 
       
   492     def printable_value(self, attr, value=_marker, attrtype=None,
       
   493                         format='text/html', displaytime=True):
       
   494         """return a displayable value (i.e. unicode string) which may contains
       
   495         html tags
       
   496         """
       
   497         attr = str(attr)
       
   498         if value is _marker:
       
   499             value = getattr(self, attr)
       
   500         if isinstance(value, basestring):
       
   501             value = value.strip()
       
   502         if value is None or value == '': # don't use "not", 0 is an acceptable value
       
   503             return u''
       
   504         if attrtype is None:
       
   505             attrtype = self.e_schema.destination(attr)
       
   506         props = self.e_schema.rproperties(attr)
       
   507         if attrtype == 'String':
       
   508             # internalinalized *and* formatted string such as schema
       
   509             # description...
       
   510             if props.get('internationalizable'):
       
   511                 value = self.req._(value)
       
   512             attrformat = self.format(attr)
       
   513             if attrformat:
       
   514                 return self.mtc_transform(value, attrformat, format,
       
   515                                           self.req.encoding)
       
   516         elif attrtype == 'Bytes':
       
   517             attrformat = self.format(attr)
       
   518             if attrformat:
       
   519                 try:
       
   520                     encoding = getattr(self, '%s_encoding' % attr)
       
   521                 except AttributeError:
       
   522                     encoding = self.req.encoding
       
   523                 return self.mtc_transform(value.getvalue(), attrformat, format,
       
   524                                           encoding)
       
   525             return u''
       
   526         value = printable_value(self.req, attrtype, value, props, displaytime)
       
   527         if format == 'text/html':
       
   528             value = html_escape(value)
       
   529         return value
       
   530 
       
   531     def mtc_transform(self, data, format, target_format, encoding,
       
   532                       _engine=ENGINE):
       
   533         trdata = TransformData(data, format, encoding, appobject=self)
       
   534         data = _engine.convert(trdata, target_format).decode()
       
   535         if format == 'text/html':
       
   536             data = soup2xhtml(data, self.req.encoding)                
       
   537         return data
       
   538     
       
   539     # entity cloning ##########################################################
       
   540 
       
   541     def copy_relations(self, ceid):
       
   542         """copy relations of the object with the given eid on this object
       
   543 
       
   544         By default meta and composite relations are skipped.
       
   545         Overrides this if you want another behaviour
       
   546         """
       
   547         assert self.has_eid()
       
   548         execute = self.req.execute
       
   549         for rschema in self.e_schema.subject_relations():
       
   550             if rschema.meta or rschema.is_final():
       
   551                 continue
       
   552             # skip already defined relations
       
   553             if getattr(self, rschema.type):
       
   554                 continue
       
   555             if rschema.type in self.skip_copy_for:
       
   556                 continue
       
   557             if rschema.type == 'in_state':
       
   558                 # if the workflow is defining an initial state (XXX AND we are
       
   559                 # not in the managers group? not done to be more consistent)
       
   560                 # don't try to copy in_state
       
   561                 if execute('Any S WHERE S state_of ET, ET initial_state S,'
       
   562                            'ET name %(etype)s', {'etype': str(self.e_schema)}):
       
   563                     continue
       
   564             # skip composite relation
       
   565             if self.e_schema.subjrproperty(rschema, 'composite'):
       
   566                 continue
       
   567             # skip relation with card in ?1 else we either change the copied
       
   568             # object (inlined relation) or inserting some inconsistency
       
   569             if self.e_schema.subjrproperty(rschema, 'cardinality')[1] in '?1':
       
   570                 continue
       
   571             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
       
   572                 rschema.type, rschema.type)
       
   573             execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
       
   574             self.clear_related_cache(rschema.type, 'subject')
       
   575         for rschema in self.e_schema.object_relations():
       
   576             if rschema.meta:
       
   577                 continue
       
   578             # skip already defined relations
       
   579             if getattr(self, 'reverse_%s' % rschema.type):
       
   580                 continue
       
   581             # skip composite relation
       
   582             if self.e_schema.objrproperty(rschema, 'composite'):
       
   583                 continue
       
   584             # skip relation with card in ?1 else we either change the copied
       
   585             # object (inlined relation) or inserting some inconsistency
       
   586             if self.e_schema.objrproperty(rschema, 'cardinality')[0] in '?1':
       
   587                 continue
       
   588             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
       
   589                 rschema.type, rschema.type)
       
   590             execute(rql, {'x': self.eid, 'y': ceid}, ('x', 'y'))
       
   591             self.clear_related_cache(rschema.type, 'object')
       
   592 
       
   593     # data fetching methods ###################################################
       
   594 
       
   595     @cached
       
   596     def as_rset(self):
       
   597         """returns a resultset containing `self` information"""
       
   598         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
       
   599                          {'x': self.eid}, [(self.id,)])
       
   600         return self.req.decorate_rset(rset)
       
   601                        
       
   602     def to_complete_relations(self):
       
   603         """by default complete final relations to when calling .complete()"""
       
   604         for rschema in self.e_schema.subject_relations():
       
   605             if rschema.is_final():
       
   606                 continue
       
   607             if len(rschema.objects(self.e_schema)) > 1:
       
   608                 # ambigous relations, the querier doesn't handle
       
   609                 # outer join correctly in this case
       
   610                 continue
       
   611             if rschema.inlined:
       
   612                 matching_groups = self.req.user.matching_groups
       
   613                 if matching_groups(rschema.get_groups('read')) and \
       
   614                    all(matching_groups(es.get_groups('read'))
       
   615                        for es in rschema.objects(self.e_schema)):
       
   616                     yield rschema, 'subject'
       
   617                     
       
   618     def to_complete_attributes(self, skip_bytes=True):
       
   619         for rschema, attrschema in self.e_schema.attribute_definitions():
       
   620             # skip binary data by default
       
   621             if skip_bytes and attrschema.type == 'Bytes':
       
   622                 continue
       
   623             attr = rschema.type
       
   624             if attr == 'eid':
       
   625                 continue
       
   626             # password retreival is blocked at the repository server level
       
   627             if not self.req.user.matching_groups(rschema.get_groups('read')) \
       
   628                    or attrschema.type == 'Password':
       
   629                 self[attr] = None
       
   630                 continue
       
   631             yield attr
       
   632             
       
   633     def complete(self, attributes=None, skip_bytes=True):
       
   634         """complete this entity by adding missing attributes (i.e. query the
       
   635         repository to fill the entity)
       
   636 
       
   637         :type skip_bytes: bool
       
   638         :param skip_bytes:
       
   639           if true, attribute of type Bytes won't be considered
       
   640         """
       
   641         assert self.has_eid()
       
   642         varmaker = rqlvar_maker()
       
   643         V = varmaker.next()
       
   644         rql = ['WHERE %s eid %%(x)s' % V]
       
   645         selected = []
       
   646         for attr in (attributes or self.to_complete_attributes(skip_bytes)):
       
   647             # if attribute already in entity, nothing to do
       
   648             if self.has_key(attr):
       
   649                 continue
       
   650             # case where attribute must be completed, but is not yet in entity
       
   651             var = varmaker.next()
       
   652             rql.append('%s %s %s' % (V, attr, var))
       
   653             selected.append((attr, var))
       
   654         # +1 since this doen't include the main variable
       
   655         lastattr = len(selected) + 1
       
   656         if attributes is None:
       
   657             # fetch additional relations (restricted to 0..1 relations)
       
   658             for rschema, role in self.to_complete_relations():
       
   659                 rtype = rschema.type
       
   660                 if self.relation_cached(rtype, role):
       
   661                     continue
       
   662                 var = varmaker.next()
       
   663                 if role == 'subject':
       
   664                     targettype = rschema.objects(self.e_schema)[0]
       
   665                     card = rschema.rproperty(self.e_schema, targettype,
       
   666                                              'cardinality')[0]
       
   667                     if card == '1':
       
   668                         rql.append('%s %s %s' % (V, rtype, var))
       
   669                     else: # '?"
       
   670                         rql.append('%s %s %s?' % (V, rtype, var))
       
   671                 else:
       
   672                     targettype = rschema.subjects(self.e_schema)[1]
       
   673                     card = rschema.rproperty(self.e_schema, targettype,
       
   674                                              'cardinality')[1]
       
   675                     if card == '1':
       
   676                         rql.append('%s %s %s' % (var, rtype, V))
       
   677                     else: # '?"
       
   678                         rql.append('%s? %s %s' % (var, rtype, V))
       
   679                 assert card in '1?', '%s %s %s %s' % (self.e_schema, rtype,
       
   680                                                       role, card)
       
   681                 selected.append(((rtype, role), var))
       
   682         if selected:
       
   683             # select V, we need it as the left most selected variable
       
   684             # if some outer join are included to fetch inlined relations
       
   685             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
       
   686                                     ','.join(rql))
       
   687             execute = getattr(self.req, 'unsafe_execute', self.req.execute)
       
   688             rset = execute(rql, {'x': self.eid}, 'x', build_descr=False)[0]
       
   689             # handle attributes
       
   690             for i in xrange(1, lastattr):
       
   691                 self[str(selected[i-1][0])] = rset[i]
       
   692             # handle relations
       
   693             for i in xrange(lastattr, len(rset)):
       
   694                 rtype, x = selected[i-1][0]
       
   695                 value = rset[i]
       
   696                 if value is None:
       
   697                     rrset = ResultSet([], rql, {'x': self.eid})
       
   698                     self.req.decorate_rset(rrset)
       
   699                 else:
       
   700                     rrset = self.req.eid_rset(value)
       
   701                 self.set_related_cache(rtype, x, rrset)
       
   702                 
       
   703     def get_value(self, name):
       
   704         """get value for the attribute relation <name>, query the repository
       
   705         to get the value if necessary.
       
   706 
       
   707         :type name: str
       
   708         :param name: name of the attribute to get
       
   709         """
       
   710         try:
       
   711             value = self[name]
       
   712         except KeyError:
       
   713             if not self.is_saved():
       
   714                 return None
       
   715             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
       
   716             # XXX should we really use unsafe_execute here??
       
   717             execute = getattr(self.req, 'unsafe_execute', self.req.execute)
       
   718             try:
       
   719                 rset = execute(rql, {'x': self.eid}, 'x')
       
   720             except Unauthorized:
       
   721                 self[name] = value = None
       
   722             else:
       
   723                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
       
   724                 try:
       
   725                     self[name] = value = rset.rows[0][0]
       
   726                 except IndexError:
       
   727                     # probably a multisource error
       
   728                     self.critical("can't get value for attribute %s of entity with eid %s",
       
   729                                   name, self.eid)
       
   730                     if self.e_schema.destination(name) == 'String':
       
   731                         self[name] = value = self.req._('unaccessible')
       
   732                     else:
       
   733                         self[name] = value = None
       
   734         return value
       
   735 
       
   736     def related(self, rtype, role='subject', limit=None, entities=False):
       
   737         """returns a resultset of related entities
       
   738         
       
   739         :param role: is the role played by 'self' in the relation ('subject' or 'object')
       
   740         :param limit: resultset's maximum size
       
   741         :param entities: if True, the entites are returned; if False, a result set is returned
       
   742         """
       
   743         try:
       
   744             return self.related_cache(rtype, role, entities, limit)
       
   745         except KeyError:
       
   746             pass
       
   747         assert self.has_eid()
       
   748         rql = self.related_rql(rtype, role)
       
   749         rset = self.req.execute(rql, {'x': self.eid}, 'x')
       
   750         self.set_related_cache(rtype, role, rset)
       
   751         return self.related(rtype, role, limit, entities)
       
   752 
       
   753     def related_rql(self, rtype, role='subject'):
       
   754         rschema = self.schema[rtype]
       
   755         if role == 'subject':
       
   756             targettypes = rschema.objects(self.e_schema)
       
   757             restriction = 'E eid %%(x)s, E %s X' % rtype
       
   758             card = greater_card(rschema, (self.e_schema,), targettypes, 0)
       
   759         else:
       
   760             targettypes = rschema.subjects(self.e_schema)
       
   761             restriction = 'E eid %%(x)s, X %s E' % rtype
       
   762             card = greater_card(rschema, targettypes, (self.e_schema,), 1)
       
   763         if len(targettypes) > 1:
       
   764             fetchattrs_list = []
       
   765             for ttype in targettypes:
       
   766                 etypecls = self.vreg.etype_class(ttype)
       
   767                 fetchattrs_list.append(set(etypecls.fetch_attrs))
       
   768             fetchattrs = reduce(set.intersection, fetchattrs_list)
       
   769             rql = etypecls.fetch_rql(self.req.user, [restriction], fetchattrs,
       
   770                                      settype=False)
       
   771         else:
       
   772             etypecls = self.vreg.etype_class(targettypes[0])
       
   773             rql = etypecls.fetch_rql(self.req.user, [restriction], settype=False)
       
   774         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
       
   775         # greater_card return 1 for those both cases)
       
   776         if card == '1':
       
   777             if ' ORDERBY ' in rql:
       
   778                 rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
       
   779                                        rql.split(' WHERE ', 1)[1])
       
   780         elif not ' ORDERBY ' in rql:
       
   781             args = tuple(rql.split(' WHERE ', 1))
       
   782             rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % args
       
   783         return rql
       
   784     
       
   785     # generic vocabulary methods ##############################################
       
   786 
       
   787     def vocabulary(self, rtype, role='subject', limit=None):
       
   788         """vocabulary functions must return a list of couples
       
   789         (label, eid) that will typically be used to fill the
       
   790         edition view's combobox.
       
   791         
       
   792         If `eid` is None in one of these couples, it should be
       
   793         interpreted as a separator in case vocabulary results are grouped
       
   794         """
       
   795         try:
       
   796             vocabfunc = getattr(self, '%s_%s_vocabulary' % (role, rtype))
       
   797         except AttributeError:
       
   798             vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
       
   799         # NOTE: it is the responsibility of `vocabfunc` to sort the result
       
   800         #       (direclty through RQL or via a python sort). This is also
       
   801         #       important because `vocabfunc` might return a list with
       
   802         #       couples (label, None) which act as separators. In these
       
   803         #       cases, it doesn't make sense to sort results afterwards.
       
   804         return vocabfunc(rtype, limit)
       
   805             
       
   806     def subject_relation_vocabulary(self, rtype, limit=None):
       
   807         """defaut vocabulary method for the given relation, looking for
       
   808         relation's object entities (i.e. self is the subject)
       
   809         """
       
   810         if isinstance(rtype, basestring):
       
   811             rtype = self.schema.rschema(rtype)
       
   812         done = None
       
   813         assert not rtype.is_final(), rtype
       
   814         if self.has_eid():
       
   815             done = set(e.eid for e in getattr(self, str(rtype)))
       
   816         result = []
       
   817         rsetsize = None
       
   818         for objtype in rtype.objects(self.e_schema):
       
   819             if limit is not None:
       
   820                 rsetsize = limit - len(result)
       
   821             result += self.relation_vocabulary(rtype, objtype, 'subject',
       
   822                                                rsetsize, done)
       
   823             if limit is not None and len(result) >= limit:
       
   824                 break
       
   825         return result
       
   826 
       
   827     def object_relation_vocabulary(self, rtype, limit=None):
       
   828         """defaut vocabulary method for the given relation, looking for
       
   829         relation's subject entities (i.e. self is the object)
       
   830         """
       
   831         if isinstance(rtype, basestring):
       
   832             rtype = self.schema.rschema(rtype)
       
   833         done = None
       
   834         if self.has_eid():
       
   835             done = set(e.eid for e in getattr(self, 'reverse_%s' % rtype))
       
   836         result = []
       
   837         rsetsize = None
       
   838         for subjtype in rtype.subjects(self.e_schema):
       
   839             if limit is not None:
       
   840                 rsetsize = limit - len(result)
       
   841             result += self.relation_vocabulary(rtype, subjtype, 'object',
       
   842                                                rsetsize, done)
       
   843             if limit is not None and len(result) >= limit:
       
   844                 break
       
   845         return result
       
   846 
       
   847     def relation_vocabulary(self, rtype, targettype, role,
       
   848                             limit=None, done=None):
       
   849         if done is None:
       
   850             done = set()
       
   851         req = self.req
       
   852         rset = self.unrelated(rtype, targettype, role, limit)
       
   853         res = []
       
   854         for entity in rset.entities():
       
   855             if entity.eid in done:
       
   856                 continue
       
   857             done.add(entity.eid)
       
   858             res.append((entity.view('combobox'), entity.eid))
       
   859         return res
       
   860 
       
   861     def unrelated_rql(self, rtype, targettype, role, ordermethod=None,
       
   862                       vocabconstraints=True):
       
   863         """build a rql to fetch `targettype` entities unrelated to this entity
       
   864         using (rtype, role) relation
       
   865         """
       
   866         ordermethod = ordermethod or 'fetch_unrelated_order'
       
   867         if isinstance(rtype, basestring):
       
   868             rtype = self.schema.rschema(rtype)
       
   869         if role == 'subject':
       
   870             evar, searchedvar = 'S', 'O'
       
   871             subjtype, objtype = self.e_schema, targettype
       
   872         else:
       
   873             searchedvar, evar = 'S', 'O'
       
   874             objtype, subjtype = self.e_schema, targettype
       
   875         if self.has_eid():
       
   876             restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar]
       
   877         else:
       
   878             restriction = []
       
   879         constraints = rtype.rproperty(subjtype, objtype, 'constraints')
       
   880         if vocabconstraints:
       
   881             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
       
   882             # will be included as well
       
   883             restriction += [cstr.restriction for cstr in constraints
       
   884                             if isinstance(cstr, RQLVocabularyConstraint)]
       
   885         else:
       
   886             restriction += [cstr.restriction for cstr in constraints
       
   887                             if isinstance(cstr, RQLConstraint)]
       
   888         etypecls = self.vreg.etype_class(targettype)
       
   889         rql = etypecls.fetch_rql(self.req.user, restriction,
       
   890                                  mainvar=searchedvar, ordermethod=ordermethod)
       
   891         # ensure we have an order defined
       
   892         if not ' ORDERBY ' in rql:
       
   893             before, after = rql.split(' WHERE ', 1)
       
   894             rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
       
   895         return rql
       
   896     
       
   897     def unrelated(self, rtype, targettype, role='subject', limit=None,
       
   898                   ordermethod=None):
       
   899         """return a result set of target type objects that may be related
       
   900         by a given relation, with self as subject or object
       
   901         """
       
   902         rql = self.unrelated_rql(rtype, targettype, role, ordermethod)
       
   903         if limit is not None:
       
   904             before, after = rql.split(' WHERE ', 1)
       
   905             rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
       
   906         if self.has_eid():
       
   907             return self.req.execute(rql, {'x': self.eid})
       
   908         return self.req.execute(rql)
       
   909         
       
   910     # relations cache handling ################################################
       
   911     
       
   912     def relation_cached(self, rtype, role):
       
   913         """return true if the given relation is already cached on the instance
       
   914         """
       
   915         return '%s_%s' % (rtype, role) in self._related_cache
       
   916     
       
   917     def related_cache(self, rtype, role, entities=True, limit=None):
       
   918         """return values for the given relation if it's cached on the instance,
       
   919         else raise `KeyError`
       
   920         """
       
   921         res = self._related_cache['%s_%s' % (rtype, role)][entities]
       
   922         if limit:
       
   923             if entities:
       
   924                 res = res[:limit]
       
   925             else:
       
   926                 res = res.limit(limit)
       
   927         return res
       
   928     
       
   929     def set_related_cache(self, rtype, role, rset, col=0):
       
   930         """set cached values for the given relation"""
       
   931         if rset:
       
   932             related = list(rset.entities(col))
       
   933             rschema = self.schema.rschema(rtype)
       
   934             if role == 'subject':
       
   935                 rcard = rschema.rproperty(self.e_schema, related[0].e_schema,
       
   936                                           'cardinality')[1]
       
   937                 target = 'object'
       
   938             else:
       
   939                 rcard = rschema.rproperty(related[0].e_schema, self.e_schema,
       
   940                                           'cardinality')[0]
       
   941                 target = 'subject'
       
   942             if rcard in '?1':
       
   943                 for rentity in related:
       
   944                     rentity._related_cache['%s_%s' % (rtype, target)] = (self.as_rset(), [self])
       
   945         else:
       
   946             related = []
       
   947         self._related_cache['%s_%s' % (rtype, role)] = (rset, related)
       
   948         
       
   949     def clear_related_cache(self, rtype=None, role=None):
       
   950         """clear cached values for the given relation or the entire cache if
       
   951         no relation is given
       
   952         """
       
   953         if rtype is None:
       
   954             self._related_cache = {}
       
   955         else:
       
   956             assert role
       
   957             self._related_cache.pop('%s_%s' % (rtype, role), None)
       
   958         
       
   959     # raw edition utilities ###################################################
       
   960     
       
   961     def set_attributes(self, **kwargs):
       
   962         assert kwargs
       
   963         relations = []
       
   964         for key in kwargs:
       
   965             relations.append('X %s %%(%s)s' % (key, key))
       
   966         # update current local object
       
   967         self.update(kwargs)
       
   968         # and now update the database
       
   969         kwargs['x'] = self.eid
       
   970         self.req.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
       
   971                          kwargs, 'x')
       
   972             
       
   973     def delete(self):
       
   974         assert self.has_eid(), self.eid
       
   975         self.req.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
       
   976                          {'x': self.eid})
       
   977     
       
   978     # server side utilities ###################################################
       
   979         
       
   980     def set_defaults(self):
       
   981         """set default values according to the schema"""
       
   982         self._default_set = set()
       
   983         for attr, value in self.e_schema.defaults():
       
   984             if not self.has_key(attr):
       
   985                 self[str(attr)] = value
       
   986                 self._default_set.add(attr)
       
   987 
       
   988     def check(self, creation=False):
       
   989         """check this entity against its schema. Only final relation
       
   990         are checked here, constraint on actual relations are checked in hooks
       
   991         """
       
   992         # necessary since eid is handled specifically and yams require it to be
       
   993         # in the dictionary
       
   994         if self.req is None:
       
   995             _ = unicode
       
   996         else:
       
   997             _ = self.req._
       
   998         self.e_schema.check(self, creation=creation, _=_)
       
   999 
       
  1000     def fti_containers(self, _done=None):
       
  1001         if _done is None:
       
  1002             _done = set()
       
  1003         _done.add(self.eid)
       
  1004         containers = tuple(self.e_schema.fulltext_containers())
       
  1005         if containers:
       
  1006             yielded = False
       
  1007             for rschema, target in containers:
       
  1008                 if target == 'object':
       
  1009                     targets = getattr(self, rschema.type)
       
  1010                 else:
       
  1011                     targets = getattr(self, 'reverse_%s' % rschema)
       
  1012                 for entity in targets:
       
  1013                     if entity.eid in _done:
       
  1014                         continue
       
  1015                     for container in entity.fti_containers(_done):
       
  1016                         yield container
       
  1017                         yielded = True
       
  1018             if not yielded:
       
  1019                 yield self
       
  1020         else:
       
  1021             yield self
       
  1022                     
       
  1023     def get_words(self):
       
  1024         """used by the full text indexer to get words to index
       
  1025 
       
  1026         this method should only be used on the repository side since it depends
       
  1027         on the indexer package
       
  1028         
       
  1029         :rtype: list
       
  1030         :return: the list of indexable word of this entity
       
  1031         """
       
  1032         from indexer.query_objects import tokenize
       
  1033         words = []
       
  1034         for rschema in self.e_schema.indexable_attributes():
       
  1035             try:
       
  1036                 value = self.printable_value(rschema, format='text/plain')
       
  1037             except TransformError, ex:
       
  1038                 continue
       
  1039             except:
       
  1040                 self.exception("can't add value of %s to text index for entity %s",
       
  1041                                rschema, self.eid)
       
  1042                 continue
       
  1043             if value:
       
  1044                 words += tokenize(value)
       
  1045         
       
  1046         for rschema, role in self.e_schema.fulltext_relations():
       
  1047             if role == 'subject':
       
  1048                 for entity in getattr(self, rschema.type):
       
  1049                     words += entity.get_words()
       
  1050             else: # if role == 'object':
       
  1051                 for entity in getattr(self, 'reverse_%s' % rschema.type):
       
  1052                     words += entity.get_words()
       
  1053         return words
       
  1054 
       
  1055 
       
  1056 # attribute and relation descriptors ##########################################
       
  1057 
       
  1058 class Attribute(object):
       
  1059     """descriptor that controls schema attribute access"""
       
  1060 
       
  1061     def __init__(self, attrname):
       
  1062         assert attrname != 'eid'
       
  1063         self._attrname = attrname
       
  1064 
       
  1065     def __get__(self, eobj, eclass):
       
  1066         if eobj is None:
       
  1067             return self
       
  1068         return eobj.get_value(self._attrname)
       
  1069 
       
  1070     def __set__(self, eobj, value):
       
  1071         # XXX bw compat
       
  1072         # would be better to generate UPDATE queries than the current behaviour
       
  1073         eobj.warning("deprecated usage, don't use 'entity.attr = val' notation)")
       
  1074         eobj[self._attrname] = value
       
  1075 
       
  1076 
       
  1077 class Relation(object):
       
  1078     """descriptor that controls schema relation access"""
       
  1079     _role = None # for pylint
       
  1080 
       
  1081     def __init__(self, rschema):
       
  1082         self._rschema = rschema
       
  1083         self._rtype = rschema.type
       
  1084 
       
  1085     def __get__(self, eobj, eclass):
       
  1086         if eobj is None:
       
  1087             raise AttributeError('%s cannot be only be accessed from instances'
       
  1088                                  % self._rtype)
       
  1089         return eobj.related(self._rtype, self._role, entities=True)
       
  1090     
       
  1091     def __set__(self, eobj, value):
       
  1092         raise NotImplementedError
       
  1093 
       
  1094 
       
  1095 class SubjectRelation(Relation):
       
  1096     """descriptor that controls schema relation access"""
       
  1097     _role = 'subject'
       
  1098     
       
  1099 class ObjectRelation(Relation):
       
  1100     """descriptor that controls schema relation access"""
       
  1101     _role = 'object'
       
  1102 
       
  1103 from logging import getLogger
       
  1104 from cubicweb import set_log_methods
       
  1105 set_log_methods(Entity, getLogger('cubicweb.entity'))