entity.py
branchstable
changeset 8124 acc23c284432
parent 7995 9a9f35ef418c
child 8190 2a3c1b787688
child 8199 fb5c0e60a615
equal deleted inserted replaced
8118:7b2c7f3d3703 8124:acc23c284432
    25 from logilab.common.decorators import cached
    25 from logilab.common.decorators import cached
    26 from logilab.common.deprecation import deprecated
    26 from logilab.common.deprecation import deprecated
    27 from logilab.mtconverter import TransformData, TransformError, xml_escape
    27 from logilab.mtconverter import TransformData, TransformError, xml_escape
    28 
    28 
    29 from rql.utils import rqlvar_maker
    29 from rql.utils import rqlvar_maker
       
    30 from rql.stmts import Select
       
    31 from rql.nodes import (Not, VariableRef, Constant, make_relation,
       
    32                        Relation as RqlRelation)
    30 
    33 
    31 from cubicweb import Unauthorized, typed_eid, neg_role
    34 from cubicweb import Unauthorized, typed_eid, neg_role
       
    35 from cubicweb.utils import support_args
    32 from cubicweb.rset import ResultSet
    36 from cubicweb.rset import ResultSet
    33 from cubicweb.selectors import yes
    37 from cubicweb.selectors import yes
    34 from cubicweb.appobject import AppObject
    38 from cubicweb.appobject import AppObject
    35 from cubicweb.req import _check_cw_unsafe
    39 from cubicweb.req import _check_cw_unsafe
    36 from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
    40 from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
       
    41                              GeneratedConstraint)
    37 from cubicweb.rqlrewrite import RQLRewriter
    42 from cubicweb.rqlrewrite import RQLRewriter
    38 
    43 
    39 from cubicweb.uilib import printable_value, soup2xhtml
    44 from cubicweb.uilib import soup2xhtml
    40 from cubicweb.mixins import MI_REL_TRIGGERS
    45 from cubicweb.mixins import MI_REL_TRIGGERS
    41 from cubicweb.mttransforms import ENGINE
    46 from cubicweb.mttransforms import ENGINE
    42 
    47 
    43 _marker = object()
    48 _marker = object()
    44 
    49 
    59     # behind Apache mod_proxy
    64     # behind Apache mod_proxy
    60     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
    65     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
    61         return False
    66         return False
    62     return True
    67     return True
    63 
    68 
    64 
    69 def rel_vars(rel):
    65 def remove_ambiguous_rels(attr_set, subjtypes, schema):
    70     return ((isinstance(rel.children[0], VariableRef)
    66     '''remove from `attr_set` the relations of entity types `subjtypes` that have
    71              and rel.children[0].variable or None),
    67     different entity type sets as target'''
    72             (isinstance(rel.children[1].children[0], VariableRef)
    68     for attr in attr_set.copy():
    73              and rel.children[1].children[0].variable or None)
    69         rschema = schema.rschema(attr)
    74             )
    70         if rschema.final:
    75 
       
    76 def rel_matches(rel, rtype, role, varname, operator='='):
       
    77     if rel.r_type == rtype and rel.children[1].operator == operator:
       
    78         same_role_var_idx = 0 if role == 'subject' else 1
       
    79         variables = rel_vars(rel)
       
    80         if variables[same_role_var_idx].name == varname:
       
    81             return variables[1 - same_role_var_idx]
       
    82 
       
    83 def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
       
    84                                  lt_infos, eidvars):
       
    85     """restrict vocabulary as much as possible in entity creation,
       
    86     based on infos provided by __linkto form param.
       
    87 
       
    88     Example based on following schema:
       
    89 
       
    90       class works_in(RelationDefinition):
       
    91           subject = 'CWUser'
       
    92           object = 'Lab'
       
    93           cardinality = '1*'
       
    94           constraints = [RQLConstraint('S in_group G, O welcomes G')]
       
    95 
       
    96       class welcomes(RelationDefinition):
       
    97           subject = 'Lab'
       
    98           object = 'CWGroup'
       
    99 
       
   100     If you create a CWUser in the "scientists" CWGroup you can show
       
   101     only the labs that welcome them using :
       
   102 
       
   103       lt_infos = {('in_group', 'subject'): 321}
       
   104 
       
   105     You get following restriction : 'O welcomes G, G eid 321'
       
   106 
       
   107     """
       
   108     st = cstr.snippet_rqlst.copy()
       
   109     # replace relations in ST by eid infos from linkto where possible
       
   110     for (info_rtype, info_role), eids in lt_infos.iteritems():
       
   111         eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
       
   112         for rel in st.iget_nodes(RqlRelation):
       
   113             targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
       
   114             if targetvar is not None:
       
   115                 if targetvar.name in eidvars:
       
   116                     rel.parent.remove(rel)
       
   117                 else:
       
   118                     eidrel = make_relation(
       
   119                         targetvar, 'eid', (targetvar.name, 'Substitute'),
       
   120                         Constant)
       
   121                     rel.parent.replace(rel, eidrel)
       
   122                     args[targetvar.name] = eid
       
   123                     eidvars.add(targetvar.name)
       
   124     # if modified ST still contains evar references we must discard the
       
   125     # constraint, otherwise evar is unknown in the final rql query which can
       
   126     # lead to a SQL table cartesian product and multiple occurences of solutions
       
   127     evarname = evar.name
       
   128     for rel in st.iget_nodes(RqlRelation):
       
   129         for variable in rel_vars(rel):
       
   130             if variable and evarname == variable.name:
       
   131                 return
       
   132     # else insert snippets into the global tree
       
   133     return GeneratedConstraint(st, cstr.mainvars - set(evarname))
       
   134 
       
   135 def pruned_lt_info(eschema, lt_infos):
       
   136     pruned = {}
       
   137     for (lt_rtype, lt_role), eids in lt_infos.iteritems():
       
   138         # we can only use lt_infos describing relation with a cardinality
       
   139         # of value 1 towards the linked entity
       
   140         if not len(eids) == 1:
    71             continue
   141             continue
    72         ttypes = None
   142         lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
    73         for subjtype in subjtypes:
   143             0 if lt_role == 'subject' else 1]
    74             cur_ttypes = rschema.objects(subjtype)
   144         if lt_card not in '?1':
    75             if ttypes is None:
   145             continue
    76                 ttypes = cur_ttypes
   146         pruned[(lt_rtype, lt_role)] = eids
    77             elif cur_ttypes != ttypes:
   147     return pruned
    78                 attr_set.remove(attr)
       
    79                 break
       
    80 
       
    81 
   148 
    82 class Entity(AppObject):
   149 class Entity(AppObject):
    83     """an entity instance has e_schema automagically set on
   150     """an entity instance has e_schema automagically set on
    84     the class and instances has access to their issuing cursor.
   151     the class and instances has access to their issuing cursor.
    85 
   152 
    89     fetched)
   156     fetched)
    90 
   157 
    91     :type e_schema: `cubicweb.schema.EntitySchema`
   158     :type e_schema: `cubicweb.schema.EntitySchema`
    92     :ivar e_schema: the entity's schema
   159     :ivar e_schema: the entity's schema
    93 
   160 
    94     :type rest_var: str
   161     :type rest_attr: str
    95     :cvar rest_var: indicates which attribute should be used to build REST urls
   162     :cvar rest_attr: indicates which attribute should be used to build REST urls
    96                     If None is specified, the first non-meta attribute will
   163        If `None` is specified (the default), the first unique attribute will
    97                     be used
   164        be used ('eid' if none found)
    98 
   165 
    99     :type skip_copy_for: list
   166     :type cw_skip_copy_for: list
   100     :cvar skip_copy_for: a list of relations that should be skipped when copying
   167     :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
   101                          this kind of entity. Note that some relations such
   168        that should be skipped when copying this kind of entity. Note that some
   102                          as composite relations or relations that have '?1' as object
   169        relations such as composite relations or relations that have '?1' as
   103                          cardinality are always skipped.
   170        object cardinality are always skipped.
   104     """
   171     """
   105     __registry__ = 'etypes'
   172     __registry__ = 'etypes'
   106     __select__ = yes()
   173     __select__ = yes()
   107 
   174 
   108     # class attributes that must be set in class definition
   175     # class attributes that must be set in class definition
   109     rest_attr = None
   176     rest_attr = None
   110     fetch_attrs = None
   177     fetch_attrs = None
   111     skip_copy_for = ('in_state',) # XXX turn into a set
   178     skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
       
   179     cw_skip_copy_for = [('in_state', 'subject')]
   112     # class attributes set automatically at registration time
   180     # class attributes set automatically at registration time
   113     e_schema = None
   181     e_schema = None
   114 
   182 
   115     @classmethod
   183     @classmethod
   116     def __initialize__(cls, schema):
   184     def __initialize__(cls, schema):
   151             mixins += cls.__bases__[1:]
   219             mixins += cls.__bases__[1:]
   152             cls.__bases__ = tuple(mixins)
   220             cls.__bases__ = tuple(mixins)
   153             cls.info('plugged %s mixins on %s', mixins, cls)
   221             cls.info('plugged %s mixins on %s', mixins, cls)
   154 
   222 
   155     fetch_attrs = ('modification_date',)
   223     fetch_attrs = ('modification_date',)
       
   224 
   156     @classmethod
   225     @classmethod
   157     def fetch_order(cls, attr, var):
   226     def cw_fetch_order(cls, select, attr, var):
   158         """class method used to control sort order when multiple entities of
   227         """This class method may be used to control sort order when multiple
   159         this type are fetched
   228         entities of this type are fetched through ORM methods. Its arguments
   160         """
   229         are:
   161         return cls.fetch_unrelated_order(attr, var)
   230 
       
   231         * `select`, the RQL syntax tree
       
   232 
       
   233         * `attr`, the attribute being watched
       
   234 
       
   235         * `var`, the variable through which this attribute's value may be
       
   236           accessed in the query
       
   237 
       
   238         When you want to do some sorting on the given attribute, you should
       
   239         modify the syntax tree accordingly. For instance:
       
   240 
       
   241         .. sourcecode:: python
       
   242 
       
   243           from rql import nodes
       
   244 
       
   245           class Version(AnyEntity):
       
   246               __regid__ = 'Version'
       
   247 
       
   248               fetch_attrs = ('num', 'description', 'in_state')
       
   249 
       
   250               @classmethod
       
   251               def cw_fetch_order(cls, select, attr, var):
       
   252                   if attr == 'num':
       
   253                       func = nodes.Function('version_sort_value')
       
   254                       func.append(nodes.variable_ref(var))
       
   255                       sterm = nodes.SortTerm(func, asc=False)
       
   256                       select.add_sort_term(sterm)
       
   257 
       
   258         The default implementation call
       
   259         :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
       
   260         """
       
   261         cls.cw_fetch_unrelated_order(select, attr, var)
   162 
   262 
   163     @classmethod
   263     @classmethod
   164     def fetch_unrelated_order(cls, attr, var):
   264     def cw_fetch_unrelated_order(cls, select, attr, var):
   165         """class method used to control sort order when multiple entities of
   265         """This class method may be used to control sort order when multiple entities of
   166         this type are fetched to use in edition (eg propose them to create a
   266         this type are fetched to use in edition (e.g. propose them to create a
   167         new relation on an edited entity).
   267         new relation on an edited entity).
       
   268 
       
   269         See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
       
   270         description of its arguments and usage.
       
   271 
       
   272         By default entities will be listed on their modification date descending,
       
   273         i.e. you'll get entities recently modified first.
   168         """
   274         """
   169         if attr == 'modification_date':
   275         if attr == 'modification_date':
   170             return '%s DESC' % var
   276             select.add_sort_var(var, asc=False)
   171         return None
       
   172 
   277 
   173     @classmethod
   278     @classmethod
   174     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   279     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
   175                   settype=True, ordermethod='fetch_order'):
   280                   settype=True, ordermethod='fetch_order'):
   176         """return a rql to fetch all entities of the class type"""
   281         st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
   177         # XXX update api and implementation to AST manipulation (see unrelated rql)
   282                              settype=settype, ordermethod=ordermethod)
   178         restrictions = restriction or []
   283         rql = st.as_string()
       
   284         if restriction:
       
   285             # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
       
   286             warn('[3.14] fetch_rql: use of `restriction` parameter is '
       
   287                  'deprecated, please use fetch_rqlst and supply a syntax'
       
   288                  'tree with your restriction instead', DeprecationWarning)
       
   289             insert = ' WHERE ' + ','.join(restriction)
       
   290             if ' WHERE ' in rql:
       
   291                 select, where = rql.split(' WHERE ', 1)
       
   292                 rql = select + insert + ',' + where
       
   293             else:
       
   294                 rql += insert
       
   295         return rql
       
   296 
       
   297     @classmethod
       
   298     def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
       
   299                     settype=True, ordermethod='fetch_order'):
       
   300         if select is None:
       
   301             select = Select()
       
   302             mainvar = select.get_variable(mainvar)
       
   303             select.add_selected(mainvar)
       
   304         elif isinstance(mainvar, basestring):
       
   305             assert mainvar in select.defined_vars
       
   306             mainvar = select.get_variable(mainvar)
       
   307         # eases string -> syntax tree test transition: please remove once stable
       
   308         select._varmaker = rqlvar_maker(defined=select.defined_vars,
       
   309                                         aliases=select.aliases, index=26)
   179         if settype:
   310         if settype:
   180             restrictions.append('%s is %s' % (mainvar, cls.__regid__))
   311             select.add_type_restriction(mainvar, cls.__regid__)
   181         if fetchattrs is None:
   312         if fetchattrs is None:
   182             fetchattrs = cls.fetch_attrs
   313             fetchattrs = cls.fetch_attrs
   183         selection = [mainvar]
   314         cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
   184         orderby = []
   315         return select
   185         # start from 26 to avoid possible conflicts with X
       
   186         # XXX not enough to be sure it'll be no conflicts
       
   187         varmaker = rqlvar_maker(index=26)
       
   188         cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
       
   189                                 orderby, restrictions, user, ordermethod)
       
   190         rql = 'Any %s' % ','.join(selection)
       
   191         if orderby:
       
   192             rql +=  ' ORDERBY %s' % ','.join(orderby)
       
   193         rql += ' WHERE %s' % ', '.join(restrictions)
       
   194         return rql
       
   195 
   316 
   196     @classmethod
   317     @classmethod
   197     def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
   318     def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema):
   198                             selection, orderby, restrictions, user,
   319         """find rtypes in `fetchattrs` that relate different subject etypes
   199                             ordermethod='fetch_order', visited=None):
   320         taken from (`subjtypes`) to different target etypes; these so called
       
   321         "ambiguous" relations, are added directly to the `select` syntax tree
       
   322         selection but removed from `fetchattrs` to avoid the fetch recursion
       
   323         because we have to choose only one targettype for the recursion and
       
   324         adding its own fetch attrs to the selection -when we recurse- would
       
   325         filter out the other possible target types from the result set
       
   326         """
       
   327         for attr in fetchattrs.copy():
       
   328             rschema = schema.rschema(attr)
       
   329             if rschema.final:
       
   330                 continue
       
   331             ttypes = None
       
   332             for subjtype in subjtypes:
       
   333                 cur_ttypes = set(rschema.objects(subjtype))
       
   334                 if ttypes is None:
       
   335                     ttypes = cur_ttypes
       
   336                 elif cur_ttypes != ttypes:
       
   337                     # we found an ambiguous relation: remove it from fetchattrs
       
   338                     fetchattrs.remove(attr)
       
   339                     # ... and add it to the selection
       
   340                     targetvar = select.make_variable()
       
   341                     select.add_selected(targetvar)
       
   342                     rel = make_relation(var, attr, (targetvar,), VariableRef)
       
   343                     select.add_restriction(rel)
       
   344                     break
       
   345 
       
   346     @classmethod
       
   347     def _fetch_restrictions(cls, mainvar, select, fetchattrs,
       
   348                             user, ordermethod='fetch_order', visited=None):
   200         eschema = cls.e_schema
   349         eschema = cls.e_schema
   201         if visited is None:
   350         if visited is None:
   202             visited = set((eschema.type,))
   351             visited = set((eschema.type,))
   203         elif eschema.type in visited:
   352         elif eschema.type in visited:
   204             # avoid infinite recursion
   353             # avoid infinite recursion
   214                             attr, cls.__regid__)
   363                             attr, cls.__regid__)
   215                 continue
   364                 continue
   216             rdef = eschema.rdef(attr)
   365             rdef = eschema.rdef(attr)
   217             if not user.matching_groups(rdef.get_groups('read')):
   366             if not user.matching_groups(rdef.get_groups('read')):
   218                 continue
   367                 continue
   219             var = varmaker.next()
   368             if rschema.final or rdef.cardinality[0] in '?1':
   220             selection.append(var)
   369                 var = select.make_variable()
   221             restriction = '%s %s %s' % (mainvar, attr, var)
   370                 select.add_selected(var)
   222             restrictions.append(restriction)
   371                 rel = make_relation(mainvar, attr, (var,), VariableRef)
       
   372                 select.add_restriction(rel)
       
   373             else:
       
   374                 cls.warning('bad relation %s specified in fetch attrs for %s',
       
   375                             attr, cls)
       
   376                 continue
   223             if not rschema.final:
   377             if not rschema.final:
   224                 card = rdef.cardinality[0]
       
   225                 if card not in '?1':
       
   226                     cls.warning('bad relation %s specified in fetch attrs for %s',
       
   227                                  attr, cls)
       
   228                     selection.pop()
       
   229                     restrictions.pop()
       
   230                     continue
       
   231                 # XXX we need outer join in case the relation is not mandatory
   378                 # XXX we need outer join in case the relation is not mandatory
   232                 # (card == '?')  *or if the entity is being added*, since in
   379                 # (card == '?')  *or if the entity is being added*, since in
   233                 # that case the relation may still be missing. As we miss this
   380                 # that case the relation may still be missing. As we miss this
   234                 # later information here, systematically add it.
   381                 # later information here, systematically add it.
   235                 restrictions[-1] += '?'
   382                 rel.change_optional('right')
   236                 targettypes = rschema.objects(eschema.type)
   383                 targettypes = rschema.objects(eschema.type)
   237                 # XXX user._cw.vreg iiiirk
   384                 vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
   238                 etypecls = user._cw.vreg['etypes'].etype_class(targettypes[0])
   385                 etypecls = vreg['etypes'].etype_class(targettypes[0])
   239                 if len(targettypes) > 1:
   386                 if len(targettypes) > 1:
   240                     # find fetch_attrs common to all destination types
   387                     # find fetch_attrs common to all destination types
   241                     fetchattrs = user._cw.vreg['etypes'].fetch_attrs(targettypes)
   388                     fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
   242                     remove_ambiguous_rels(fetchattrs, targettypes, user._cw.vreg.schema)
   389                     # ... and handle ambiguous relations
       
   390                     cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
       
   391                                                 targettypes, vreg.schema)
   243                 else:
   392                 else:
   244                     fetchattrs = etypecls.fetch_attrs
   393                     fetchattrs = etypecls.fetch_attrs
   245                 etypecls._fetch_restrictions(var, varmaker, fetchattrs,
   394                 etypecls._fetch_restrictions(var, select, fetchattrs,
   246                                              selection, orderby, restrictions,
       
   247                                              user, ordermethod, visited=visited)
   395                                              user, ordermethod, visited=visited)
   248             if ordermethod is not None:
   396             if ordermethod is not None:
   249                 orderterm = getattr(cls, ordermethod)(attr, var)
   397                 try:
   250                 if orderterm:
   398                     cmeth = getattr(cls, ordermethod)
   251                     orderby.append(orderterm)
   399                     warn('[3.14] %s %s class method should be renamed to cw_%s'
   252         return selection, orderby, restrictions
   400                          % (cls.__regid__, ordermethod, ordermethod),
       
   401                          DeprecationWarning)
       
   402                 except AttributeError:
       
   403                     cmeth = getattr(cls, 'cw_' + ordermethod)
       
   404                 if support_args(cmeth, 'select'):
       
   405                     cmeth(select, attr, var)
       
   406                 else:
       
   407                     warn('[3.14] %s should now take (select, attr, var) and '
       
   408                          'modify the syntax tree when desired instead of '
       
   409                          'returning something' % cmeth, DeprecationWarning)
       
   410                     orderterm = cmeth(attr, var.name)
       
   411                     if orderterm is not None:
       
   412                         try:
       
   413                             var, order = orderterm.split()
       
   414                         except ValueError:
       
   415                             if '(' in orderterm:
       
   416                                 cls.error('ignore %s until %s is upgraded',
       
   417                                           orderterm, cmeth)
       
   418                                 orderterm = None
       
   419                             elif not ' ' in orderterm.strip():
       
   420                                 var = orderterm
       
   421                                 order = 'ASC'
       
   422                         if orderterm is not None:
       
   423                             select.add_sort_var(select.get_variable(var),
       
   424                                                 order=='ASC')
   253 
   425 
   254     @classmethod
   426     @classmethod
   255     @cached
   427     @cached
   256     def _rest_attr_info(cls):
   428     def cw_rest_attr_info(cls):
       
   429         """this class method return an attribute name to be used in URL for
       
   430         entities of this type and a boolean flag telling if its value should be
       
   431         checked for uniqness.
       
   432 
       
   433         The attribute returned is, in order of priority:
       
   434 
       
   435         * class's `rest_attr` class attribute
       
   436         * an attribute defined as unique in the class'schema
       
   437         * 'eid'
       
   438         """
   257         mainattr, needcheck = 'eid', True
   439         mainattr, needcheck = 'eid', True
   258         if cls.rest_attr:
   440         if cls.rest_attr:
   259             mainattr = cls.rest_attr
   441             mainattr = cls.rest_attr
   260             needcheck = not cls.e_schema.has_unique_values(mainattr)
   442             needcheck = not cls.e_schema.has_unique_values(mainattr)
   261         else:
   443         else:
   262             for rschema in cls.e_schema.subject_relations():
   444             for rschema in cls.e_schema.subject_relations():
   263                 if rschema.final and rschema != 'eid' and cls.e_schema.has_unique_values(rschema):
   445                 if rschema.final and rschema != 'eid' \
       
   446                         and cls.e_schema.has_unique_values(rschema):
   264                     mainattr = str(rschema)
   447                     mainattr = str(rschema)
   265                     needcheck = False
   448                     needcheck = False
   266                     break
   449                     break
   267         if mainattr == 'eid':
   450         if mainattr == 'eid':
   268             needcheck = False
   451             needcheck = False
   352 
   535 
   353     def __json_encode__(self):
   536     def __json_encode__(self):
   354         """custom json dumps hook to dump the entity's eid
   537         """custom json dumps hook to dump the entity's eid
   355         which is not part of dict structure itself
   538         which is not part of dict structure itself
   356         """
   539         """
   357         dumpable = dict(self)
   540         dumpable = self.cw_attr_cache.copy()
   358         dumpable['eid'] = self.eid
   541         dumpable['eid'] = self.eid
   359         return dumpable
   542         return dumpable
   360 
   543 
   361     def cw_adapt_to(self, interface):
   544     def cw_adapt_to(self, interface):
   362         """return an adapter the entity to the given interface name.
   545         """return an adapter the entity to the given interface name.
   438                 return self.cwuri # XXX consider kwargs?
   621                 return self.cwuri # XXX consider kwargs?
   439             if sourcemeta.get('base-url'):
   622             if sourcemeta.get('base-url'):
   440                 kwargs['base_url'] = sourcemeta['base-url']
   623                 kwargs['base_url'] = sourcemeta['base-url']
   441                 use_ext_id = True
   624                 use_ext_id = True
   442         if method in (None, 'view'):
   625         if method in (None, 'view'):
   443             try:
   626             kwargs['_restpath'] = self.rest_path(use_ext_id)
   444                 kwargs['_restpath'] = self.rest_path(use_ext_id)
       
   445             except TypeError:
       
   446                 warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
       
   447                      'please update' % self.__regid__, DeprecationWarning)
       
   448                 kwargs['_restpath'] = self.rest_path()
       
   449         else:
   627         else:
   450             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
   628             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
   451         return self._cw.build_url(method, **kwargs)
   629         return self._cw.build_url(method, **kwargs)
   452 
   630 
   453     def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
   631     def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
   454         """returns a REST-like (relative) path for this entity"""
   632         """returns a REST-like (relative) path for this entity"""
   455         mainattr, needcheck = self._rest_attr_info()
   633         mainattr, needcheck = self.cw_rest_attr_info()
   456         etype = str(self.e_schema)
   634         etype = str(self.e_schema)
   457         path = etype.lower()
   635         path = etype.lower()
   458         if mainattr != 'eid':
   636         if mainattr != 'eid':
   459             value = getattr(self, mainattr)
   637             value = getattr(self, mainattr)
   460             if not can_use_rest_path(value):
   638             if not can_use_rest_path(value):
   514             if attrformat:
   692             if attrformat:
   515                 encoding = self.cw_attr_metadata(attr, 'encoding')
   693                 encoding = self.cw_attr_metadata(attr, 'encoding')
   516                 return self._cw_mtc_transform(value.getvalue(), attrformat, format,
   694                 return self._cw_mtc_transform(value.getvalue(), attrformat, format,
   517                                               encoding)
   695                                               encoding)
   518             return u''
   696             return u''
   519         value = printable_value(self._cw, attrtype, value, props,
   697         value = self._cw.printable_value(attrtype, value, props,
   520                                 displaytime=displaytime)
   698                                          displaytime=displaytime)
   521         if format == 'text/html':
   699         if format == 'text/html':
   522             value = xml_escape(value)
   700             value = xml_escape(value)
   523         return value
   701         return value
   524 
   702 
   525     def _cw_mtc_transform(self, data, format, target_format, encoding,
   703     def _cw_mtc_transform(self, data, format, target_format, encoding,
   540         By default meta and composite relations are skipped.
   718         By default meta and composite relations are skipped.
   541         Overrides this if you want another behaviour
   719         Overrides this if you want another behaviour
   542         """
   720         """
   543         assert self.has_eid()
   721         assert self.has_eid()
   544         execute = self._cw.execute
   722         execute = self._cw.execute
       
   723         skip_copy_for = {'subject': set(), 'object': set()}
       
   724         for rtype in self.skip_copy_for:
       
   725             skip_copy_for['subject'].add(rtype)
       
   726             warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
       
   727                  'use cw_skip_for instead with list of couples (rtype, role)' % self.__regid__,
       
   728                  DeprecationWarning)
       
   729         for rtype, role in self.cw_skip_copy_for:
       
   730             assert role in ('subject', 'object'), role
       
   731             skip_copy_for[role].add(rtype)
   545         for rschema in self.e_schema.subject_relations():
   732         for rschema in self.e_schema.subject_relations():
   546             if rschema.final or rschema.meta:
   733             if rschema.final or rschema.meta:
   547                 continue
   734                 continue
   548             # skip already defined relations
   735             # skip already defined relations
   549             if getattr(self, rschema.type):
   736             if getattr(self, rschema.type):
   550                 continue
   737                 continue
   551             if rschema.type in self.skip_copy_for:
   738             if rschema.type in skip_copy_for['subject']:
   552                 continue
   739                 continue
   553             # skip composite relation
   740             # skip composite relation
   554             rdef = self.e_schema.rdef(rschema)
   741             rdef = self.e_schema.rdef(rschema)
   555             if rdef.composite:
   742             if rdef.composite:
   556                 continue
   743                 continue
   565         for rschema in self.e_schema.object_relations():
   752         for rschema in self.e_schema.object_relations():
   566             if rschema.meta:
   753             if rschema.meta:
   567                 continue
   754                 continue
   568             # skip already defined relations
   755             # skip already defined relations
   569             if self.related(rschema.type, 'object'):
   756             if self.related(rschema.type, 'object'):
       
   757                 continue
       
   758             if rschema.type in skip_copy_for['object']:
   570                 continue
   759                 continue
   571             rdef = self.e_schema.rdef(rschema, 'object')
   760             rdef = self.e_schema.rdef(rschema, 'object')
   572             # skip composite relation
   761             # skip composite relation
   573             if rdef.composite:
   762             if rdef.composite:
   574                 continue
   763                 continue
   644                 continue
   833                 continue
   645             # case where attribute must be completed, but is not yet in entity
   834             # case where attribute must be completed, but is not yet in entity
   646             var = varmaker.next()
   835             var = varmaker.next()
   647             rql.append('%s %s %s' % (V, attr, var))
   836             rql.append('%s %s %s' % (V, attr, var))
   648             selected.append((attr, var))
   837             selected.append((attr, var))
   649         # +1 since this doen't include the main variable
   838         # +1 since this doesn't include the main variable
   650         lastattr = len(selected) + 1
   839         lastattr = len(selected) + 1
   651         # don't fetch extra relation if attributes specified or of the entity is
   840         # don't fetch extra relation if attributes specified or of the entity is
   652         # coming from an external source (may lead to error)
   841         # coming from an external source (may lead to error)
   653         if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
   842         if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
   654             # fetch additional relations (restricted to 0..1 relations)
   843             # fetch additional relations (restricted to 0..1 relations)
   736           if True, the entites are returned; if False, a result set is returned
   925           if True, the entites are returned; if False, a result set is returned
   737         :param safe:
   926         :param safe:
   738           if True, an empty rset/list of entities will be returned in case of
   927           if True, an empty rset/list of entities will be returned in case of
   739           :exc:`Unauthorized`, else (the default), the exception is propagated
   928           :exc:`Unauthorized`, else (the default), the exception is propagated
   740         """
   929         """
       
   930         rtype = str(rtype)
   741         try:
   931         try:
   742             return self._cw_relation_cache(rtype, role, entities, limit)
   932             return self._cw_relation_cache(rtype, role, entities, limit)
   743         except KeyError:
   933         except KeyError:
   744             pass
   934             pass
   745         if not self.has_eid():
   935         if not self.has_eid():
   755             rset = self._cw.empty_rset()
   945             rset = self._cw.empty_rset()
   756         self.cw_set_relation_cache(rtype, role, rset)
   946         self.cw_set_relation_cache(rtype, role, rset)
   757         return self.related(rtype, role, limit, entities)
   947         return self.related(rtype, role, limit, entities)
   758 
   948 
   759     def cw_related_rql(self, rtype, role='subject', targettypes=None):
   949     def cw_related_rql(self, rtype, role='subject', targettypes=None):
   760         rschema = self._cw.vreg.schema[rtype]
   950         vreg = self._cw.vreg
       
   951         rschema = vreg.schema[rtype]
       
   952         select = Select()
       
   953         mainvar, evar = select.get_variable('X'), select.get_variable('E')
       
   954         select.add_selected(mainvar)
       
   955         select.add_eid_restriction(evar, 'x', 'Substitute')
   761         if role == 'subject':
   956         if role == 'subject':
   762             restriction = 'E eid %%(x)s, E %s X' % rtype
   957             rel = make_relation(evar, rtype, (mainvar,), VariableRef)
       
   958             select.add_restriction(rel)
   763             if targettypes is None:
   959             if targettypes is None:
   764                 targettypes = rschema.objects(self.e_schema)
   960                 targettypes = rschema.objects(self.e_schema)
   765             else:
   961             else:
   766                 restriction += ', X is IN (%s)' % ','.join(targettypes)
   962                 select.add_constant_restriction(mainvar, 'is',
   767             card = greater_card(rschema, (self.e_schema,), targettypes, 0)
   963                                                 targettypes, 'etype')
       
   964             gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
   768         else:
   965         else:
   769             restriction = 'E eid %%(x)s, X %s E' % rtype
   966             rel = make_relation(mainvar, rtype, (evar,), VariableRef)
       
   967             select.add_restriction(rel)
   770             if targettypes is None:
   968             if targettypes is None:
   771                 targettypes = rschema.subjects(self.e_schema)
   969                 targettypes = rschema.subjects(self.e_schema)
   772             else:
   970             else:
   773                 restriction += ', X is IN (%s)' % ','.join(targettypes)
   971                 select.add_constant_restriction(mainvar, 'is', targettypes,
   774             card = greater_card(rschema, targettypes, (self.e_schema,), 1)
   972                                                 'etype')
   775         etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
   973             gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
       
   974         etypecls = vreg['etypes'].etype_class(targettypes[0])
   776         if len(targettypes) > 1:
   975         if len(targettypes) > 1:
   777             fetchattrs = self._cw.vreg['etypes'].fetch_attrs(targettypes)
   976             fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
   778             # XXX we should fetch ambiguous relation objects too but not
   977             self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
   779             # recurse on them in _fetch_restrictions; it is easier to remove
   978                                          targettypes, vreg.schema)
   780             # them completely for now, as it would require an deeper api rewrite
       
   781             remove_ambiguous_rels(fetchattrs, targettypes, self._cw.vreg.schema)
       
   782         else:
   979         else:
   783             fetchattrs = etypecls.fetch_attrs
   980             fetchattrs = etypecls.fetch_attrs
   784         rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
   981         etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
   785                                  settype=False)
   982                              settype=False)
   786         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
   983         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
   787         # greater_card return 1 for those both cases)
   984         # greater_card return 1 for those both cases)
   788         if card == '1':
   985         if gcard == '1':
   789             if ' ORDERBY ' in rql:
   986             select.remove_sort_terms()
   790                 rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
   987         elif not select.orderby:
   791                                        rql.split(' WHERE ', 1)[1])
   988             # if modification_date is already retrieved, we use it instead
   792         elif not ' ORDERBY ' in rql:
   989             # of adding another variable for sorting. This should not be
   793             args = rql.split(' WHERE ', 1)
   990             # problematic, but it is with sqlserver, see ticket #694445
   794             # if modification_date already retrieved, we should use it instead
   991             for rel in select.where.get_nodes(RqlRelation):
   795             # of adding another variable for sort. This should be be problematic
   992                 if (rel.r_type == 'modification_date'
   796             # but it's actually with sqlserver, see ticket #694445
   993                     and rel.children[0].variable == mainvar
   797             if 'X modification_date ' in args[1]:
   994                     and rel.children[1].operator == '='):
   798                 var = args[1].split('X modification_date ', 1)[1].split(',', 1)[0]
   995                     var = rel.children[1].children[0].variable
   799                 args.insert(1, var.strip())
   996                     select.add_sort_var(var, asc=False)
   800                 rql = '%s ORDERBY %s DESC WHERE %s' % tuple(args)
   997                     break
   801             else:
   998             else:
   802                 rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % \
   999                 mdvar = select.make_variable()
   803                       tuple(args)
  1000                 rel = make_relation(mainvar, 'modification_date',
   804         return rql
  1001                                     (mdvar,), VariableRef)
       
  1002                 select.add_restriction(rel)
       
  1003                 select.add_sort_var(mdvar, asc=False)
       
  1004         return select.as_string()
   805 
  1005 
   806     # generic vocabulary methods ##############################################
  1006     # generic vocabulary methods ##############################################
   807 
  1007 
   808     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
  1008     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
   809                          vocabconstraints=True):
  1009                          vocabconstraints=True, lt_infos={}):
   810         """build a rql to fetch `targettype` entities unrelated to this entity
  1010         """build a rql to fetch `targettype` entities unrelated to this entity
   811         using (rtype, role) relation.
  1011         using (rtype, role) relation.
   812 
  1012 
   813         Consider relation permissions so that returned entities may be actually
  1013         Consider relation permissions so that returned entities may be actually
   814         linked by `rtype`.
  1014         linked by `rtype`.
       
  1015 
       
  1016         `lt_infos` are supplementary informations, usually coming from __linkto
       
  1017         parameter, that can help further restricting the results in case current
       
  1018         entity is not yet created. It is a dict describing entities the current
       
  1019         entity will be linked to, which keys are (rtype, role) tuples and values
       
  1020         are a list of eids.
   815         """
  1021         """
   816         ordermethod = ordermethod or 'fetch_unrelated_order'
  1022         ordermethod = ordermethod or 'fetch_unrelated_order'
   817         if isinstance(rtype, basestring):
  1023         rschema = self._cw.vreg.schema.rschema(rtype)
   818             rtype = self._cw.vreg.schema.rschema(rtype)
  1024         rdef = rschema.role_rdef(self.e_schema, targettype, role)
   819         rdef = rtype.role_rdef(self.e_schema, targettype, role)
       
   820         rewriter = RQLRewriter(self._cw)
  1025         rewriter = RQLRewriter(self._cw)
       
  1026         select = Select()
   821         # initialize some variables according to the `role` of `self` in the
  1027         # initialize some variables according to the `role` of `self` in the
   822         # relation:
  1028         # relation (variable names must respect constraints conventions):
   823         # * variable for myself (`evar`) and searched entities (`searchvedvar`)
  1029         # * variable for myself (`evar`)
   824         # * entity type of the subject (`subjtype`) and of the object
  1030         # * variable for searched entities (`searchvedvar`)
   825         #   (`objtype`) of the relation
       
   826         if role == 'subject':
  1031         if role == 'subject':
   827             evar, searchedvar = 'S', 'O'
  1032             evar = subjvar = select.get_variable('S')
   828             subjtype, objtype = self.e_schema, targettype
  1033             searchedvar = objvar = select.get_variable('O')
   829         else:
  1034         else:
   830             searchedvar, evar = 'S', 'O'
  1035             searchedvar = subjvar = select.get_variable('S')
   831             objtype, subjtype = self.e_schema, targettype
  1036             evar = objvar = select.get_variable('O')
   832         # initialize some variables according to `self` existance
  1037         select.add_selected(searchedvar)
       
  1038         # initialize some variables according to `self` existence
   833         if rdef.role_cardinality(neg_role(role)) in '?1':
  1039         if rdef.role_cardinality(neg_role(role)) in '?1':
   834             # if cardinality in '1?', we want a target entity which isn't
  1040             # if cardinality in '1?', we want a target entity which isn't
   835             # already linked using this relation
  1041             # already linked using this relation
   836             if searchedvar == 'S':
  1042             variable = select.make_variable()
   837                 restriction = ['NOT S %s ZZ' % rtype]
  1043             if role == 'subject':
   838             else:
  1044                 rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
   839                 restriction = ['NOT ZZ %s O' % rtype]
  1045             else:
       
  1046                 rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
       
  1047             select.add_restriction(Not(rel))
   840         elif self.has_eid():
  1048         elif self.has_eid():
   841             # elif we have an eid, we don't want a target entity which is
  1049             # elif we have an eid, we don't want a target entity which is
   842             # already linked to ourself through this relation
  1050             # already linked to ourself through this relation
   843             restriction = ['NOT S %s O' % rtype]
  1051             rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
   844         else:
  1052             select.add_restriction(Not(rel))
   845             restriction = []
       
   846         if self.has_eid():
  1053         if self.has_eid():
   847             restriction += ['%s eid %%(x)s' % evar]
  1054             rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
       
  1055             select.add_restriction(rel)
   848             args = {'x': self.eid}
  1056             args = {'x': self.eid}
   849             if role == 'subject':
  1057             if role == 'subject':
   850                 sec_check_args = {'fromeid': self.eid}
  1058                 sec_check_args = {'fromeid': self.eid}
   851             else:
  1059             else:
   852                 sec_check_args = {'toeid': self.eid}
  1060                 sec_check_args = {'toeid': self.eid}
   853             existant = None # instead of 'SO', improve perfs
  1061             existant = None # instead of 'SO', improve perfs
   854         else:
  1062         else:
   855             args = {}
  1063             args = {}
   856             sec_check_args = {}
  1064             sec_check_args = {}
   857             existant = searchedvar
  1065             existant = searchedvar.name
   858         # retreive entity class for targettype to compute base rql
  1066             # undefine unused evar, or the type resolver will consider it
       
  1067             select.undefine_variable(evar)
       
  1068         # retrieve entity class for targettype to compute base rql
   859         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
  1069         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
   860         rql = etypecls.fetch_rql(self._cw.user, restriction,
  1070         etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
   861                                  mainvar=searchedvar, ordermethod=ordermethod)
  1071                              ordermethod=ordermethod)
   862         select = self._cw.vreg.parse(self._cw, rql, args).children[0]
  1072         # from now on, we need variable type resolving
       
  1073         self._cw.vreg.solutions(self._cw, select, args)
   863         # insert RQL expressions for schema constraints into the rql syntax tree
  1074         # insert RQL expressions for schema constraints into the rql syntax tree
   864         if vocabconstraints:
  1075         if vocabconstraints:
   865             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
  1076             # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
   866             # will be included as well
  1077             # will be included as well
   867             cstrcls = RQLVocabularyConstraint
  1078             cstrcls = RQLVocabularyConstraint
   868         else:
  1079         else:
   869             cstrcls = RQLConstraint
  1080             cstrcls = RQLConstraint
       
  1081         lt_infos = pruned_lt_info(self.e_schema, lt_infos or {})
       
  1082         # if there are still lt_infos, use set to keep track of added eid
       
  1083         # relations (adding twice the same eid relation is incorrect RQL)
       
  1084         eidvars = set()
   870         for cstr in rdef.constraints:
  1085         for cstr in rdef.constraints:
   871             # consider constraint.mainvars to check if constraint apply
  1086             # consider constraint.mainvars to check if constraint apply
   872             if isinstance(cstr, cstrcls) and searchedvar in cstr.mainvars:
  1087             if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
   873                 if not self.has_eid() and evar in cstr.mainvars:
  1088                 if not self.has_eid():
   874                     continue
  1089                     if lt_infos:
       
  1090                         # we can perhaps further restrict with linkto infos using
       
  1091                         # a custom constraint built from cstr and lt_infos
       
  1092                         cstr = build_cstr_with_linkto_infos(
       
  1093                             cstr, args, searchedvar, evar, lt_infos, eidvars)
       
  1094                         if cstr is None:
       
  1095                             continue # could not build constraint -> discard
       
  1096                     elif evar.name in cstr.mainvars:
       
  1097                         continue
   875                 # compute a varmap suitable to RQLRewriter.rewrite argument
  1098                 # compute a varmap suitable to RQLRewriter.rewrite argument
   876                 varmap = dict((v, v) for v in 'SO' if v in select.defined_vars
  1099                 varmap = dict((v, v) for v in (searchedvar.name, evar.name)
   877                               and v in cstr.mainvars)
  1100                               if v in select.defined_vars and v in cstr.mainvars)
   878                 # rewrite constraint by constraint since we want a AND between
  1101                 # rewrite constraint by constraint since we want a AND between
   879                 # expressions.
  1102                 # expressions.
   880                 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions,
  1103                 rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions,
   881                                  args, existant)
  1104                                  args, existant)
   882         # insert security RQL expressions granting the permission to 'add' the
  1105         # insert security RQL expressions granting the permission to 'add' the
   883         # relation into the rql syntax tree, if necessary
  1106         # relation into the rql syntax tree, if necessary
   884         rqlexprs = rdef.get_rqlexprs('add')
  1107         rqlexprs = rdef.get_rqlexprs('add')
   885         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
  1108         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
   886             # compute a varmap suitable to RQLRewriter.rewrite argument
  1109             # compute a varmap suitable to RQLRewriter.rewrite argument
   887             varmap = dict((v, v) for v in 'SO' if v in select.defined_vars)
  1110             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
       
  1111                           if v in select.defined_vars)
   888             # rewrite all expressions at once since we want a OR between them.
  1112             # rewrite all expressions at once since we want a OR between them.
   889             rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions,
  1113             rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions,
   890                              args, existant)
  1114                              args, existant)
   891         # ensure we have an order defined
  1115         # ensure we have an order defined
   892         if not select.orderby:
  1116         if not select.orderby:
   893             select.add_sort_var(select.defined_vars[searchedvar])
  1117             select.add_sort_var(select.defined_vars[searchedvar.name])
   894         # we're done, turn the rql syntax tree as a string
  1118         # we're done, turn the rql syntax tree as a string
   895         rql = select.as_string()
  1119         rql = select.as_string()
   896         return rql, args
  1120         return rql, args
   897 
  1121 
   898     def unrelated(self, rtype, targettype, role='subject', limit=None,
  1122     def unrelated(self, rtype, targettype, role='subject', limit=None,
   899                   ordermethod=None): # XXX .cw_unrelated
  1123                   ordermethod=None, lt_infos={}): # XXX .cw_unrelated
   900         """return a result set of target type objects that may be related
  1124         """return a result set of target type objects that may be related
   901         by a given relation, with self as subject or object
  1125         by a given relation, with self as subject or object
   902         """
  1126         """
   903         try:
  1127         try:
   904             rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
  1128             rql, args = self.cw_unrelated_rql(rtype, targettype, role,
       
  1129                                               ordermethod, lt_infos=lt_infos)
   905         except Unauthorized:
  1130         except Unauthorized:
   906             return self._cw.empty_rset()
  1131             return self._cw.empty_rset()
   907         # XXX should be set in unrelated rql when manipulating the AST
  1132         # XXX should be set in unrelated rql when manipulating the AST
   908         if limit is not None:
  1133         if limit is not None:
   909             before, after = rql.split(' WHERE ', 1)
  1134             before, after = rql.split(' WHERE ', 1)