entity.py 64.4 KB
Newer Older
1
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2
3
4
5
6
7
8
9
10
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
#
# CubicWeb is free software: you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free
# Software Foundation, either version 2.1 of the License, or (at your option)
# any later version.
#
11
# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
12
13
14
15
16
17
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
18
"""Base class for entity objects manipulated in clients"""
Adrien Di Mascio's avatar
Adrien Di Mascio committed
19
20
21

__docformat__ = "restructuredtext en"

22
from warnings import warn
23
from functools import partial
24

Adrien Di Mascio's avatar
Adrien Di Mascio committed
25
26
from logilab.common import interface
from logilab.common.decorators import cached
27
from logilab.common.deprecation import deprecated
28
from logilab.common.registry import yes
Sylvain Thénault's avatar
Sylvain Thénault committed
29
from logilab.mtconverter import TransformData, TransformError, xml_escape
30

Adrien Di Mascio's avatar
Adrien Di Mascio committed
31
from rql.utils import rqlvar_maker
32
33
34
from rql.stmts import Select
from rql.nodes import (Not, VariableRef, Constant, make_relation,
                       Relation as RqlRelation)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
35

36
from cubicweb import Unauthorized, typed_eid, neg_role
37
from cubicweb.utils import support_args
Adrien Di Mascio's avatar
Adrien Di Mascio committed
38
from cubicweb.rset import ResultSet
39
from cubicweb.appobject import AppObject
40
from cubicweb.req import _check_cw_unsafe
41
42
from cubicweb.schema import (RQLVocabularyConstraint, RQLConstraint,
                             GeneratedConstraint)
43
from cubicweb.rqlrewrite import RQLRewriter
44

45
from cubicweb.uilib import soup2xhtml
Sylvain Thénault's avatar
Sylvain Thénault committed
46
47
from cubicweb.mixins import MI_REL_TRIGGERS
from cubicweb.mttransforms import ENGINE
Adrien Di Mascio's avatar
Adrien Di Mascio committed
48
49
50
51
52
53

_marker = object()

def greater_card(rschema, subjtypes, objtypes, index):
    for subjtype in subjtypes:
        for objtype in objtypes:
Sylvain Thénault's avatar
Sylvain Thénault committed
54
            card = rschema.rdef(subjtype, objtype).cardinality[index]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
55
56
57
58
            if card in '+*':
                return card
    return '1'

59
60
61
62
63
64
65
66
67
68
69
def can_use_rest_path(value):
    """return True if value can be used at the end of a Rest URL path"""
    if value is None:
        return False
    value = unicode(value)
    # the check for ?, /, & are to prevent problems when running
    # behind Apache mod_proxy
    if value == u'' or u'?' in value or u'/' in value or u'&' in value:
        return False
    return True

70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
def rel_vars(rel):
    return ((isinstance(rel.children[0], VariableRef)
             and rel.children[0].variable or None),
            (isinstance(rel.children[1].children[0], VariableRef)
             and rel.children[1].children[0].variable or None)
            )

def rel_matches(rel, rtype, role, varname, operator='='):
    if rel.r_type == rtype and rel.children[1].operator == operator:
        same_role_var_idx = 0 if role == 'subject' else 1
        variables = rel_vars(rel)
        if variables[same_role_var_idx].name == varname:
            return variables[1 - same_role_var_idx]

def build_cstr_with_linkto_infos(cstr, args, searchedvar, evar,
                                 lt_infos, eidvars):
    """restrict vocabulary as much as possible in entity creation,
    based on infos provided by __linkto form param.

    Example based on following schema:

      class works_in(RelationDefinition):
          subject = 'CWUser'
          object = 'Lab'
          cardinality = '1*'
          constraints = [RQLConstraint('S in_group G, O welcomes G')]

      class welcomes(RelationDefinition):
          subject = 'Lab'
          object = 'CWGroup'

    If you create a CWUser in the "scientists" CWGroup you can show
    only the labs that welcome them using :

      lt_infos = {('in_group', 'subject'): 321}

    You get following restriction : 'O welcomes G, G eid 321'

    """
    st = cstr.snippet_rqlst.copy()
    # replace relations in ST by eid infos from linkto where possible
    for (info_rtype, info_role), eids in lt_infos.iteritems():
        eid = eids[0] # NOTE: we currently assume a pruned lt_info with only 1 eid
        for rel in st.iget_nodes(RqlRelation):
            targetvar = rel_matches(rel, info_rtype, info_role, evar.name)
            if targetvar is not None:
                if targetvar.name in eidvars:
                    rel.parent.remove(rel)
                else:
                    eidrel = make_relation(
                        targetvar, 'eid', (targetvar.name, 'Substitute'),
                        Constant)
                    rel.parent.replace(rel, eidrel)
                    args[targetvar.name] = eid
                    eidvars.add(targetvar.name)
    # if modified ST still contains evar references we must discard the
    # constraint, otherwise evar is unknown in the final rql query which can
    # lead to a SQL table cartesian product and multiple occurences of solutions
    evarname = evar.name
    for rel in st.iget_nodes(RqlRelation):
        for variable in rel_vars(rel):
            if variable and evarname == variable.name:
                return
    # else insert snippets into the global tree
    return GeneratedConstraint(st, cstr.mainvars - set(evarname))

def pruned_lt_info(eschema, lt_infos):
    pruned = {}
    for (lt_rtype, lt_role), eids in lt_infos.iteritems():
        # we can only use lt_infos describing relation with a cardinality
        # of value 1 towards the linked entity
        if not len(eids) == 1:
            continue
        lt_card = eschema.rdef(lt_rtype, lt_role).cardinality[
            0 if lt_role == 'subject' else 1]
        if lt_card not in '?1':
            continue
        pruned[(lt_rtype, lt_role)] = eids
    return pruned
149

150
class Entity(AppObject):
151
152
    """an entity instance has e_schema automagically set on
    the class and instances has access to their issuing cursor.
153

154
155
156
157
    A property is set for each attribute and relation on each entity's type
    class. Becare that among attributes, 'eid' is *NEITHER* stored in the
    dict containment (which acts as a cache for other attributes dynamically
    fetched)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
158
159
160
161

    :type e_schema: `cubicweb.schema.EntitySchema`
    :ivar e_schema: the entity's schema

162
163
164
165
    :type rest_attr: str
    :cvar rest_attr: indicates which attribute should be used to build REST urls
       If `None` is specified (the default), the first unique attribute will
       be used ('eid' if none found)
166

167
168
    :type cw_skip_copy_for: list
    :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
169
170
171
       that should be skipped when copying this kind of entity. Note that some
       relations such as composite relations or relations that have '?1' as
       object cardinality are always skipped.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
172
173
    """
    __registry__ = 'etypes'
174
    __select__ = yes()
175

176
    # class attributes that must be set in class definition
Adrien Di Mascio's avatar
Adrien Di Mascio committed
177
    rest_attr = None
178
    fetch_attrs = None
179
180
    skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
    cw_skip_copy_for = [('in_state', 'subject')]
181
182
    # class attributes set automatically at registration time
    e_schema = None
183

Adrien Di Mascio's avatar
Adrien Di Mascio committed
184
    @classmethod
185
    def __initialize__(cls, schema):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
186
187
188
        """initialize a specific entity class by adding descriptors to access
        entity type's attributes and relations
        """
189
        etype = cls.__regid__
Adrien Di Mascio's avatar
Adrien Di Mascio committed
190
        assert etype != 'Any', etype
191
        cls.e_schema = eschema = schema.eschema(etype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
192
        for rschema, _ in eschema.attribute_definitions():
193
            if rschema.type == 'eid':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
194
                continue
195
            setattr(cls, rschema.type, Attribute(rschema.type))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
196
        mixins = []
197
198
199
        for rschema, _, role in eschema.relation_definitions():
            if (rschema, role) in MI_REL_TRIGGERS:
                mixin = MI_REL_TRIGGERS[(rschema, role)]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
200
201
202
203
204
                if not (issubclass(cls, mixin) or mixin in mixins): # already mixed ?
                    mixins.append(mixin)
                for iface in getattr(mixin, '__implements__', ()):
                    if not interface.implements(cls, iface):
                        interface.extend(cls, iface)
205
            if role == 'subject':
206
                attr = rschema.type
Adrien Di Mascio's avatar
Adrien Di Mascio committed
207
            else:
208
                attr = 'reverse_%s' % rschema.type
209
            setattr(cls, attr, Relation(rschema, role))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
        if mixins:
211
212
213
214
215
216
217
218
219
220
221
222
            # see etype class instantation in cwvreg.ETypeRegistry.etype_class method:
            # due to class dumping, cls is the generated top level class with actual
            # user class as (only) parent. Since we want to be able to override mixins
            # method from this user class, we have to take care to insert mixins after that
            # class
            #
            # note that we don't plug mixins as user class parent since it causes pb
            # with some cases of entity classes inheritance.
            mixins.insert(0, cls.__bases__[0])
            mixins += cls.__bases__[1:]
            cls.__bases__ = tuple(mixins)
            cls.info('plugged %s mixins on %s', mixins, cls)
223

224
    fetch_attrs = ('modification_date',)
225

226
    @classmethod
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
    def cw_fetch_order(cls, select, attr, var):
        """This class method may be used to control sort order when multiple
        entities of this type are fetched through ORM methods. Its arguments
        are:

        * `select`, the RQL syntax tree

        * `attr`, the attribute being watched

        * `var`, the variable through which this attribute's value may be
          accessed in the query

        When you want to do some sorting on the given attribute, you should
        modify the syntax tree accordingly. For instance:

        .. sourcecode:: python

          from rql import nodes

          class Version(AnyEntity):
              __regid__ = 'Version'

              fetch_attrs = ('num', 'description', 'in_state')

              @classmethod
              def cw_fetch_order(cls, select, attr, var):
                  if attr == 'num':
                      func = nodes.Function('version_sort_value')
                      func.append(nodes.variable_ref(var))
                      sterm = nodes.SortTerm(func, asc=False)
                      select.add_sort_term(sterm)

        The default implementation call
        :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order`
261
        """
262
        cls.cw_fetch_unrelated_order(select, attr, var)
263
264

    @classmethod
265
266
267
    def cw_fetch_unrelated_order(cls, select, attr, var):
        """This class method may be used to control sort order when multiple entities of
        this type are fetched to use in edition (e.g. propose them to create a
268
        new relation on an edited entity).
269
270
271
272
273
274

        See :meth:`~cubicweb.entity.Entity.cw_fetch_unrelated_order` for a
        description of its arguments and usage.

        By default entities will be listed on their modification date descending,
        i.e. you'll get entities recently modified first.
275
276
        """
        if attr == 'modification_date':
277
            select.add_sort_var(var, asc=False)
278

Adrien Di Mascio's avatar
Adrien Di Mascio committed
279
280
281
    @classmethod
    def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
                  settype=True, ordermethod='fetch_order'):
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
        st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
                             settype=settype, ordermethod=ordermethod)
        rql = st.as_string()
        if restriction:
            # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
            warn('[3.14] fetch_rql: use of `restriction` parameter is '
                 'deprecated, please use fetch_rqlst and supply a syntax'
                 'tree with your restriction instead', DeprecationWarning)
            insert = ' WHERE ' + ','.join(restriction)
            if ' WHERE ' in rql:
                select, where = rql.split(' WHERE ', 1)
                rql = select + insert + ',' + where
            else:
                rql += insert
        return rql

    @classmethod
    def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
                    settype=True, ordermethod='fetch_order'):
        if select is None:
            select = Select()
            mainvar = select.get_variable(mainvar)
            select.add_selected(mainvar)
        elif isinstance(mainvar, basestring):
            assert mainvar in select.defined_vars
            mainvar = select.get_variable(mainvar)
        # eases string -> syntax tree test transition: please remove once stable
        select._varmaker = rqlvar_maker(defined=select.defined_vars,
                                        aliases=select.aliases, index=26)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
311
        if settype:
312
            select.add_type_restriction(mainvar, cls.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
313
314
        if fetchattrs is None:
            fetchattrs = cls.fetch_attrs
315
316
        cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
        return select
317

318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    @classmethod
    def _fetch_ambiguous_rtypes(cls, select, var, fetchattrs, subjtypes, schema):
        """find rtypes in `fetchattrs` that relate different subject etypes
        taken from (`subjtypes`) to different target etypes; these so called
        "ambiguous" relations, are added directly to the `select` syntax tree
        selection but removed from `fetchattrs` to avoid the fetch recursion
        because we have to choose only one targettype for the recursion and
        adding its own fetch attrs to the selection -when we recurse- would
        filter out the other possible target types from the result set
        """
        for attr in fetchattrs.copy():
            rschema = schema.rschema(attr)
            if rschema.final:
                continue
            ttypes = None
            for subjtype in subjtypes:
                cur_ttypes = set(rschema.objects(subjtype))
                if ttypes is None:
                    ttypes = cur_ttypes
                elif cur_ttypes != ttypes:
                    # we found an ambiguous relation: remove it from fetchattrs
                    fetchattrs.remove(attr)
                    # ... and add it to the selection
                    targetvar = select.make_variable()
                    select.add_selected(targetvar)
                    rel = make_relation(var, attr, (targetvar,), VariableRef)
                    select.add_restriction(rel)
                    break

Adrien Di Mascio's avatar
Adrien Di Mascio committed
347
    @classmethod
348
349
    def _fetch_restrictions(cls, mainvar, select, fetchattrs,
                            user, ordermethod='fetch_order', visited=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
350
351
352
353
354
355
356
357
358
359
360
        eschema = cls.e_schema
        if visited is None:
            visited = set((eschema.type,))
        elif eschema.type in visited:
            # avoid infinite recursion
            return
        else:
            visited.add(eschema.type)
        _fetchattrs = []
        for attr in fetchattrs:
            try:
361
                rschema = eschema.subjrels[attr]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
362
363
            except KeyError:
                cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
364
                            attr, cls.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
365
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
366
367
            rdef = eschema.rdef(attr)
            if not user.matching_groups(rdef.get_groups('read')):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
368
                continue
369
370
371
372
373
374
375
376
377
            if rschema.final or rdef.cardinality[0] in '?1':
                var = select.make_variable()
                select.add_selected(var)
                rel = make_relation(mainvar, attr, (var,), VariableRef)
                select.add_restriction(rel)
            else:
                cls.warning('bad relation %s specified in fetch attrs for %s',
                            attr, cls)
                continue
378
            if not rschema.final:
379
380
381
382
                # XXX we need outer join in case the relation is not mandatory
                # (card == '?')  *or if the entity is being added*, since in
                # that case the relation may still be missing. As we miss this
                # later information here, systematically add it.
383
                rel.change_optional('right')
384
                targettypes = rschema.objects(eschema.type)
385
386
                vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
                etypecls = vreg['etypes'].etype_class(targettypes[0])
387
388
                if len(targettypes) > 1:
                    # find fetch_attrs common to all destination types
389
                    fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
Sylvain Thénault's avatar
Sylvain Thénault committed
390
                    # ... and handle ambiguous relations
391
392
                    cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
                                                targettypes, vreg.schema)
393
394
                else:
                    fetchattrs = etypecls.fetch_attrs
395
                etypecls._fetch_restrictions(var, select, fetchattrs,
396
                                             user, ordermethod, visited=visited)
397
            if ordermethod is not None:
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
                try:
                    cmeth = getattr(cls, ordermethod)
                    warn('[3.14] %s %s class method should be renamed to cw_%s'
                         % (cls.__regid__, ordermethod, ordermethod),
                         DeprecationWarning)
                except AttributeError:
                    cmeth = getattr(cls, 'cw_' + ordermethod)
                if support_args(cmeth, 'select'):
                    cmeth(select, attr, var)
                else:
                    warn('[3.14] %s should now take (select, attr, var) and '
                         'modify the syntax tree when desired instead of '
                         'returning something' % cmeth, DeprecationWarning)
                    orderterm = cmeth(attr, var.name)
                    if orderterm is not None:
                        try:
                            var, order = orderterm.split()
                        except ValueError:
                            if '(' in orderterm:
417
418
                                cls.error('ignore %s until %s is upgraded',
                                          orderterm, cmeth)
419
420
421
422
423
424
425
                                orderterm = None
                            elif not ' ' in orderterm.strip():
                                var = orderterm
                                order = 'ASC'
                        if orderterm is not None:
                            select.add_sort_var(select.get_variable(var),
                                                order=='ASC')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
426

427
428
    @classmethod
    @cached
429
430
431
432
433
434
435
436
437
438
439
    def cw_rest_attr_info(cls):
        """this class method return an attribute name to be used in URL for
        entities of this type and a boolean flag telling if its value should be
        checked for uniqness.

        The attribute returned is, in order of priority:

        * class's `rest_attr` class attribute
        * an attribute defined as unique in the class'schema
        * 'eid'
        """
440
441
442
443
444
445
        mainattr, needcheck = 'eid', True
        if cls.rest_attr:
            mainattr = cls.rest_attr
            needcheck = not cls.e_schema.has_unique_values(mainattr)
        else:
            for rschema in cls.e_schema.subject_relations():
446
447
                if rschema.final and rschema != 'eid' \
                        and cls.e_schema.has_unique_values(rschema):
448
449
450
451
452
453
454
                    mainattr = str(rschema)
                    needcheck = False
                    break
        if mainattr == 'eid':
            needcheck = False
        return mainattr, needcheck

455
    @classmethod
456
    def _cw_build_entity_query(cls, kwargs):
457
458
        relations = []
        restrictions = set()
459
        pendingrels = []
460
        eschema = cls.e_schema
461
        qargs = {}
462
        attrcache = {}
463
        for attr, value in kwargs.items():
464
465
466
467
468
            if attr.startswith('reverse_'):
                attr = attr[len('reverse_'):]
                role = 'object'
            else:
                role = 'subject'
469
            assert eschema.has_relation(attr, role), '%s %s not found on %s' % (attr, role, eschema)
470
            rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
471
            if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
472
473
474
                if len(value) == 0:
                    continue # avoid crash with empty IN clause
                elif len(value) == 1:
475
476
                    value = iter(value).next()
                else:
477
                    # prepare IN clause
478
                    pendingrels.append( (attr, role, value) )
479
                    continue
480
481
            if rschema.final: # attribute
                relations.append('X %s %%(%s)s' % (attr, attr))
482
483
                attrcache[attr] = value
            elif value is None:
484
                pendingrels.append( (attr, role, value) )
485
            else:
486
                rvar = attr.upper()
487
488
                if role == 'object':
                    relations.append('%s %s X' % (rvar, attr))
489
490
491
492
493
                else:
                    relations.append('X %s %s' % (attr, rvar))
                restriction = '%s eid %%(%s)s' % (rvar, attr)
                if not restriction in restrictions:
                    restrictions.add(restriction)
494
495
496
                if hasattr(value, 'eid'):
                    value = value.eid
            qargs[attr] = value
497
        rql = u''
498
        if relations:
499
            rql += ', '.join(relations)
500
        if restrictions:
501
            rql += ' WHERE %s' % ', '.join(restrictions)
502
        return rql, qargs, pendingrels, attrcache
503
504

    @classmethod
505
506
    def _cw_handle_pending_relations(cls, eid, pendingrels, execute):
        for attr, role, values in pendingrels:
507
508
            if role == 'object':
                restr = 'Y %s X' % attr
509
510
            else:
                restr = 'X %s Y' % attr
511
512
513
            if values is None:
                execute('DELETE %s WHERE X eid %%(x)s' % restr, {'x': eid})
                continue
514
            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
515
                restr, ','.join(str(getattr(r, 'eid', r)) for r in values)),
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
                    {'x': eid}, build_descr=False)

    @classmethod
    def cw_instantiate(cls, execute, **kwargs):
        """add a new entity of this given type

        Example (in a shell session):

        >>> companycls = vreg['etypes'].etype_class(('Company')
        >>> personcls = vreg['etypes'].etype_class(('Person')
        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
        ...                              works_for=c)

        You can also set relations where the entity has 'object' role by
        prefixing the relation name by 'reverse_'. Also, relation values may be
        an entity or eid, a list of entities or eids.
        """
534
        rql, qargs, pendingrels, attrcache = cls._cw_build_entity_query(kwargs)
535
536
537
538
539
        if rql:
            rql = 'INSERT %s X: %s' % (cls.__regid__, rql)
        else:
            rql = 'INSERT %s X' % (cls.__regid__)
        created = execute(rql, qargs).get_entity(0, 0)
540
541
        created._cw_update_attr_cache(attrcache)
        cls._cw_handle_pending_relations(created.eid, pendingrels, execute)
542
543
        return created

544
    def __init__(self, req, rset=None, row=None, col=0):
545
        AppObject.__init__(self, req, rset=rset, row=row, col=col)
546
        self._cw_related_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
547
548
549
550
        if rset is not None:
            self.eid = rset[row][col]
        else:
            self.eid = None
551
        self._cw_is_saved = True
552
        self.cw_attr_cache = {}
553

Adrien Di Mascio's avatar
Adrien Di Mascio committed
554
555
    def __repr__(self):
        return '<Entity %s %s %s at %s>' % (
556
            self.e_schema, self.eid, list(self.cw_attr_cache), id(self))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
557

558
559
560
    def __cmp__(self, other):
        raise NotImplementedError('comparison not implemented for %s' % self.__class__)

561
    def _cw_update_attr_cache(self, attrcache):
562
563
564
        # if context is a repository session, don't consider dont-cache-attrs as
        # the instance already hold modified values and loosing them could
        # introduce severe problems
565
566
567
568
        get_set = partial(self._cw.get_shared_data, default=(), txdata=True,
                          pop=True)
        uncached_attrs = set()
        uncached_attrs.update(get_set('%s.storage-special-process-attrs' % self.eid))
569
        if self._cw.is_request:
570
571
572
573
            uncached_attrs.update(get_set('%s.dont-cache-attrs' % self.eid))
        for attr in uncached_attrs:
            attrcache.pop(attr, None)
            self.cw_attr_cache.pop(attr, None)
574
575
        self.cw_attr_cache.update(attrcache)

576
577
    def _cw_dont_cache_attribute(self, attr, repo_side=False):
        """Repository side method called when some attribute has been
578
        transformed by a hook, hence original value should not be cached by
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
        the client.

        If repo_side is True, this means that the attribute has been
        transformed by a *storage*, hence the original value should
        not be cached **by anyone**.

        This only applies to a storage special case where the value
        specified in creation or update is **not** the value that will
        be transparently exposed later.

        For example we have a special "fs_importing" mode in BFSS
        where a file path is given as attribute value and stored as is
        in the data base. Later access to the attribute will provide
        the content of the file at the specified path. We do not want
        the "filepath" value to be cached.
594
595
        """
        self._cw.transaction_data.setdefault('%s.dont-cache-attrs' % self.eid, set()).add(attr)
596
597
598
        if repo_side:
            trdata = self._cw.transaction_data
            trdata.setdefault('%s.storage-special-process-attrs' % self.eid, set()).add(attr)
599

600
601
602
603
    def __json_encode__(self):
        """custom json dumps hook to dump the entity's eid
        which is not part of dict structure itself
        """
604
        dumpable = self.cw_attr_cache.copy()
605
606
607
        dumpable['eid'] = self.eid
        return dumpable

608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
    def cw_adapt_to(self, interface):
        """return an adapter the entity to the given interface name.

        return None if it can not be adapted.
        """
        try:
            cache = self._cw_adapters_cache
        except AttributeError:
            self._cw_adapters_cache = cache = {}
        try:
            return cache[interface]
        except KeyError:
            adapter = self._cw.vreg['adapters'].select_or_none(
                interface, self._cw, entity=self)
            cache[interface] = adapter
            return adapter

625
    def has_eid(self): # XXX cw_has_eid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
626
627
628
629
        """return True if the entity has an attributed eid (False
        meaning that the entity has to be created
        """
        try:
Sylvain Thénault's avatar
Sylvain Thénault committed
630
            typed_eid(self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
631
632
633
634
            return True
        except (ValueError, TypeError):
            return False

635
    def cw_is_saved(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
636
        """during entity creation, there is some time during which the entity
637
638
639
        has an eid attributed though it's not saved (eg during
        'before_add_entity' hooks). You can use this method to ensure the entity
        has an eid *and* is saved in its source.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
640
        """
641
        return self.has_eid() and self._cw_is_saved
642

Adrien Di Mascio's avatar
Adrien Di Mascio committed
643
    @cached
644
    def cw_metainformation(self):
645
646
647
648
        res = self._cw.describe(self.eid, asdict=True)
        # use 'asource' and not 'source' since this is the actual source,
        # while 'source' is the physical source (where it's stored)
        res['source'] = self._cw.source_defs()[res.pop('asource')]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
649
650
        return res

651
    def cw_check_perm(self, action):
652
        self.e_schema.check_perm(self._cw, action, eid=self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
653

654
    def cw_has_perm(self, action):
655
        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
656

657
    def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
Adrien Di Mascio's avatar
Adrien Di Mascio committed
658
        """shortcut to apply a view on this entity"""
659
660
661
662
        if initargs is None:
            initargs = kwargs
        else:
            initargs.update(kwargs)
663
        view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
664
                                                row=self.cw_row, col=self.cw_col,
665
                                                **initargs)
666
        return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
667

668
    def absolute_url(self, *args, **kwargs): # XXX cw_url
Adrien Di Mascio's avatar
Adrien Di Mascio committed
669
        """return an absolute url to view this entity"""
670
671
672
673
674
675
676
        # use *args since we don't want first argument to be "anonymous" to
        # avoid potential clash with kwargs
        if args:
            assert len(args) == 1, 'only 0 or 1 non-named-argument expected'
            method = args[0]
        else:
            method = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
677
678
679
        # in linksearch mode, we don't want external urls else selecting
        # the object for use in the relation is tricky
        # XXX search_state is web specific
680
        use_ext_id = False
681
        if 'base_url' not in kwargs and \
682
               getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
683
684
685
686
687
            sourcemeta = self.cw_metainformation()['source']
            if sourcemeta.get('use-cwuri-as-url'):
                return self.cwuri # XXX consider kwargs?
            if sourcemeta.get('base-url'):
                kwargs['base_url'] = sourcemeta['base-url']
688
                use_ext_id = True
689
        if method in (None, 'view'):
690
            kwargs['_restpath'] = self.rest_path(use_ext_id)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
691
692
        else:
            kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
693
        return self._cw.build_url(method, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
694

695
    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
Adrien Di Mascio's avatar
Adrien Di Mascio committed
696
        """returns a REST-like (relative) path for this entity"""
697
        mainattr, needcheck = self.cw_rest_attr_info()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
698
        etype = str(self.e_schema)
699
        path = etype.lower()
700
        fallback = False
701
        if mainattr != 'eid':
702
            value = getattr(self, mainattr)
703
            if not can_use_rest_path(value):
704
                mainattr = 'eid'
705
                path = None
706
707
            elif needcheck:
                # make sure url is not ambiguous
708
709
710
711
712
713
                try:
                    nbresults = self.__unique
                except AttributeError:
                    rql = 'Any COUNT(X) WHERE X is %s, X %s %%(value)s' % (
                        etype, mainattr)
                    nbresults = self.__unique = self._cw.execute(rql, {'value' : value})[0][0]
714
715
                if nbresults != 1: # ambiguity?
                    mainattr = 'eid'
716
                    path = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
717
        if mainattr == 'eid':
718
            if use_ext_eid:
719
                value = self.cw_metainformation()['extid']
720
721
            else:
                value = self.eid
722
723
724
725
        if path is None:
            # fallback url: <base-url>/<eid> url is used as cw entities uri,
            # prefer it to <base-url>/<etype>/eid/<eid>
            return unicode(value)
726
        return '%s/%s' % (path, self._cw.url_quote(value))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
727

728
    def cw_attr_metadata(self, attr, metadata):
729
        """return a metadata for an attribute (None if unspecified)"""
730
        value = getattr(self, '%s_%s' % (attr, metadata), None)
731
        if value is None and metadata == 'encoding':
732
            value = self._cw.vreg.property_value('ui.encoding')
733
        return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
734
735

    def printable_value(self, attr, value=_marker, attrtype=None,
736
                        format='text/html', displaytime=True): # XXX cw_printable_value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
737
738
739
740
741
        """return a displayable value (i.e. unicode string) which may contains
        html tags
        """
        attr = str(attr)
        if value is _marker:
742
            value = getattr(self, attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
743
744
745
746
747
748
        if isinstance(value, basestring):
            value = value.strip()
        if value is None or value == '': # don't use "not", 0 is an acceptable value
            return u''
        if attrtype is None:
            attrtype = self.e_schema.destination(attr)
Sylvain Thénault's avatar
Sylvain Thénault committed
749
        props = self.e_schema.rdef(attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
750
751
752
        if attrtype == 'String':
            # internalinalized *and* formatted string such as schema
            # description...
Sylvain Thénault's avatar
Sylvain Thénault committed
753
            if props.internationalizable:
754
                value = self._cw._(value)
755
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
756
            if attrformat:
757
758
                return self._cw_mtc_transform(value, attrformat, format,
                                              self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
759
        elif attrtype == 'Bytes':
760
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
761
            if attrformat:
762
763
764
                encoding = self.cw_attr_metadata(attr, 'encoding')
                return self._cw_mtc_transform(value.getvalue(), attrformat, format,
                                              encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
765
            return u''
766
767
        value = self._cw.printable_value(attrtype, value, props,
                                         displaytime=displaytime)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
768
        if format == 'text/html':
Sylvain Thénault's avatar
Sylvain Thénault committed
769
            value = xml_escape(value)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
770
771
        return value

772
773
    def _cw_mtc_transform(self, data, format, target_format, encoding,
                          _engine=ENGINE):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
774
775
776
        trdata = TransformData(data, format, encoding, appobject=self)
        data = _engine.convert(trdata, target_format).decode()
        if format == 'text/html':
777
            data = soup2xhtml(data, self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
778
        return data
779

Adrien Di Mascio's avatar
Adrien Di Mascio committed
780
781
    # entity cloning ##########################################################

782
    def copy_relations(self, ceid): # XXX cw_copy_relations
Alexandre Fayolle's avatar
Alexandre Fayolle committed
783
784
785
        """copy relations of the object with the given eid on this
        object (this method is called on the newly created copy, and
        ceid designates the original entity).
Adrien Di Mascio's avatar
Adrien Di Mascio committed
786
787
788
789
790

        By default meta and composite relations are skipped.
        Overrides this if you want another behaviour
        """
        assert self.has_eid()
791
        execute = self._cw.execute
792
793
794
795
796
797
798
799
800
        skip_copy_for = {'subject': set(), 'object': set()}
        for rtype in self.skip_copy_for:
            skip_copy_for['subject'].add(rtype)
            warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
                 'use cw_skip_for instead with list of couples (rtype, role)' % self.__regid__,
                 DeprecationWarning)
        for rtype, role in self.cw_skip_copy_for:
            assert role in ('subject', 'object'), role
            skip_copy_for[role].add(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
801
        for rschema in self.e_schema.subject_relations():
802
            if rschema.final or rschema.meta:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
803
                continue
804
805
            # skip already defined relations
            if getattr(self, rschema.type):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
806
                continue
807
            if rschema.type in skip_copy_for['subject']:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
808
                continue
809
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
810
811
            rdef = self.e_schema.rdef(rschema)
            if rdef.composite:
812
813
814
                continue
            # skip relation with card in ?1 else we either change the copied
            # object (inlined relation) or inserting some inconsistency
Sylvain Thénault's avatar
Sylvain Thénault committed
815
            if rdef.cardinality[1] in '?1':
816
                continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
817
            rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
818
                rschema.type, rschema.type)
819
            execute(rql, {'x': self.eid, 'y': ceid})
820
            self.cw_clear_relation_cache(rschema.type, 'subject')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
821
        for rschema in self.e_schema.object_relations():
822
823
824
            if rschema.meta:
                continue
            # skip already defined relations
Sylvain Thénault's avatar
Sylvain Thénault committed
825
            if self.related(rschema.type, 'object'):
826
                continue
827
828
            if rschema.type in skip_copy_for['object']:
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
829
            rdef = self.e_schema.rdef(rschema, 'object')
830
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
831
            if rdef.composite:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
832
833
834
                continue
            # skip relation with card in ?1 else we either change the copied
            # object (inlined relation) or inserting some inconsistency
Sylvain Thénault's avatar
Sylvain Thénault committed
835
            if rdef.cardinality[0] in '?1':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
836
837
                continue
            rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
838
                rschema.type, rschema.type)
839
            execute(rql, {'x': self.eid, 'y': ceid})
840
            self.cw_clear_relation_cache(rschema.type, 'object')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
841
842
843
844

    # data fetching methods ###################################################

    @cached
845
    def as_rset(self): # XXX .cw_as_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
846
847
        """returns a resultset containing `self` information"""
        rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
848
                         {'x': self.eid}, [(self.__regid__,)])
849
850
        rset.req = self._cw
        return rset
851

852
    def _cw_to_complete_relations(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
853
854
        """by default complete final relations to when calling .complete()"""
        for rschema in self.e_schema.subject_relations():
855
            if rschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
856
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
857
            targets = rschema.objects(self.e_schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
858
            if rschema.inlined:
859
                matching_groups = self._cw.user.matching_groups
860
861
862
                if all(matching_groups(e.get_groups('read')) and
                       rschema.rdef(self.e_schema, e).get_groups('read')
                       for e in targets):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
863
                    yield rschema, 'subject'
864

865
    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
866
867
868
869
870
871
872
873
        for rschema, attrschema in self.e_schema.attribute_definitions():
            # skip binary data by default
            if skip_bytes and attrschema.type == 'Bytes':
                continue
            attr = rschema.type
            if attr == 'eid':
                continue
            # password retreival is blocked at the repository server level
Sylvain Thénault's avatar
Sylvain Thénault committed
874
            rdef = rschema.rdef(self.e_schema, attrschema)
875
            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
876
                   or (attrschema.type == 'Password' and skip_pwd):
877
                self.cw_attr_cache[attr] = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
878
879
                continue
            yield attr
880

881
    _cw_completed = False
882
    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
Adrien Di Mascio's avatar
Adrien Di Mascio committed
883
884
885
886
887
888
889
890
        """complete this entity by adding missing attributes (i.e. query the
        repository to fill the entity)

        :type skip_bytes: bool
        :param skip_bytes:
          if true, attribute of type Bytes won't be considered
        """
        assert self.has_eid()
891
892
893
894
        if self._cw_completed:
            return
        if attributes is None:
            self._cw_completed = True
Adrien Di Mascio's avatar
Adrien Di Mascio committed
895
896
897
898
        varmaker = rqlvar_maker()
        V = varmaker.next()
        rql = ['WHERE %s eid %%(x)s' % V]
        selected = []
899
        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
900
            # if attribute already in entity, nothing to do
901
            if self.cw_attr_cache.has_key(attr):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
902
903
904
905
906
                continue
            # case where attribute must be completed, but is not yet in entity
            var = varmaker.next()
            rql.append('%s %s %s' % (V, attr, var))
            selected.append((attr, var))
907
        # +1 since this doesn't include the main variable
Adrien Di Mascio's avatar
Adrien Di Mascio committed
908
        lastattr = len(selected) + 1
909
910
911
        # don't fetch extra relation if attributes specified or of the entity is
        # coming from an external source (may lead to error)
        if attributes is None and self.cw_metainformation()['source']['uri'] == 'system':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
912
            # fetch additional relations (restricted to 0..1 relations)
913
            for rschema, role in self._cw_to_complete_relations():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
914
                rtype = rschema.type
915
                if self.cw_relation_cached(rtype, role):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
916
                    continue
917
918
919
920
921
922
                # at this point we suppose that:
                # * this is a inlined relation
                # * entity (self) is the subject
                # * user has read perm on the relation and on the target entity
                assert rschema.inlined
                assert role == 'subject'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
923
                var = varmaker.next()
924
925
926
                # keep outer join anyway, we don't want .complete to crash on
                # missing mandatory relation (see #1058267)
                rql.append('%s %s %s?' % (V, rtype, var))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
927
928
929
930
931
932
                selected.append(((rtype, role), var))
        if selected:
            # select V, we need it as the left most selected variable
            # if some outer join are included to fetch inlined relations
            rql = 'Any %s,%s %s' % (V, ','.join(var for attr, var in selected),
                                    ','.join(rql))
933
934
935
936
937
            try:
                rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
            except IndexError:
                raise Exception('unable to fetch attributes for entity with eid %s'
                                % self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
938
939
            # handle attributes
            for i in xrange(1, lastattr):
940
                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
941
942
            # handle relations
            for i in xrange(lastattr, len(rset)):
943
                rtype, role = selected[i-1][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
944
945
946
                value = rset[i]
                if value is None:
                    rrset = ResultSet([], rql, {'x': self.eid})
947
                    rrset.req = self._cw
Adrien Di Mascio's avatar
Adrien Di Mascio committed
948
                else:
949
                    rrset = self._cw.eid_rset(value)
950
                self.cw_set_relation_cache(rtype, role, rrset)
951

952
    def cw_attr_value(self, name):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
953
954
955
956
957
958
959
        """get value for the attribute relation <name>, query the repository
        to get the value if necessary.

        :type name: str
        :param name: name of the attribute to get
        """
        try:
960
            return self.cw_attr_cache[name]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
961
        except KeyError:
962
            if not self.cw_is_saved():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
963
964
965
                return None
            rql = "Any A WHERE X eid %%(x)s, X %s A" % name
            try:
966
                rset = self._cw.execute(rql, {'x': self.eid})
Adrien Di Mascio's avatar
Adrien Di Mascio committed
967
            except Unauthorized:
968
                self.cw_attr_cache[name] = value = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
969
970
971
            else:
                assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                try:
972
                    self.cw_attr_cache[name] = value = rset.rows[0][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
973
974
975
976
977
                except IndexError:
                    # probably a multisource error
                    self.critical("can't get value for attribute %s of entity with eid %s",
                                  name, self.eid)
                    if self.e_schema.destination(name) == 'String':
978
                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
979
                    else:
980
981
                        self.cw_attr_cache[name] = value = None
            return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
982

983
984
    def related(self, rtype, role='subject', limit=None, entities=False, # XXX .cw_related
                safe=False):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
985
        """returns a resultset of related entities
986

987
988
989
990
991
992
993
994
995
996
997
        :param rtype:
          the name of the relation, aka relation type
        :param role:
          the role played by 'self' in the relation ('subject' or 'object')
        :param limit:
          resultset's maximum size
        :param entities:
          if True, the entites are returned; if False, a result set is returned
        :param safe:
          if True, an empty rset/list of entities will be returned in case of
          :exc:`Unauthorized`, else (the default), the exception is propagated
Adrien Di Mascio's avatar
Adrien Di Mascio committed
998
        """
999
        rtype = str(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1000
        try:
1001
            return self._cw_relation_cache(rtype, role, entities, limit)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1002
1003
        except KeyError:
            pass
1004
1005
1006
        if not self.has_eid():
            if entities:
                return []
1007
            return self._cw.empty_rset()
1008
        rql = self.cw_related_rql(rtype, role)
1009
1010
1011
1012
1013
1014
        try:
            rset = self._cw.execute(rql, {'x': self.eid})
        except Unauthorized:
            if not safe:
                raise
            rset = self._cw.empty_rset()
1015
        self.cw_set_relation_cache(rtype, role, rset)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1016
1017
        return self.related(rtype, role, limit, entities)

1018
    def cw_related_rql(self, rtype, role='subject', targettypes=None):
1019
1020
        vreg = self._cw.vreg
        rschema = vreg.schema[rtype]
1021
1022
1023
1024
        select = Select()
        mainvar, evar = select.get_variable('X'), select.get_variable('E')
        select.add_selected(mainvar)
        select.add_eid_restriction(evar, 'x', 'Substitute')