cubicweb/entity.py
changeset 11057 0b59724cb3f2
parent 11047 bfd11ffa79f7
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """Base class for entity objects manipulated in clients"""
       
    19 
       
    20 __docformat__ = "restructuredtext en"
       
    21 
       
    22 from warnings import warn
       
    23 from functools import partial
       
    24 
       
    25 from six import text_type, string_types, integer_types
       
    26 from six.moves import range
       
    27 
       
    28 from logilab.common.decorators import cached
       
    29 from logilab.common.deprecation import deprecated
       
    30 from logilab.common.registry import yes
       
    31 from logilab.mtconverter import TransformData, xml_escape
       
    32 
       
    33 from rql.utils import rqlvar_maker
       
    34 from rql.stmts import Select
       
    35 from rql.nodes import (Not, VariableRef, Constant, make_relation,
       
    36                        Relation as RqlRelation)
       
    37 
       
    38 from cubicweb import Unauthorized, neg_role
       
    39 from cubicweb.utils import support_args
       
    40 from cubicweb.rset import ResultSet
       
    41 from cubicweb.appobject import AppObject
       
    42 from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
       
    43                              GeneratedConstraint)
       
    44 from cubicweb.rqlrewrite import RQLRewriter
       
    45 
       
    46 from cubicweb.uilib import soup2xhtml
       
    47 from cubicweb.mttransforms import ENGINE
       
    48 
       
    49 _marker = object()
       
    50 
       
    51 def greater_card(rschema, subjtypes, objtypes, index):
       
    52     for subjtype in subjtypes:
       
    53         for objtype in objtypes:
       
    54             card = rschema.rdef(subjtype, objtype).cardinality[index]
       
    55             if card in '+*':
       
    56                 return card
       
    57     return '1'
       
    58 
       
    59 def can_use_rest_path(value):
       
    60     """return True if value can be used at the end of a Rest URL path"""
       
    61     if value is None:
       
    62         return False
       
    63     value = text_type(value)
       
    64     # the check for ?, /, & are to prevent problems when running
       
    65     # behind Apache mod_proxy
       
    66     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
       
    67         return False
       
    68     return True
       
    69 
       
    70 def rel_vars(rel):
       
    71     return ((isinstance(rel.children[0], VariableRef)
       
    72              and rel.children[0].variable or None),
       
    73             (isinstance(rel.children[1].children[0], VariableRef)
       
    74              and rel.children[1].children[0].variable or None)
       
    75             )
       
    76 
       
    77 def rel_matches(rel, rtype, role, varname, operator='='):
       
    78     if rel.r_type == rtype and rel.children[1].operator == operator:
       
    79         same_role_var_idx = 0 if role == 'subject' else 1
       
    80         variables = rel_vars(rel)
       
    81         if variables[same_role_var_idx].name == varname:
       
    82             return variables[1 - same_role_var_idx]
       
    83 
       
    84 def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
       
    85                                  lt_infos, eidvars):
       
    86     """restrict vocabulary as much as possible in entity creation,
       
    87     based on infos provided by __linkto form param.
       
    88 
       
    89     Example based on following schema:
       
    90 
       
    91       class works_in(RelationDefinition):
       
    92           subject = 'CWUser'
       
    93           object = 'Lab'
       
    94           cardinality = '1*'
       
    95           constraints = [RQLConstraint('S in_group G, O welcomes G')]
       
    96 
       
    97       class welcomes(RelationDefinition):
       
    98           subject = 'Lab'
       
    99           object = 'CWGroup'
       
   100 
       
   101     If you create a CWUser in the "scientists" CWGroup you can show
       
   102     only the labs that welcome them using :
       
   103 
       
   104       lt_infos = {('in_group', 'subject'): 321}
       
   105 
       
   106     You get following restriction : 'O welcomes G, G eid 321'
       
   107 
       
   108     """
       
   109     st = cstr.snippet_rqlst.copy()
       
   110     # replace relations in ST by eid infos from linkto where possible
       
   111     for (info_rtype, info_role), eids in lt_infos.items():
       
   112         eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
       
   113         for rel in st.iget_nodes(RqlRelation):
       
   114             targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
       
   115             if targetvar is not None:
       
   116                 if targetvar.name in eidvars:
       
   117                     rel.parent.remove(rel)
       
   118                 else:
       
   119                     eidrel = make_relation(
       
   120                         targetvar, 'eid', (targetvar.name, 'Substitute'),
       
   121                         Constant)
       
   122                     rel.parent.replace(rel, eidrel)
       
   123                     args[targetvar.name] = eid
       
   124                     eidvars.add(targetvar.name)
       
   125     # if modified ST still contains evar references we must discard the
       
   126     # constraint, otherwise evar is unknown in the final rql query which can
       
   127     # lead to a SQL table cartesian product and multiple occurences of solutions
       
   128     evarname = evar.name
       
   129     for rel in st.iget_nodes(RqlRelation):
       
   130         for variable in rel_vars(rel):
       
   131             if variable and evarname == variable.name:
       
   132                 return
       
   133     # else insert snippets into the global tree
       
   134     return GeneratedConstraint(st, cstr.mainvars - set(evarname))
       
   135 
       
   136 def pruned_lt_info(eschema, lt_infos):
       
   137     pruned = {}
       
   138     for (lt_rtype, lt_role), eids in lt_infos.items():
       
   139         # we can only use lt_infos describing relation with a cardinality
       
   140         # of value 1 towards the linked entity
       
   141         if not len(eids) == 1:
       
   142             continue
       
   143         lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
       
   144             0 if lt_role == 'subject' else 1]
       
   145         if lt_card not in '?1':
       
   146             continue
       
   147         pruned[(lt_rtype, lt_role)] = eids
       
   148     return pruned
       
   149 
       
   150 
       
   151 class Entity(AppObject):
       
   152     """an entity instance has e_schema automagically set on
       
   153     the class and instances has access to their issuing cursor.
       
   154 
       
   155     A property is set for each attribute and relation on each entity's type
       
   156     class. Becare that among attributes, 'eid' is *NEITHER* stored in the
       
   157     dict containment (which acts as a cache for other attributes dynamically
       
   158     fetched)
       
   159 
       
   160     :type e_schema: `cubicweb.schema.EntitySchema`
       
   161     :ivar e_schema: the entity's schema
       
   162 
       
   163     :type rest_attr: str
       
   164     :cvar rest_attr: indicates which attribute should be used to build REST urls
       
   165        If `None` is specified (the default), the first unique attribute will
       
   166        be used ('eid' if none found)
       
   167 
       
   168     :type cw_skip_copy_for: list
       
   169     :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
       
   170        that should be skipped when copying this kind of entity. Note that some
       
   171        relations such as composite relations or relations that have '?1' as
       
   172        object cardinality are always skipped.
       
   173     """
       
   174     __registry__ = 'etypes'
       
   175     __select__ = yes()
       
   176 
       
   177     # class attributes that must be set in class definition
       
   178     rest_attr = None
       
   179     fetch_attrs = None
       
   180     skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
       
   181     cw_skip_copy_for = [('in_state', 'subject')]
       
   182     # class attributes set automatically at registration time
       
   183     e_schema = None
       
   184 
       
   185     @classmethod
       
   186     def __initialize__(cls, schema):
       
   187         """initialize a specific entity class by adding descriptors to access
       
   188         entity type's attributes and relations
       
   189         """
       
   190         etype = cls.__regid__
       
   191         assert etype != 'Any', etype
       
   192         cls.e_schema = eschema = schema.eschema(etype)
       
   193         for rschema, _ in eschema.attribute_definitions():
       
   194             if rschema.type == 'eid':
       
   195                 continue
       
   196             setattr(cls, rschema.type, Attribute(rschema.type))
       
   197         mixins = []
       
   198         for rschema, _, role in eschema.relation_definitions():
       
   199             if role == 'subject':
       
   200                 attr = rschema.type
       
   201             else:
       
   202                 attr = 'reverse_%s' % rschema.type
       
   203             setattr(cls, attr, Relation(rschema, role))
       
   204 
       
   205     fetch_attrs = ('modification_date',)
       
   206 
       
   207     @classmethod
       
   208     def cw_fetch_order(cls, select, attr, var):
       
   209         """This class method may be used to control sort order when multiple
       
   210         entities of this type are fetched through ORM methods. Its arguments
       
   211         are:
       
   212 
       
   213         * `select`, the RQL syntax tree
       
   214 
       
   215         * `attr`, the attribute being watched
       
   216 
       
   217         * `var`, the variable through which this attribute's value may be
       
   218           accessed in the query
       
   219 
       
   220         When you want to do some sorting on the given attribute, you should
       
   221         modify the syntax tree accordingly. For instance:
       
   222 
       
   223         .. sourcecode:: python
       
   224 
       
   225           from rql import nodes
       
   226 
       
   227           class Version(AnyEntity):
       
   228               __regid__ = 'Version'
       
   229 
       
   230               fetch_attrs = ('num', 'description', 'in_state')
       
   231 
       
   232               @classmethod
       
   233               def cw_fetch_order(cls, select, attr, var):
       
   234                   if attr == 'num':
       
   235                       func = nodes.Function('version_sort_value')
       
   236                       func.append(nodes.variable_ref(var))
       
   237                       sterm = nodes.SortTerm(func, asc=False)
       
   238                       select.add_sort_term(sterm)
       
   239 
       
   240         The default implementation call
       
   241         :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
       
   242         """
       
   243         cls.cw_fetch_unrelated_order(select, attr, var)
       
   244 
       
   245     @classmethod
       
   246     def cw_fetch_unrelated_order(cls, select, attr, var):
       
   247         """This class method may be used to control sort order when multiple entities of
       
   248         this type are fetched to use in edition (e.g. propose them to create a
       
   249         new relation on an edited entity).
       
   250 
       
   251         See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
       
   252         description of its arguments and usage.
       
   253 
       
   254         By default entities will be listed on their modification date descending,
       
   255         i.e. you'll get entities recently modified first.
       
   256         """
       
   257         if attr == 'modification_date':
       
   258             select.add_sort_var(var, asc=False)
       
   259 
       
   260     @classmethod
       
   261     def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
       
   262                   settype=True, ordermethod='fetch_order'):
       
   263         st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
       
   264                              settype=settype, ordermethod=ordermethod)
       
   265         rql = st.as_string()
       
   266         if restriction:
       
   267             # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
       
   268             warn('[3.14] fetch_rql: use of `restriction` parameter is '
       
   269                  'deprecated, please use fetch_rqlst and supply a syntax'
       
   270                  'tree with your restriction instead', DeprecationWarning)
       
   271             insert = ' WHERE ' + ','.join(restriction)
       
   272             if ' WHERE ' in rql:
       
   273                 select, where = rql.split(' WHERE ', 1)
       
   274                 rql = select + insert + ',' + where
       
   275             else:
       
   276                 rql += insert
       
   277         return rql
       
   278 
       
   279     @classmethod
       
   280     def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
       
   281                     settype=True, ordermethod='fetch_order'):
       
   282         if select is None:
       
   283             select = Select()
       
   284             mainvar = select.get_variable(mainvar)
       
   285             select.add_selected(mainvar)
       
   286         elif isinstance(mainvar, string_types):
       
   287             assert mainvar in select.defined_vars
       
   288             mainvar = select.get_variable(mainvar)
       
   289         # eases string -> syntax tree test transition: please remove once stable
       
   290         select._varmaker = rqlvar_maker(defined=select.defined_vars,
       
   291                                         aliases=select.aliases, index=26)
       
   292         if settype:
       
   293             rel = select.add_type_restriction(mainvar, cls.__regid__)
       
   294             # should use 'is_instance_of' instead of 'is' so we retrieve
       
   295             # subclasses instances as well
       
   296             rel.r_type = 'is_instance_of'
       
   297         if fetchattrs is None:
       
   298             fetchattrs = cls.fetch_attrs
       
   299         cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
       
   300         return select
       
   301 
       
   302     @classmethod
       
   303     def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema):
       
   304         """find rtypes in `fetchattrs` that relate different subject etypes
       
   305         taken from (`subjtypes`) to different target etypes; these so called
       
   306         "ambiguous" relations, are added directly to the `select` syntax tree
       
   307         selection but removed from `fetchattrs` to avoid the fetch recursion
       
   308         because we have to choose only one targettype for the recursion and
       
   309         adding its own fetch attrs to the selection -when we recurse- would
       
   310         filter out the other possible target types from the result set
       
   311         """
       
   312         for attr in fetchattrs.copy():
       
   313             rschema = schema.rschema(attr)
       
   314             if rschema.final:
       
   315                 continue
       
   316             ttypes = None
       
   317             for subjtype in subjtypes:
       
   318                 cur_ttypes = set(rschema.objects(subjtype))
       
   319                 if ttypes is None:
       
   320                     ttypes = cur_ttypes
       
   321                 elif cur_ttypes != ttypes:
       
   322                     # we found an ambiguous relation: remove it from fetchattrs
       
   323                     fetchattrs.remove(attr)
       
   324                     # ... and add it to the selection
       
   325                     targetvar = select.make_variable()
       
   326                     select.add_selected(targetvar)
       
   327                     rel = make_relation(var, attr, (targetvar,), VariableRef)
       
   328                     select.add_restriction(rel)
       
   329                     break
       
   330 
       
   331     @classmethod
       
   332     def _fetch_restrictions(cls, mainvar, select, fetchattrs,
       
   333                             user, ordermethod='fetch_order', visited=None):
       
   334         eschema = cls.e_schema
       
   335         if visited is None:
       
   336             visited = set((eschema.type,))
       
   337         elif eschema.type in visited:
       
   338             # avoid infinite recursion
       
   339             return
       
   340         else:
       
   341             visited.add(eschema.type)
       
   342         _fetchattrs = []
       
   343         for attr in sorted(fetchattrs):
       
   344             try:
       
   345                 rschema = eschema.subjrels[attr]
       
   346             except KeyError:
       
   347                 cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
       
   348                             attr, cls.__regid__)
       
   349                 continue
       
   350             # XXX takefirst=True to remove warning triggered by ambiguous inlined relations
       
   351             rdef = eschema.rdef(attr, takefirst=True)
       
   352             if not user.matching_groups(rdef.get_groups('read')):
       
   353                 continue
       
   354             if rschema.final or rdef.cardinality[0] in '?1':
       
   355                 var = select.make_variable()
       
   356                 select.add_selected(var)
       
   357                 rel = make_relation(mainvar, attr, (var,), VariableRef)
       
   358                 select.add_restriction(rel)
       
   359             else:
       
   360                 cls.warning('bad relation %s specified in fetch attrs for %s',
       
   361                             attr, cls)
       
   362                 continue
       
   363             if not rschema.final:
       
   364                 # XXX we need outer join in case the relation is not mandatory
       
   365                 # (card == '?')  *or if the entity is being added*, since in
       
   366                 # that case the relation may still be missing. As we miss this
       
   367                 # later information here, systematically add it.
       
   368                 rel.change_optional('right')
       
   369                 targettypes = rschema.objects(eschema.type)
       
   370                 vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
       
   371                 etypecls = vreg['etypes'].etype_class(targettypes[0])
       
   372                 if len(targettypes) > 1:
       
   373                     # find fetch_attrs common to all destination types
       
   374                     fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
       
   375                     # ... and handle ambiguous relations
       
   376                     cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
       
   377                                                 targettypes, vreg.schema)
       
   378                 else:
       
   379                     fetchattrs = etypecls.fetch_attrs
       
   380                 etypecls._fetch_restrictions(var, select, fetchattrs,
       
   381                                              user, None, visited=visited)
       
   382             if ordermethod is not None:
       
   383                 try:
       
   384                     cmeth = getattr(cls, ordermethod)
       
   385                     warn('[3.14] %s %s class method should be renamed to cw_%s'
       
   386                          % (cls.__regid__, ordermethod, ordermethod),
       
   387                          DeprecationWarning)
       
   388                 except AttributeError:
       
   389                     cmeth = getattr(cls, 'cw_' + ordermethod)
       
   390                 if support_args(cmeth, 'select'):
       
   391                     cmeth(select, attr, var)
       
   392                 else:
       
   393                     warn('[3.14] %s should now take (select, attr, var) and '
       
   394                          'modify the syntax tree when desired instead of '
       
   395                          'returning something' % cmeth, DeprecationWarning)
       
   396                     orderterm = cmeth(attr, var.name)
       
   397                     if orderterm is not None:
       
   398                         try:
       
   399                             var, order = orderterm.split()
       
   400                         except ValueError:
       
   401                             if '(' in orderterm:
       
   402                                 cls.error('ignore %s until %s is upgraded',
       
   403                                           orderterm, cmeth)
       
   404                                 orderterm = None
       
   405                             elif not ' ' in orderterm.strip():
       
   406                                 var = orderterm
       
   407                                 order = 'ASC'
       
   408                         if orderterm is not None:
       
   409                             select.add_sort_var(select.get_variable(var),
       
   410                                                 order=='ASC')
       
   411 
       
   412     @classmethod
       
   413     @cached
       
   414     def cw_rest_attr_info(cls):
       
   415         """this class method return an attribute name to be used in URL for
       
   416         entities of this type and a boolean flag telling if its value should be
       
   417         checked for uniqness.
       
   418 
       
   419         The attribute returned is, in order of priority:
       
   420 
       
   421         * class's `rest_attr` class attribute
       
   422         * an attribute defined as unique in the class'schema
       
   423         * 'eid'
       
   424         """
       
   425         mainattr, needcheck = 'eid', True
       
   426         if cls.rest_attr:
       
   427             mainattr = cls.rest_attr
       
   428             needcheck = not cls.e_schema.has_unique_values(mainattr)
       
   429         else:
       
   430             for rschema in cls.e_schema.subject_relations():
       
   431                 if (rschema.final
       
   432                     and rschema not in ('eid', 'cwuri')
       
   433                     and cls.e_schema.has_unique_values(rschema)
       
   434                     and cls.e_schema.rdef(rschema.type).cardinality[0] == '1'):
       
   435                     mainattr = str(rschema)
       
   436                     needcheck = False
       
   437                     break
       
   438         if mainattr == 'eid':
       
   439             needcheck = False
       
   440         return mainattr, needcheck
       
   441 
       
   442     @classmethod
       
   443     def _cw_build_entity_query(cls, kwargs):
       
   444         relations = []
       
   445         restrictions = set()
       
   446         pendingrels = []
       
   447         eschema = cls.e_schema
       
   448         qargs = {}
       
   449         attrcache = {}
       
   450         for attr, value in kwargs.items():
       
   451             if attr.startswith('reverse_'):
       
   452                 attr = attr[len('reverse_'):]
       
   453                 role = 'object'
       
   454             else:
       
   455                 role = 'subject'
       
   456             assert eschema.has_relation(attr, role), '%s %s not found on %s' % (attr, role, eschema)
       
   457             rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
       
   458             if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
       
   459                 if len(value) == 0:
       
   460                     continue # avoid crash with empty IN clause
       
   461                 elif len(value) == 1:
       
   462                     value = next(iter(value))
       
   463                 else:
       
   464                     # prepare IN clause
       
   465                     pendingrels.append( (attr, role, value) )
       
   466                     continue
       
   467             if rschema.final: # attribute
       
   468                 relations.append('X %s %%(%s)s' % (attr, attr))
       
   469                 attrcache[attr] = value
       
   470             elif value is None:
       
   471                 pendingrels.append( (attr, role, value) )
       
   472             else:
       
   473                 rvar = attr.upper()
       
   474                 if role == 'object':
       
   475                     relations.append('%s %s X' % (rvar, attr))
       
   476                 else:
       
   477                     relations.append('X %s %s' % (attr, rvar))
       
   478                 restriction = '%s eid %%(%s)s' % (rvar, attr)
       
   479                 if not restriction in restrictions:
       
   480                     restrictions.add(restriction)
       
   481                 if hasattr(value, 'eid'):
       
   482                     value = value.eid
       
   483             qargs[attr] = value
       
   484         rql = u''
       
   485         if relations:
       
   486             rql += ', '.join(relations)
       
   487         if restrictions:
       
   488             rql += ' WHERE %s' % ', '.join(restrictions)
       
   489         return rql, qargs, pendingrels, attrcache
       
   490 
       
   491     @classmethod
       
   492     def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
       
   493         for attr, role, values in pendingrels:
       
   494             if role == 'object':
       
   495                 restr = 'Y %s X' % attr
       
   496             else:
       
   497                 restr = 'X %s Y' % attr
       
   498             if values is None:
       
   499                 execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
       
   500                 continue
       
   501             execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
       
   502                 restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
       
   503                     {'x': eid}, build_descr=False)
       
   504 
       
   505     @classmethod
       
   506     def cw_instantiate(cls, execute, **kwargs):
       
   507         """add a new entity of this given type
       
   508 
       
   509         Example (in a shell session):
       
   510 
       
   511         >>> companycls = vreg['etypes'].etype_class('Company')
       
   512         >>> personcls = vreg['etypes'].etype_class('Person')
       
   513         >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
       
   514         >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
       
   515         ...                              works_for=c)
       
   516 
       
   517         You can also set relations where the entity has 'object' role by
       
   518         prefixing the relation name by 'reverse_'. Also, relation values may be
       
   519         an entity or eid, a list of entities or eids.
       
   520         """
       
   521         rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
       
   522         if rql:
       
   523             rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
       
   524         else:
       
   525             rql = 'INSERT %s X' % (cls.__regid__)
       
   526         try:
       
   527             created = execute(rql, qargs).get_entity(0, 0)
       
   528         except IndexError:
       
   529             raise Exception('could not create a %r with %r (%r)' %
       
   530                             (cls.__regid__, rql, qargs))
       
   531         created._cw_update_attr_cache(attrcache)
       
   532         cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
       
   533         return created
       
   534 
       
   535     def __init__(self, req, rset=None, row=None, col=0):
       
   536         AppObject.__init__(self, req, rset=rset, row=row, col=col)
       
   537         self._cw_related_cache = {}
       
   538         self._cw_adapters_cache = {}
       
   539         if rset is not None:
       
   540             self.eid = rset[row][col]
       
   541         else:
       
   542             self.eid = None
       
   543         self._cw_is_saved = True
       
   544         self.cw_attr_cache = {}
       
   545 
       
   546     def __repr__(self):
       
   547         return '<Entity %s %s %s at %s>' % (
       
   548             self.e_schema, self.eid, list(self.cw_attr_cache), id(self))
       
   549 
       
   550     def __lt__(self, other):
       
   551         raise NotImplementedError('comparison not implemented for %s' % self.__class__)
       
   552 
       
   553     def __eq__(self, other):
       
   554         if isinstance(self.eid, integer_types):
       
   555             return self.eid == other.eid
       
   556         return self is other
       
   557 
       
   558     def __hash__(self):
       
   559         if isinstance(self.eid, integer_types):
       
   560             return self.eid
       
   561         return super(Entity, self).__hash__()
       
   562 
       
   563     def _cw_update_attr_cache(self, attrcache):
       
   564         trdata = self._cw.transaction_data
       
   565         uncached_attrs = trdata.get('%s.storage-special-process-attrs' % self.eid, set())
       
   566         uncached_attrs.update(trdata.get('%s.dont-cache-attrs' % self.eid, set()))
       
   567         for attr in uncached_attrs:
       
   568             attrcache.pop(attr, None)
       
   569             self.cw_attr_cache.pop(attr, None)
       
   570         self.cw_attr_cache.update(attrcache)
       
   571 
       
   572     def _cw_dont_cache_attribute(self, attr, repo_side=False):
       
   573         """Called when some attribute has been transformed by a *storage*,
       
   574         hence the original value should not be cached **by anyone**.
       
   575 
       
   576         For example we have a special "fs_importing" mode in BFSS
       
   577         where a file path is given as attribute value and stored as is
       
   578         in the data base. Later access to the attribute will provide
       
   579         the content of the file at the specified path. We do not want
       
   580         the "filepath" value to be cached.
       
   581 
       
   582         """
       
   583         trdata = self._cw.transaction_data
       
   584         trdata.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
       
   585         if repo_side:
       
   586             trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
       
   587 
       
   588     def __json_encode__(self):
       
   589         """custom json dumps hook to dump the entity's eid
       
   590         which is not part of dict structure itself
       
   591         """
       
   592         dumpable = self.cw_attr_cache.copy()
       
   593         dumpable['eid'] = self.eid
       
   594         return dumpable
       
   595 
       
   596     def cw_adapt_to(self, interface):
       
   597         """return an adapter the entity to the given interface name.
       
   598 
       
   599         return None if it can not be adapted.
       
   600         """
       
   601         cache = self._cw_adapters_cache
       
   602         try:
       
   603             return cache[interface]
       
   604         except KeyError:
       
   605             adapter = self._cw.vreg['adapters'].select_or_none(
       
   606                 interface, self._cw, entity=self)
       
   607             cache[interface] = adapter
       
   608             return adapter
       
   609 
       
   610     def has_eid(self): # XXX cw_has_eid
       
   611         """return True if the entity has an attributed eid (False
       
   612         meaning that the entity has to be created
       
   613         """
       
   614         try:
       
   615             int(self.eid)
       
   616             return True
       
   617         except (ValueError, TypeError):
       
   618             return False
       
   619 
       
   620     def cw_is_saved(self):
       
   621         """during entity creation, there is some time during which the entity
       
   622         has an eid attributed though it's not saved (eg during
       
   623         'before_add_entity' hooks). You can use this method to ensure the entity
       
   624         has an eid *and* is saved in its source.
       
   625         """
       
   626         return self.has_eid() and self._cw_is_saved
       
   627 
       
   628     @cached
       
   629     def cw_metainformation(self):
       
   630         metas = self._cw.entity_metas(self.eid)
       
   631         metas['source'] = self._cw.source_defs()[metas['source']]
       
   632         return metas
       
   633 
       
   634     def cw_check_perm(self, action):
       
   635         self.e_schema.check_perm(self._cw, action, eid=self.eid)
       
   636 
       
   637     def cw_has_perm(self, action):
       
   638         return self.e_schema.has_perm(self._cw, action, eid=self.eid)
       
   639 
       
   640     def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
       
   641         """shortcut to apply a view on this entity"""
       
   642         if initargs is None:
       
   643             initargs = kwargs
       
   644         else:
       
   645             initargs.update(kwargs)
       
   646         view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
       
   647                                                 row=self.cw_row, col=self.cw_col,
       
   648                                                 **initargs)
       
   649         return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
       
   650 
       
   651     def absolute_url(self, *args, **kwargs): # XXX cw_url
       
   652         """return an absolute url to view this entity"""
       
   653         # use *args since we don't want first argument to be "anonymous" to
       
   654         # avoid potential clash with kwargs
       
   655         if args:
       
   656             assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
       
   657             method = args[0]
       
   658         else:
       
   659             method = None
       
   660         # in linksearch mode, we don't want external urls else selecting
       
   661         # the object for use in the relation is tricky
       
   662         # XXX search_state is web specific
       
   663         use_ext_id = False
       
   664         if 'base_url' not in kwargs and \
       
   665                getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
       
   666             sourcemeta = self.cw_metainformation()['source']
       
   667             if sourcemeta.get('use-cwuri-as-url'):
       
   668                 return self.cwuri # XXX consider kwargs?
       
   669             if sourcemeta.get('base-url'):
       
   670                 kwargs['base_url'] = sourcemeta['base-url']
       
   671                 use_ext_id = True
       
   672         if method in (None, 'view'):
       
   673             kwargs['_restpath'] = self.rest_path(use_ext_id)
       
   674         else:
       
   675             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
       
   676         return self._cw.build_url(method, **kwargs)
       
   677 
       
   678     def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
       
   679         """returns a REST-like (relative) path for this entity"""
       
   680         mainattr, needcheck = self.cw_rest_attr_info()
       
   681         etype = str(self.e_schema)
       
   682         path = etype.lower()
       
   683         fallback = False
       
   684         if mainattr != 'eid':
       
   685             value = getattr(self, mainattr)
       
   686             if not can_use_rest_path(value):
       
   687                 mainattr = 'eid'
       
   688                 path = None
       
   689             elif needcheck:
       
   690                 # make sure url is not ambiguous
       
   691                 try:
       
   692                     nbresults = self.__unique
       
   693                 except AttributeError:
       
   694                     rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (
       
   695                         etype, mainattr)
       
   696                     nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
       
   697                 if nbresults != 1: # ambiguity?
       
   698                     mainattr = 'eid'
       
   699                     path = None
       
   700         if mainattr == 'eid':
       
   701             if use_ext_eid:
       
   702                 value = self.cw_metainformation()['extid']
       
   703             else:
       
   704                 value = self.eid
       
   705         if path is None:
       
   706             # fallback url: <base-url>/<eid> url is used as cw entities uri,
       
   707             # prefer it to <base-url>/<etype>/eid/<eid>
       
   708             return text_type(value)
       
   709         return u'%s/%s' % (path, self._cw.url_quote(value))
       
   710 
       
   711     def cw_attr_metadata(self, attr, metadata):
       
   712         """return a metadata for an attribute (None if unspecified)"""
       
   713         value = getattr(self, '%s_%s' % (attr, metadata), None)
       
   714         if value is None and metadata == 'encoding':
       
   715             value = self._cw.vreg.property_value('ui.encoding')
       
   716         return value
       
   717 
       
   718     def printable_value(self, attr, value=_marker, attrtype=None,
       
   719                         format='text/html', displaytime=True): # XXX cw_printable_value
       
   720         """return a displayable value (i.e. unicode string) which may contains
       
   721         html tags
       
   722         """
       
   723         attr = str(attr)
       
   724         if value is _marker:
       
   725             value = getattr(self, attr)
       
   726         if isinstance(value, string_types):
       
   727             value = value.strip()
       
   728         if value is None or value == '': # don't use "not", 0 is an acceptable value
       
   729             return u''
       
   730         if attrtype is None:
       
   731             attrtype = self.e_schema.destination(attr)
       
   732         props = self.e_schema.rdef(attr)
       
   733         if attrtype == 'String':
       
   734             # internalinalized *and* formatted string such as schema
       
   735             # description...
       
   736             if props.internationalizable:
       
   737                 value = self._cw._(value)
       
   738             attrformat = self.cw_attr_metadata(attr, 'format')
       
   739             if attrformat:
       
   740                 return self._cw_mtc_transform(value, attrformat, format,
       
   741                                               self._cw.encoding)
       
   742         elif attrtype == 'Bytes':
       
   743             attrformat = self.cw_attr_metadata(attr, 'format')
       
   744             if attrformat:
       
   745                 encoding = self.cw_attr_metadata(attr, 'encoding')
       
   746                 return self._cw_mtc_transform(value.getvalue(), attrformat, format,
       
   747                                               encoding)
       
   748             return u''
       
   749         value = self._cw.printable_value(attrtype, value, props,
       
   750                                          displaytime=displaytime)
       
   751         if format == 'text/html':
       
   752             value = xml_escape(value)
       
   753         return value
       
   754 
       
   755     def _cw_mtc_transform(self, data, format, target_format, encoding,
       
   756                           _engine=ENGINE):
       
   757         trdata = TransformData(data, format, encoding, appobject=self)
       
   758         data = _engine.convert(trdata, target_format).decode()
       
   759         if target_format == 'text/html':
       
   760             data = soup2xhtml(data, self._cw.encoding)
       
   761         return data
       
   762 
       
   763     # entity cloning ##########################################################
       
   764 
       
   765     def copy_relations(self, ceid): # XXX cw_copy_relations
       
   766         """copy relations of the object with the given eid on this
       
   767         object (this method is called on the newly created copy, and
       
   768         ceid designates the original entity).
       
   769 
       
   770         By default meta and composite relations are skipped.
       
   771         Overrides this if you want another behaviour
       
   772         """
       
   773         assert self.has_eid()
       
   774         execute = self._cw.execute
       
   775         skip_copy_for = {'subject': set(), 'object': set()}
       
   776         for rtype in self.skip_copy_for:
       
   777             skip_copy_for['subject'].add(rtype)
       
   778             warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
       
   779                  'use cw_skip_for instead with list of couples (rtype, role)' % self.cw_etype,
       
   780                  DeprecationWarning)
       
   781         for rtype, role in self.cw_skip_copy_for:
       
   782             assert role in ('subject', 'object'), role
       
   783             skip_copy_for[role].add(rtype)
       
   784         for rschema in self.e_schema.subject_relations():
       
   785             if rschema.type in skip_copy_for['subject']:
       
   786                 continue
       
   787             if rschema.final or rschema.meta:
       
   788                 continue
       
   789             # skip already defined relations
       
   790             if getattr(self, rschema.type):
       
   791                 continue
       
   792             # XXX takefirst=True to remove warning triggered by ambiguous relations
       
   793             rdef = self.e_schema.rdef(rschema, takefirst=True)
       
   794             # skip composite relation
       
   795             if rdef.composite:
       
   796                 continue
       
   797             # skip relation with card in ?1 else we either change the copied
       
   798             # object (inlined relation) or inserting some inconsistency
       
   799             if rdef.cardinality[1] in '?1':
       
   800                 continue
       
   801             rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
       
   802                 rschema.type, rschema.type)
       
   803             execute(rql, {'x': self.eid, 'y': ceid})
       
   804             self.cw_clear_relation_cache(rschema.type, 'subject')
       
   805         for rschema in self.e_schema.object_relations():
       
   806             if rschema.meta:
       
   807                 continue
       
   808             # skip already defined relations
       
   809             if self.related(rschema.type, 'object'):
       
   810                 continue
       
   811             if rschema.type in skip_copy_for['object']:
       
   812                 continue
       
   813             # XXX takefirst=True to remove warning triggered by ambiguous relations
       
   814             rdef = self.e_schema.rdef(rschema, 'object', takefirst=True)
       
   815             # skip composite relation
       
   816             if rdef.composite:
       
   817                 continue
       
   818             # skip relation with card in ?1 else we either change the copied
       
   819             # object (inlined relation) or inserting some inconsistency
       
   820             if rdef.cardinality[0] in '?1':
       
   821                 continue
       
   822             rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
       
   823                 rschema.type, rschema.type)
       
   824             execute(rql, {'x': self.eid, 'y': ceid})
       
   825             self.cw_clear_relation_cache(rschema.type, 'object')
       
   826 
       
   827     # data fetching methods ###################################################
       
   828 
       
   829     @cached
       
   830     def as_rset(self): # XXX .cw_as_rset
       
   831         """returns a resultset containing `self` information"""
       
   832         rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
       
   833                          {'x': self.eid}, [(self.cw_etype,)])
       
   834         rset.req = self._cw
       
   835         return rset
       
   836 
       
   837     def _cw_to_complete_relations(self):
       
   838         """by default complete final relations to when calling .complete()"""
       
   839         for rschema in self.e_schema.subject_relations():
       
   840             if rschema.final:
       
   841                 continue
       
   842             targets = rschema.objects(self.e_schema)
       
   843             if rschema.inlined:
       
   844                 matching_groups = self._cw.user.matching_groups
       
   845                 if all(matching_groups(e.get_groups('read')) and
       
   846                        rschema.rdef(self.e_schema, e).get_groups('read')
       
   847                        for e in targets):
       
   848                     yield rschema, 'subject'
       
   849 
       
   850     def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
       
   851         for rschema, attrschema in self.e_schema.attribute_definitions():
       
   852             # skip binary data by default
       
   853             if skip_bytes and attrschema.type == 'Bytes':
       
   854                 continue
       
   855             attr = rschema.type
       
   856             if attr == 'eid':
       
   857                 continue
       
   858             # password retrieval is blocked at the repository server level
       
   859             rdef = rschema.rdef(self.e_schema, attrschema)
       
   860             if not self._cw.user.matching_groups(rdef.get_groups('read')) \
       
   861                    or (attrschema.type == 'Password' and skip_pwd):
       
   862                 self.cw_attr_cache[attr] = None
       
   863                 continue
       
   864             yield attr
       
   865 
       
   866     _cw_completed = False
       
   867     def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
       
   868         """complete this entity by adding missing attributes (i.e. query the
       
   869         repository to fill the entity)
       
   870 
       
   871         :type skip_bytes: bool
       
   872         :param skip_bytes:
       
   873           if true, attribute of type Bytes won't be considered
       
   874         """
       
   875         assert self.has_eid()
       
   876         if self._cw_completed:
       
   877             return
       
   878         if attributes is None:
       
   879             self._cw_completed = True
       
   880         varmaker = rqlvar_maker()
       
   881         V = next(varmaker)
       
   882         rql = ['WHERE %s eid %%(x)s' % V]
       
   883         selected = []
       
   884         for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
       
   885             # if attribute already in entity, nothing to do
       
   886             if attr in self.cw_attr_cache:
       
   887                 continue
       
   888             # case where attribute must be completed, but is not yet in entity
       
   889             var = next(varmaker)
       
   890             rql.append('%s %s %s' % (V, attr, var))
       
   891             selected.append((attr, var))
       
   892         # +1 since this doesn't include the main variable
       
   893         lastattr = len(selected) + 1
       
   894         # don't fetch extra relation if attributes specified or of the entity is
       
   895         # coming from an external source (may lead to error)
       
   896         if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
       
   897             # fetch additional relations (restricted to 0..1 relations)
       
   898             for rschema, role in self._cw_to_complete_relations():
       
   899                 rtype = rschema.type
       
   900                 if self.cw_relation_cached(rtype, role):
       
   901                     continue
       
   902                 # at this point we suppose that:
       
   903                 # * this is a inlined relation
       
   904                 # * entity (self) is the subject
       
   905                 # * user has read perm on the relation and on the target entity
       
   906                 assert rschema.inlined
       
   907                 assert role == 'subject'
       
   908                 var = next(varmaker)
       
   909                 # keep outer join anyway, we don't want .complete to crash on
       
   910                 # missing mandatory relation (see #1058267)
       
   911                 rql.append('%s %s %s?' % (V, rtype, var))
       
   912                 selected.append(((rtype, role), var))
       
   913         if selected:
       
   914             # select V, we need it as the left most selected variable
       
   915             # if some outer join are included to fetch inlined relations
       
   916             rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
       
   917                                     ','.join(rql))
       
   918             try:
       
   919                 rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
       
   920             except IndexError:
       
   921                 raise Exception('unable to fetch attributes for entity with eid %s'
       
   922                                 % self.eid)
       
   923             # handle attributes
       
   924             for i in range(1, lastattr):
       
   925                 self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
       
   926             # handle relations
       
   927             for i in range(lastattr, len(rset)):
       
   928                 rtype, role = selected[i-1][0]
       
   929                 value = rset[i]
       
   930                 if value is None:
       
   931                     rrset = ResultSet([], rql, {'x': self.eid})
       
   932                     rrset.req = self._cw
       
   933                 else:
       
   934                     rrset = self._cw.eid_rset(value)
       
   935                 self.cw_set_relation_cache(rtype, role, rrset)
       
   936 
       
   937     def cw_attr_value(self, name):
       
   938         """get value for the attribute relation <name>, query the repository
       
   939         to get the value if necessary.
       
   940 
       
   941         :type name: str
       
   942         :param name: name of the attribute to get
       
   943         """
       
   944         try:
       
   945             return self.cw_attr_cache[name]
       
   946         except KeyError:
       
   947             if not self.cw_is_saved():
       
   948                 return None
       
   949             rql = "Any A WHERE X eid %%(x)s, X %s A" % name
       
   950             try:
       
   951                 rset = self._cw.execute(rql, {'x': self.eid})
       
   952             except Unauthorized:
       
   953                 self.cw_attr_cache[name] = value = None
       
   954             else:
       
   955                 assert rset.rowcount <= 1, (self, rql, rset.rowcount)
       
   956                 try:
       
   957                     self.cw_attr_cache[name] = value = rset.rows[0][0]
       
   958                 except IndexError:
       
   959                     # probably a multisource error
       
   960                     self.critical("can't get value for attribute %s of entity with eid %s",
       
   961                                   name, self.eid)
       
   962                     if self.e_schema.destination(name) == 'String':
       
   963                         self.cw_attr_cache[name] = value = self._cw._('unaccessible')
       
   964                     else:
       
   965                         self.cw_attr_cache[name] = value = None
       
   966             return value
       
   967 
       
   968     def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related
       
   969                 safe=False, targettypes=None):
       
   970         """returns a resultset of related entities
       
   971 
       
   972         :param rtype:
       
   973           the name of the relation, aka relation type
       
   974         :param role:
       
   975           the role played by 'self' in the relation ('subject' or 'object')
       
   976         :param limit:
       
   977           resultset's maximum size
       
   978         :param entities:
       
   979           if True, the entites are returned; if False, a result set is returned
       
   980         :param safe:
       
   981           if True, an empty rset/list of entities will be returned in case of
       
   982           :exc:`Unauthorized`, else (the default), the exception is propagated
       
   983         :param targettypes:
       
   984           a tuple of target entity types to restrict the query
       
   985         """
       
   986         rtype = str(rtype)
       
   987         # Caching restricted/limited results is best avoided.
       
   988         cacheable = limit is None and targettypes is None
       
   989         if cacheable:
       
   990             cache_key = '%s_%s' % (rtype, role)
       
   991             if cache_key in self._cw_related_cache:
       
   992                 return self._cw_related_cache[cache_key][entities]
       
   993         if not self.has_eid():
       
   994             if entities:
       
   995                 return []
       
   996             return self._cw.empty_rset()
       
   997         rql = self.cw_related_rql(rtype, role, limit=limit, targettypes=targettypes)
       
   998         try:
       
   999             rset = self._cw.execute(rql, {'x': self.eid})
       
  1000         except Unauthorized:
       
  1001             if not safe:
       
  1002                 raise
       
  1003             rset = self._cw.empty_rset()
       
  1004         if entities:
       
  1005             if cacheable:
       
  1006                 self.cw_set_relation_cache(rtype, role, rset)
       
  1007                 return self.related(rtype, role, entities=entities)
       
  1008             return list(rset.entities())
       
  1009         else:
       
  1010             return rset
       
  1011 
       
  1012     def cw_related_rql(self, rtype, role='subject', targettypes=None, limit=None):
       
  1013         vreg = self._cw.vreg
       
  1014         rschema = vreg.schema[rtype]
       
  1015         select = Select()
       
  1016         mainvar, evar = select.get_variable('X'), select.get_variable('E')
       
  1017         select.add_selected(mainvar)
       
  1018         if limit is not None:
       
  1019             select.set_limit(limit)
       
  1020         select.add_eid_restriction(evar, 'x', 'Substitute')
       
  1021         if role == 'subject':
       
  1022             rel = make_relation(evar, rtype, (mainvar,), VariableRef)
       
  1023             select.add_restriction(rel)
       
  1024             if targettypes is None:
       
  1025                 targettypes = rschema.objects(self.e_schema)
       
  1026             else:
       
  1027                 select.add_constant_restriction(mainvar, 'is',
       
  1028                                                 targettypes, 'etype')
       
  1029             gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
       
  1030         else:
       
  1031             rel = make_relation(mainvar, rtype, (evar,), VariableRef)
       
  1032             select.add_restriction(rel)
       
  1033             if targettypes is None:
       
  1034                 targettypes = rschema.subjects(self.e_schema)
       
  1035             else:
       
  1036                 select.add_constant_restriction(mainvar, 'is', targettypes,
       
  1037                                                 'etype')
       
  1038             gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
       
  1039         etypecls = vreg['etypes'].etype_class(targettypes[0])
       
  1040         if len(targettypes) > 1:
       
  1041             fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
       
  1042             self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
       
  1043                                          targettypes, vreg.schema)
       
  1044         else:
       
  1045             fetchattrs = etypecls.fetch_attrs
       
  1046         etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
       
  1047                              settype=False)
       
  1048         # optimisation: remove ORDERBY if cardinality is 1 or ? (though
       
  1049         # greater_card return 1 for those both cases)
       
  1050         if gcard == '1':
       
  1051             select.remove_sort_terms()
       
  1052         elif not select.orderby:
       
  1053             # if modification_date is already retrieved, we use it instead
       
  1054             # of adding another variable for sorting. This should not be
       
  1055             # problematic, but it is with sqlserver, see ticket #694445
       
  1056             for rel in select.where.get_nodes(RqlRelation):
       
  1057                 if (rel.r_type == 'modification_date'
       
  1058                     and rel.children[0].variable == mainvar
       
  1059                     and rel.children[1].operator == '='):
       
  1060                     var = rel.children[1].children[0].variable
       
  1061                     select.add_sort_var(var, asc=False)
       
  1062                     break
       
  1063             else:
       
  1064                 mdvar = select.make_variable()
       
  1065                 rel = make_relation(mainvar, 'modification_date',
       
  1066                                     (mdvar,), VariableRef)
       
  1067                 select.add_restriction(rel)
       
  1068                 select.add_sort_var(mdvar, asc=False)
       
  1069         return select.as_string()
       
  1070 
       
  1071     # generic vocabulary methods ##############################################
       
  1072 
       
  1073     def cw_linkable_rql(self, rtype, targettype, role, ordermethod=None,
       
  1074                         vocabconstraints=True, lt_infos={}, limit=None):
       
  1075         """build a rql to fetch targettype entities either related or unrelated
       
  1076         to this entity using (rtype, role) relation.
       
  1077 
       
  1078         Consider relation permissions so that returned entities may be actually
       
  1079         linked by `rtype`.
       
  1080 
       
  1081         `lt_infos` are supplementary informations, usually coming from __linkto
       
  1082         parameter, that can help further restricting the results in case current
       
  1083         entity is not yet created. It is a dict describing entities the current
       
  1084         entity will be linked to, which keys are (rtype, role) tuples and values
       
  1085         are a list of eids.
       
  1086         """
       
  1087         return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
       
  1088                                              vocabconstraints=vocabconstraints,
       
  1089                                              lt_infos=lt_infos, limit=limit,
       
  1090                                              unrelated_only=False)
       
  1091 
       
  1092     def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
       
  1093                          vocabconstraints=True, lt_infos={}, limit=None):
       
  1094         """build a rql to fetch `targettype` entities unrelated to this entity
       
  1095         using (rtype, role) relation.
       
  1096 
       
  1097         Consider relation permissions so that returned entities may be actually
       
  1098         linked by `rtype`.
       
  1099 
       
  1100         `lt_infos` are supplementary informations, usually coming from __linkto
       
  1101         parameter, that can help further restricting the results in case current
       
  1102         entity is not yet created. It is a dict describing entities the current
       
  1103         entity will be linked to, which keys are (rtype, role) tuples and values
       
  1104         are a list of eids.
       
  1105         """
       
  1106         return self._cw_compute_linkable_rql(rtype, targettype, role, ordermethod=None,
       
  1107                                              vocabconstraints=vocabconstraints,
       
  1108                                              lt_infos=lt_infos, limit=limit,
       
  1109                                              unrelated_only=True)
       
  1110 
       
  1111     def _cw_compute_linkable_rql(self, rtype, targettype, role, ordermethod=None,
       
  1112                                  vocabconstraints=True, lt_infos={}, limit=None,
       
  1113                                  unrelated_only=False):
       
  1114         """build a rql to fetch `targettype` entities that may be related to
       
  1115         this entity using the (rtype, role) relation.
       
  1116 
       
  1117         By default (unrelated_only=False), this includes the already linked
       
  1118         entities as well as the unrelated ones. If `unrelated_only` is True, the
       
  1119         rql filters out the already related entities.
       
  1120         """
       
  1121         ordermethod = ordermethod or 'fetch_unrelated_order'
       
  1122         rschema = self._cw.vreg.schema.rschema(rtype)
       
  1123         rdef = rschema.role_rdef(self.e_schema, targettype, role)
       
  1124         rewriter = RQLRewriter(self._cw)
       
  1125         select = Select()
       
  1126         # initialize some variables according to the `role` of `self` in the
       
  1127         # relation (variable names must respect constraints conventions):
       
  1128         # * variable for myself (`evar`)
       
  1129         # * variable for searched entities (`searchvedvar`)
       
  1130         if role == 'subject':
       
  1131             evar = subjvar = select.get_variable('S')
       
  1132             searchedvar = objvar = select.get_variable('O')
       
  1133         else:
       
  1134             searchedvar = subjvar = select.get_variable('S')
       
  1135             evar = objvar = select.get_variable('O')
       
  1136         select.add_selected(searchedvar)
       
  1137         if limit is not None:
       
  1138             select.set_limit(limit)
       
  1139         # initialize some variables according to `self` existence
       
  1140         if rdef.role_cardinality(neg_role(role)) in '?1':
       
  1141             # if cardinality in '1?', we want a target entity which isn't
       
  1142             # already linked using this relation
       
  1143             variable = select.make_variable()
       
  1144             if role == 'subject':
       
  1145                 rel = make_relation(variable, rtype, (searchedvar,), VariableRef)
       
  1146             else:
       
  1147                 rel = make_relation(searchedvar, rtype, (variable,), VariableRef)
       
  1148             select.add_restriction(Not(rel))
       
  1149         elif self.has_eid() and unrelated_only:
       
  1150             # elif we have an eid, we don't want a target entity which is
       
  1151             # already linked to ourself through this relation
       
  1152             rel = make_relation(subjvar, rtype, (objvar,), VariableRef)
       
  1153             select.add_restriction(Not(rel))
       
  1154         if self.has_eid():
       
  1155             rel = make_relation(evar, 'eid', ('x', 'Substitute'), Constant)
       
  1156             select.add_restriction(rel)
       
  1157             args = {'x': self.eid}
       
  1158             if role == 'subject':
       
  1159                 sec_check_args = {'fromeid': self.eid}
       
  1160             else:
       
  1161                 sec_check_args = {'toeid': self.eid}
       
  1162             existant = None # instead of 'SO', improve perfs
       
  1163         else:
       
  1164             args = {}
       
  1165             sec_check_args = {}
       
  1166             existant = searchedvar.name
       
  1167             # undefine unused evar, or the type resolver will consider it
       
  1168             select.undefine_variable(evar)
       
  1169         # retrieve entity class for targettype to compute base rql
       
  1170         etypecls = self._cw.vreg['etypes'].etype_class(targettype)
       
  1171         etypecls.fetch_rqlst(self._cw.user, select, searchedvar,
       
  1172                              ordermethod=ordermethod)
       
  1173         # from now on, we need variable type resolving
       
  1174         self._cw.vreg.solutions(self._cw, select, args)
       
  1175         # insert RQL expressions for schema constraints into the rql syntax tree
       
  1176         if vocabconstraints:
       
  1177             cstrcls = (RQLVocabularyConstraint, RQLConstraint)
       
  1178         else:
       
  1179             cstrcls = RQLConstraint
       
  1180         lt_infos = pruned_lt_info(self.e_schema, lt_infos or {})
       
  1181         # if there are still lt_infos, use set to keep track of added eid
       
  1182         # relations (adding twice the same eid relation is incorrect RQL)
       
  1183         eidvars = set()
       
  1184         for cstr in rdef.constraints:
       
  1185             # consider constraint.mainvars to check if constraint apply
       
  1186             if isinstance(cstr, cstrcls) and searchedvar.name in cstr.mainvars:
       
  1187                 if not self.has_eid():
       
  1188                     if lt_infos:
       
  1189                         # we can perhaps further restrict with linkto infos using
       
  1190                         # a custom constraint built from cstr and lt_infos
       
  1191                         cstr = build_cstr_with_linkto_infos(
       
  1192                             cstr, args, searchedvar, evar, lt_infos, eidvars)
       
  1193                         if cstr is None:
       
  1194                             continue # could not build constraint -> discard
       
  1195                     elif evar.name in cstr.mainvars:
       
  1196                         continue
       
  1197                 # compute a varmap suitable to RQLRewriter.rewrite argument
       
  1198                 varmap = dict((v, v) for v in (searchedvar.name, evar.name)
       
  1199                               if v in select.defined_vars and v in cstr.mainvars)
       
  1200                 # rewrite constraint by constraint since we want a AND between
       
  1201                 # expressions.
       
  1202                 rewriter.rewrite(select, [(varmap, (cstr,))], args, existant)
       
  1203         # insert security RQL expressions granting the permission to 'add' the
       
  1204         # relation into the rql syntax tree, if necessary
       
  1205         rqlexprs = rdef.get_rqlexprs('add')
       
  1206         if not self.has_eid():
       
  1207             rqlexprs = [rqlexpr for rqlexpr in rqlexprs
       
  1208                         if searchedvar.name in rqlexpr.mainvars]
       
  1209         if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
       
  1210             # compute a varmap suitable to RQLRewriter.rewrite argument
       
  1211             varmap = dict((v, v) for v in (searchedvar.name, evar.name)
       
  1212                           if v in select.defined_vars)
       
  1213             # rewrite all expressions at once since we want a OR between them.
       
  1214             rewriter.rewrite(select, [(varmap, rqlexprs)], args, existant)
       
  1215         # ensure we have an order defined
       
  1216         if not select.orderby:
       
  1217             select.add_sort_var(select.defined_vars[searchedvar.name])
       
  1218         # we're done, turn the rql syntax tree as a string
       
  1219         rql = select.as_string()
       
  1220         return rql, args
       
  1221 
       
  1222     def unrelated(self, rtype, targettype, role='subject', limit=None,
       
  1223                   ordermethod=None, lt_infos={}): # XXX .cw_unrelated
       
  1224         """return a result set of target type objects that may be related
       
  1225         by a given relation, with self as subject or object
       
  1226         """
       
  1227         try:
       
  1228             rql, args = self.cw_unrelated_rql(rtype, targettype, role, limit=limit,
       
  1229                                               ordermethod=ordermethod, lt_infos=lt_infos)
       
  1230         except Unauthorized:
       
  1231             return self._cw.empty_rset()
       
  1232         return self._cw.execute(rql, args)
       
  1233 
       
  1234     # relations cache handling #################################################
       
  1235 
       
  1236     def cw_relation_cached(self, rtype, role):
       
  1237         """return None if the given relation isn't already cached on the
       
  1238         instance, else the content of the cache (a 2-uple (rset, entities)).
       
  1239         """
       
  1240         return self._cw_related_cache.get('%s_%s' % (rtype, role))
       
  1241 
       
  1242     def cw_set_relation_cache(self, rtype, role, rset):
       
  1243         """set cached values for the given relation"""
       
  1244         if rset:
       
  1245             related = list(rset.entities(0))
       
  1246             rschema = self._cw.vreg.schema.rschema(rtype)
       
  1247             if role == 'subject':
       
  1248                 rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
       
  1249                 target = 'object'
       
  1250             else:
       
  1251                 rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
       
  1252                 target = 'subject'
       
  1253             if rcard in '?1':
       
  1254                 for rentity in related:
       
  1255                     rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
       
  1256                         self.as_rset(), (self,))
       
  1257         else:
       
  1258             related = ()
       
  1259         self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
       
  1260 
       
  1261     def cw_clear_relation_cache(self, rtype=None, role=None):
       
  1262         """clear cached values for the given relation or the entire cache if
       
  1263         no relation is given
       
  1264         """
       
  1265         if rtype is None:
       
  1266             self._cw_related_cache.clear()
       
  1267             self._cw_adapters_cache.clear()
       
  1268         else:
       
  1269             assert role
       
  1270             self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
       
  1271 
       
  1272     def cw_clear_all_caches(self):
       
  1273         """flush all caches on this entity. Further attributes/relations access
       
  1274         will triggers new database queries to get back values.
       
  1275 
       
  1276         If you use custom caches on your entity class (take care to @cached!),
       
  1277         you should override this method to clear them as well.
       
  1278         """
       
  1279         # clear attributes cache
       
  1280         self._cw_completed = False
       
  1281         self.cw_attr_cache.clear()
       
  1282         # clear relations cache
       
  1283         self.cw_clear_relation_cache()
       
  1284         # rest path unique cache
       
  1285         try:
       
  1286             del self.__unique
       
  1287         except AttributeError:
       
  1288             pass
       
  1289 
       
  1290     # raw edition utilities ###################################################
       
  1291 
       
  1292     def cw_set(self, **kwargs):
       
  1293         """update this entity using given attributes / relation, working in the
       
  1294         same fashion as :meth:`cw_instantiate`.
       
  1295 
       
  1296         Example (in a shell session):
       
  1297 
       
  1298         >>> c = rql('Any X WHERE X is Company').get_entity(0, 0)
       
  1299         >>> p = rql('Any X WHERE X is Person').get_entity(0, 0)
       
  1300         >>> c.cw_set(name=u'Logilab')
       
  1301         >>> p.cw_set(firstname=u'John', lastname=u'Doe', works_for=c)
       
  1302 
       
  1303         You can also set relations where the entity has 'object' role by
       
  1304         prefixing the relation name by 'reverse_'.  Also, relation values may be
       
  1305         an entity or eid, a list of entities or eids, or None (meaning that all
       
  1306         relations of the given type from or to this object should be deleted).
       
  1307         """
       
  1308         assert kwargs
       
  1309         assert self.cw_is_saved(), "should not call set_attributes while entity "\
       
  1310                "hasn't been saved yet"
       
  1311         rql, qargs, pendingrels, attrcache = self._cw_build_entity_query(kwargs)
       
  1312         if rql:
       
  1313             rql = 'SET ' + rql
       
  1314             qargs['x'] = self.eid
       
  1315             if ' WHERE ' in rql:
       
  1316                 rql += ', X eid %(x)s'
       
  1317             else:
       
  1318                 rql += ' WHERE X eid %(x)s'
       
  1319             self._cw.execute(rql, qargs)
       
  1320         # update current local object _after_ the rql query to avoid
       
  1321         # interferences between the query execution itself and the cw_edited /
       
  1322         # skip_security machinery
       
  1323         self._cw_update_attr_cache(attrcache)
       
  1324         self._cw_handle_pending_relations(self.eid, pendingrels, self._cw.execute)
       
  1325         # XXX update relation cache
       
  1326 
       
  1327     def cw_delete(self, **kwargs):
       
  1328         assert self.has_eid(), self.eid
       
  1329         self._cw.execute('DELETE %s X WHERE X eid %%(x)s' % self.e_schema,
       
  1330                          {'x': self.eid}, **kwargs)
       
  1331 
       
  1332     # server side utilities ####################################################
       
  1333 
       
  1334     def _cw_clear_local_perm_cache(self, action):
       
  1335         for rqlexpr in self.e_schema.get_rqlexprs(action):
       
  1336             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
       
  1337 
       
  1338     # deprecated stuff #########################################################
       
  1339 
       
  1340     @deprecated('[3.16] use cw_set() instead of set_attributes()')
       
  1341     def set_attributes(self, **kwargs): # XXX cw_set_attributes
       
  1342         if kwargs:
       
  1343             self.cw_set(**kwargs)
       
  1344 
       
  1345     @deprecated('[3.16] use cw_set() instead of set_relations()')
       
  1346     def set_relations(self, **kwargs): # XXX cw_set_relations
       
  1347         """add relations to the given object. To set a relation where this entity
       
  1348         is the object of the relation, use 'reverse_'<relation> as argument name.
       
  1349 
       
  1350         Values may be an entity or eid, a list of entities or eids, or None
       
  1351         (meaning that all relations of the given type from or to this object
       
  1352         should be deleted).
       
  1353         """
       
  1354         if kwargs:
       
  1355             self.cw_set(**kwargs)
       
  1356 
       
  1357     @deprecated('[3.13] use entity.cw_clear_all_caches()')
       
  1358     def clear_all_caches(self):
       
  1359         return self.cw_clear_all_caches()
       
  1360 
       
  1361 
       
  1362 # attribute and relation descriptors ##########################################
       
  1363 
       
  1364 class Attribute(object):
       
  1365     """descriptor that controls schema attribute access"""
       
  1366 
       
  1367     def __init__(self, attrname):
       
  1368         assert attrname != 'eid'
       
  1369         self._attrname = attrname
       
  1370 
       
  1371     def __get__(self, eobj, eclass):
       
  1372         if eobj is None:
       
  1373             return self
       
  1374         return eobj.cw_attr_value(self._attrname)
       
  1375 
       
  1376     @deprecated('[3.10] assign to entity.cw_attr_cache[attr] or entity.cw_edited[attr]')
       
  1377     def __set__(self, eobj, value):
       
  1378         if hasattr(eobj, 'cw_edited') and not eobj.cw_edited.saved:
       
  1379             eobj.cw_edited[self._attrname] = value
       
  1380         else:
       
  1381             eobj.cw_attr_cache[self._attrname] = value
       
  1382 
       
  1383 
       
  1384 class Relation(object):
       
  1385     """descriptor that controls schema relation access"""
       
  1386 
       
  1387     def __init__(self, rschema, role):
       
  1388         self._rtype = rschema.type
       
  1389         self._role = role
       
  1390 
       
  1391     def __get__(self, eobj, eclass):
       
  1392         if eobj is None:
       
  1393             raise AttributeError('%s can only be accessed from instances'
       
  1394                                  % self._rtype)
       
  1395         return eobj.related(self._rtype, self._role, entities=True)
       
  1396 
       
  1397     def __set__(self, eobj, value):
       
  1398         raise NotImplementedError
       
  1399 
       
  1400 
       
  1401 from logging import getLogger
       
  1402 from cubicweb import set_log_methods
       
  1403 set_log_methods(Entity, getLogger('cubicweb.entity'))