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