entity.py 61.2 KB
Newer Older
1
# copyright 2003-2011 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
23
from warnings import warn

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
29
from rql.utils import rqlvar_maker
30
31
32
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
33

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

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

_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
53
            card = rschema.rdef(subjtype, objtype).cardinality[index]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
54
55
56
57
            if card in '+*':
                return card
    return '1'

58
59
60
61
62
63
64
65
66
67
68
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

69
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
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
148

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

153
154
155
156
    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
157
158
159
160

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

161
162
163
164
    :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)
165

166
167
    :type cw_skip_copy_for: list
    :cvar cw_skip_copy_for: a list of couples (rtype, role) for each relation
168
169
170
       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
171
172
    """
    __registry__ = 'etypes'
173
    __select__ = yes()
174

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
183
    @classmethod
184
    def __initialize__(cls, schema):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
185
186
187
        """initialize a specific entity class by adding descriptors to access
        entity type's attributes and relations
        """
188
        etype = cls.__regid__
Adrien Di Mascio's avatar
Adrien Di Mascio committed
189
        assert etype != 'Any', etype
190
        cls.e_schema = eschema = schema.eschema(etype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
191
        for rschema, _ in eschema.attribute_definitions():
192
            if rschema.type == 'eid':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
193
                continue
194
            setattr(cls, rschema.type, Attribute(rschema.type))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
195
        mixins = []
196
197
198
        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
199
200
201
202
203
                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)
204
            if role == 'subject':
205
                attr = rschema.type
Adrien Di Mascio's avatar
Adrien Di Mascio committed
206
            else:
207
                attr = 'reverse_%s' % rschema.type
208
            setattr(cls, attr, Relation(rschema, role))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
209
        if mixins:
210
211
212
213
214
215
216
217
218
219
220
221
            # 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)
222

223
    fetch_attrs = ('modification_date',)
224

225
    @classmethod
226
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
    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`
260
        """
261
        cls.cw_fetch_unrelated_order(select, attr, var)
262
263

    @classmethod
264
265
266
    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
267
        new relation on an edited entity).
268
269
270
271
272
273

        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.
274
275
        """
        if attr == 'modification_date':
276
            select.add_sort_var(var, asc=False)
277

Adrien Di Mascio's avatar
Adrien Di Mascio committed
278
279
280
    @classmethod
    def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
                  settype=True, ordermethod='fetch_order'):
281
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
        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
310
        if settype:
311
            select.add_type_restriction(mainvar, cls.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
312
313
        if fetchattrs is None:
            fetchattrs = cls.fetch_attrs
314
315
        cls._fetch_restrictions(mainvar, select, fetchattrs, user, ordermethod)
        return select
316

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
    @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
346
    @classmethod
347
348
    def _fetch_restrictions(cls, mainvar, select, fetchattrs,
                            user, ordermethod='fetch_order', visited=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
349
350
351
352
353
354
355
356
357
358
359
        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:
360
                rschema = eschema.subjrels[attr]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
361
362
            except KeyError:
                cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
363
                            attr, cls.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
364
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
365
366
            rdef = eschema.rdef(attr)
            if not user.matching_groups(rdef.get_groups('read')):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
367
                continue
368
369
370
371
372
373
374
375
376
            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
377
            if not rschema.final:
378
379
380
381
                # 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.
382
                rel.change_optional('right')
383
                targettypes = rschema.objects(eschema.type)
384
385
                vreg = user._cw.vreg # XXX user._cw.vreg iiiirk
                etypecls = vreg['etypes'].etype_class(targettypes[0])
386
387
                if len(targettypes) > 1:
                    # find fetch_attrs common to all destination types
388
                    fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
Sylvain Thénault's avatar
Sylvain Thénault committed
389
                    # ... and handle ambiguous relations
390
391
                    cls._fetch_ambiguous_rtypes(select, var, fetchattrs,
                                                targettypes, vreg.schema)
392
393
                else:
                    fetchattrs = etypecls.fetch_attrs
394
                etypecls._fetch_restrictions(var, select, fetchattrs,
395
                                             user, ordermethod, visited=visited)
396
            if ordermethod is not None:
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
                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:
416
417
                                cls.error('ignore %s until %s is upgraded',
                                          orderterm, cmeth)
418
419
420
421
422
423
424
                                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
425

426
427
    @classmethod
    @cached
428
429
430
431
432
433
434
435
436
437
438
    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'
        """
439
440
441
442
443
444
        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():
445
446
                if rschema.final and rschema != 'eid' \
                        and cls.e_schema.has_unique_values(rschema):
447
448
449
450
451
452
453
                    mainattr = str(rschema)
                    needcheck = False
                    break
        if mainattr == 'eid':
            needcheck = False
        return mainattr, needcheck

454
455
456
457
458
459
460
461
    @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')
462
463
464
        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
        ...                              works_for=c)
465

466
467
        You can also set relation where the entity has 'object' role by
        prefixing the relation by 'reverse_'.
468
469
470
471
472
        """
        rql = 'INSERT %s X' % cls.__regid__
        relations = []
        restrictions = set()
        pending_relations = []
473
        eschema = cls.e_schema
474
        qargs = {}
475
        for attr, value in kwargs.items():
476
477
478
479
480
            if attr.startswith('reverse_'):
                attr = attr[len('reverse_'):]
                role = 'object'
            else:
                role = 'subject'
481
            assert eschema.has_relation(attr, role), '%s %s not found on %s' % (attr, role, eschema)
482
            rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
483
            if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
484
485
486
                if len(value) == 1:
                    value = iter(value).next()
                else:
487
                    # prepare IN clause
488
                    pending_relations.append( (attr, role, value) )
489
                    continue
490
491
492
            if rschema.final: # attribute
                relations.append('X %s %%(%s)s' % (attr, attr))
            else:
493
                rvar = attr.upper()
494
495
                if role == 'object':
                    relations.append('%s %s X' % (rvar, attr))
496
497
498
499
500
                else:
                    relations.append('X %s %s' % (attr, rvar))
                restriction = '%s eid %%(%s)s' % (rvar, attr)
                if not restriction in restrictions:
                    restrictions.add(restriction)
501
502
503
                if hasattr(value, 'eid'):
                    value = value.eid
            qargs[attr] = value
504
505
506
507
        if relations:
            rql = '%s: %s' % (rql, ', '.join(relations))
        if restrictions:
            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
508
509
510
511
        created = execute(rql, qargs).get_entity(0, 0)
        for attr, role, values in pending_relations:
            if role == 'object':
                restr = 'Y %s X' % attr
512
513
514
            else:
                restr = 'X %s Y' % attr
            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
                    {'x': created.eid}, build_descr=False)
        return created

519
    def __init__(self, req, rset=None, row=None, col=0):
520
        AppObject.__init__(self, req, rset=rset, row=row, col=col)
521
        self._cw_related_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
522
523
524
525
        if rset is not None:
            self.eid = rset[row][col]
        else:
            self.eid = None
526
        self._cw_is_saved = True
527
        self.cw_attr_cache = {}
528

Adrien Di Mascio's avatar
Adrien Di Mascio committed
529
530
    def __repr__(self):
        return '<Entity %s %s %s at %s>' % (
531
            self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
532

533
534
535
    def __cmp__(self, other):
        raise NotImplementedError('comparison not implemented for %s' % self.__class__)

536
537
538
539
    def __json_encode__(self):
        """custom json dumps hook to dump the entity's eid
        which is not part of dict structure itself
        """
540
        dumpable = self.cw_attr_cache.copy()
541
542
543
        dumpable['eid'] = self.eid
        return dumpable

544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
    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

561
    def has_eid(self): # XXX cw_has_eid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
562
563
564
565
        """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
566
            typed_eid(self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
567
568
569
570
            return True
        except (ValueError, TypeError):
            return False

571
    def cw_is_saved(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
572
        """during entity creation, there is some time during which the entity
573
574
575
        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
576
        """
577
        return self.has_eid() and self._cw_is_saved
578

Adrien Di Mascio's avatar
Adrien Di Mascio committed
579
    @cached
580
    def cw_metainformation(self):
581
582
583
584
        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
585
586
        return res

587
    def cw_check_perm(self, action):
588
        self.e_schema.check_perm(self._cw, action, eid=self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
589

590
    def cw_has_perm(self, action):
591
        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
592

593
    def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
Adrien Di Mascio's avatar
Adrien Di Mascio committed
594
        """shortcut to apply a view on this entity"""
595
596
597
598
        if initargs is None:
            initargs = kwargs
        else:
            initargs.update(kwargs)
599
        view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
600
                                                row=self.cw_row, col=self.cw_col,
601
                                                **initargs)
602
        return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
603

604
    def absolute_url(self, *args, **kwargs): # XXX cw_url
Adrien Di Mascio's avatar
Adrien Di Mascio committed
605
        """return an absolute url to view this entity"""
606
607
608
609
610
611
612
        # 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
613
614
615
        # 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
616
        use_ext_id = False
617
        if 'base_url' not in kwargs and \
618
               getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
619
620
621
622
623
            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']
624
                use_ext_id = True
625
        if method in (None, 'view'):
626
            kwargs['_restpath'] = self.rest_path(use_ext_id)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
627
628
        else:
            kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
629
        return self._cw.build_url(method, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
630

631
    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
Adrien Di Mascio's avatar
Adrien Di Mascio committed
632
        """returns a REST-like (relative) path for this entity"""
633
        mainattr, needcheck = self.cw_rest_attr_info()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
634
        etype = str(self.e_schema)
635
636
        path = etype.lower()
        if mainattr != 'eid':
637
            value = getattr(self, mainattr)
638
            if not can_use_rest_path(value):
639
640
641
642
                mainattr = 'eid'
                path += '/eid'
            elif needcheck:
                # make sure url is not ambiguous
643
644
645
646
647
648
                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]
649
650
651
                if nbresults != 1: # ambiguity?
                    mainattr = 'eid'
                    path += '/eid'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
652
        if mainattr == 'eid':
653
            if use_ext_eid:
654
                value = self.cw_metainformation()['extid']
655
656
            else:
                value = self.eid
657
        return '%s/%s' % (path, self._cw.url_quote(value))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
658

659
    def cw_attr_metadata(self, attr, metadata):
660
        """return a metadata for an attribute (None if unspecified)"""
661
        value = getattr(self, '%s_%s' % (attr, metadata), None)
662
        if value is None and metadata == 'encoding':
663
            value = self._cw.vreg.property_value('ui.encoding')
664
        return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
665
666

    def printable_value(self, attr, value=_marker, attrtype=None,
667
                        format='text/html', displaytime=True): # XXX cw_printable_value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
668
669
670
671
672
        """return a displayable value (i.e. unicode string) which may contains
        html tags
        """
        attr = str(attr)
        if value is _marker:
673
            value = getattr(self, attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
674
675
676
677
678
679
        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
680
        props = self.e_schema.rdef(attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
681
682
683
        if attrtype == 'String':
            # internalinalized *and* formatted string such as schema
            # description...
Sylvain Thénault's avatar
Sylvain Thénault committed
684
            if props.internationalizable:
685
                value = self._cw._(value)
686
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
687
            if attrformat:
688
689
                return self._cw_mtc_transform(value, attrformat, format,
                                              self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
690
        elif attrtype == 'Bytes':
691
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
692
            if attrformat:
693
694
695
                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
696
            return u''
697
698
        value = self._cw.printable_value(attrtype, value, props,
                                         displaytime=displaytime)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
699
        if format == 'text/html':
Sylvain Thénault's avatar
Sylvain Thénault committed
700
            value = xml_escape(value)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
701
702
        return value

703
704
    def _cw_mtc_transform(self, data, format, target_format, encoding,
                          _engine=ENGINE):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
705
706
707
        trdata = TransformData(data, format, encoding, appobject=self)
        data = _engine.convert(trdata, target_format).decode()
        if format == 'text/html':
708
            data = soup2xhtml(data, self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
709
        return data
710

Adrien Di Mascio's avatar
Adrien Di Mascio committed
711
712
    # entity cloning ##########################################################

713
    def copy_relations(self, ceid): # XXX cw_copy_relations
Alexandre Fayolle's avatar
Alexandre Fayolle committed
714
715
716
        """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
717
718
719
720
721

        By default meta and composite relations are skipped.
        Overrides this if you want another behaviour
        """
        assert self.has_eid()
722
        execute = self._cw.execute
723
724
725
726
727
728
729
730
731
        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
732
        for rschema in self.e_schema.subject_relations():
733
            if rschema.final or rschema.meta:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
734
                continue
735
736
            # skip already defined relations
            if getattr(self, rschema.type):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
737
                continue
738
            if rschema.type in skip_copy_for['subject']:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
739
                continue
740
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
741
742
            rdef = self.e_schema.rdef(rschema)
            if rdef.composite:
743
744
745
                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
746
            if rdef.cardinality[1] in '?1':
747
                continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
748
            rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
749
                rschema.type, rschema.type)
750
            execute(rql, {'x': self.eid, 'y': ceid})
751
            self.cw_clear_relation_cache(rschema.type, 'subject')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
752
        for rschema in self.e_schema.object_relations():
753
754
755
            if rschema.meta:
                continue
            # skip already defined relations
Sylvain Thénault's avatar
Sylvain Thénault committed
756
            if self.related(rschema.type, 'object'):
757
                continue
758
759
            if rschema.type in skip_copy_for['object']:
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
760
            rdef = self.e_schema.rdef(rschema, 'object')
761
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
762
            if rdef.composite:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
763
764
765
                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
766
            if rdef.cardinality[0] in '?1':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
767
768
                continue
            rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
769
                rschema.type, rschema.type)
770
            execute(rql, {'x': self.eid, 'y': ceid})
771
            self.cw_clear_relation_cache(rschema.type, 'object')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
772
773
774
775

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

    @cached
776
    def as_rset(self): # XXX .cw_as_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
777
778
        """returns a resultset containing `self` information"""
        rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
779
                         {'x': self.eid}, [(self.__regid__,)])
780
781
        rset.req = self._cw
        return rset
782

783
    def _cw_to_complete_relations(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
784
785
        """by default complete final relations to when calling .complete()"""
        for rschema in self.e_schema.subject_relations():
786
            if rschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
787
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
788
            targets = rschema.objects(self.e_schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
789
            if rschema.inlined:
790
                matching_groups = self._cw.user.matching_groups
791
792
793
                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
794
                    yield rschema, 'subject'
795

796
    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
797
798
799
800
801
802
803
804
        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
805
            rdef = rschema.rdef(self.e_schema, attrschema)
806
            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
807
                   or (attrschema.type == 'Password' and skip_pwd):
808
                self.cw_attr_cache[attr] = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
809
810
                continue
            yield attr
811

812
    _cw_completed = False
813
    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
Adrien Di Mascio's avatar
Adrien Di Mascio committed
814
815
816
817
818
819
820
821
        """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()
822
823
824
825
        if self._cw_completed:
            return
        if attributes is None:
            self._cw_completed = True
Adrien Di Mascio's avatar
Adrien Di Mascio committed
826
827
828
829
        varmaker = rqlvar_maker()
        V = varmaker.next()
        rql = ['WHERE %s eid %%(x)s' % V]
        selected = []
830
        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
831
            # if attribute already in entity, nothing to do
832
            if self.cw_attr_cache.has_key(attr):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
833
834
835
836
837
                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))
838
        # +1 since this doesn't include the main variable
Adrien Di Mascio's avatar
Adrien Di Mascio committed
839
        lastattr = len(selected) + 1
840
841
842
        # 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
843
            # fetch additional relations (restricted to 0..1 relations)
844
            for rschema, role in self._cw_to_complete_relations():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
845
                rtype = rschema.type
846
                if self.cw_relation_cached(rtype, role):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
847
                    continue
848
849
850
851
852
853
                # 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
854
                var = varmaker.next()
855
856
857
                # 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
858
859
860
861
862
863
                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))
864
865
866
867
868
            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
869
870
            # handle attributes
            for i in xrange(1, lastattr):
871
                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
872
873
            # handle relations
            for i in xrange(lastattr, len(rset)):
874
                rtype, role = selected[i-1][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
875
876
877
                value = rset[i]
                if value is None:
                    rrset = ResultSet([], rql, {'x': self.eid})
878
                    rrset.req = self._cw
Adrien Di Mascio's avatar
Adrien Di Mascio committed
879
                else:
880
                    rrset = self._cw.eid_rset(value)
881
                self.cw_set_relation_cache(rtype, role, rrset)
882

883
    def cw_attr_value(self, name):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
884
885
886
887
888
889
890
        """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:
891
            return self.cw_attr_cache[name]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
892
        except KeyError:
893
            if not self.cw_is_saved():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
894
895
896
                return None
            rql = "Any A WHERE X eid %%(x)s, X %s A" % name
            try:
897
                rset = self._cw.execute(rql, {'x': self.eid})
Adrien Di Mascio's avatar
Adrien Di Mascio committed
898
            except Unauthorized:
899
                self.cw_attr_cache[name] = value = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
900
901
902
            else:
                assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                try:
903
                    self.cw_attr_cache[name] = value = rset.rows[0][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
904
905
906
907
908
                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':
909
                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
910
                    else:
911
912
                        self.cw_attr_cache[name] = value = None
            return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
913

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

918
919
920
921
922
923
924
925
926
927
928
        :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
929
        """
930
        rtype = str(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
931
        try:
932
            return self._cw_relation_cache(rtype, role, entities, limit)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
933
934
        except KeyError:
            pass
935
936
937
        if not self.has_eid():
            if entities:
                return []
938
            return self._cw.empty_rset()
939
        rql = self.cw_related_rql(rtype, role)
940
941
942
943
944
945
        try:
            rset = self._cw.execute(rql, {'x': self.eid})
        except Unauthorized:
            if not safe:
                raise
            rset = self._cw.empty_rset()
946
        self.cw_set_relation_cache(rtype, role, rset)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
947
948
        return self.related(rtype, role, limit, entities)

949
    def cw_related_rql(self, rtype, role='subject', targettypes=None):
950
951
        vreg = self._cw.vreg
        rschema = vreg.schema[rtype]
952
953
954
955
        select = Select()
        mainvar, evar = select.get_variable('X'), select.get_variable('E')
        select.add_selected(mainvar)
        select.add_eid_restriction(evar, 'x', 'Substitute')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
956
        if role == 'subject':
957
958
            rel = make_relation(evar, rtype, (mainvar,), VariableRef)
            select.add_restriction(rel)
959
960
            if targettypes is None:
                targettypes = rschema.objects(self.e_schema)
961
            else:
962
963
964
                select.add_constant_restriction(mainvar, 'is',
                                                targettypes, 'etype')
            gcard = greater_card(rschema, (self.e_schema,), targettypes, 0)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
965
        else:
966
967
            rel = make_relation(mainvar, rtype, (evar,), VariableRef)
            select.add_restriction(rel)
968
969
            if targettypes is None:
                targettypes = rschema.subjects(self.e_schema)
970
            else:
971
                select.add_constant_restriction(mainvar, 'is', targettypes,
972
                                                'etype')
973
            gcard = greater_card(rschema, targettypes, (self.e_schema,), 1)
974
        etypecls = vreg['etypes'].etype_class(targettypes[0])
Adrien Di Mascio's avatar
Adrien Di Mascio committed
975
        if len(targettypes) > 1:
976
977
978
            fetchattrs = vreg['etypes'].fetch_attrs(targettypes)
            self._fetch_ambiguous_rtypes(select, mainvar, fetchattrs,
                                         targettypes, vreg.schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
979
        else:
980
            fetchattrs = etypecls.fetch_attrs
981
982
        etypecls.fetch_rqlst(self._cw.user, select, mainvar, fetchattrs,
                             settype=False)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
983
984
        # optimisation: remove ORDERBY if cardinality is 1 or ? (though
        # greater_card return 1 for those both cases)
985
986
987
988
989
990
991
992
993
994
995
996
997
        if gcard == '1':
            select.remove_sort_terms()
        elif not select.orderby:
            # if modification_date is already retrieved, we use it instead
            # of adding another variable for sorting. This should not be
            # problematic, but it is with sqlserver, see ticket #694445
            for rel in select.where.get_nodes(RqlRelation):
                if (rel.r_type == 'modification_date'
                    and rel.children[0].variable == mainvar
                    and rel.children[1].operator == '='):
                    var = rel.children[1].children[0].variable
                    select.add_sort_var(var, asc=False)
                    break
998
            else:
999
1000
1001
1002
1003
1004
                mdvar = select.make_variable()
                rel = make_relation(mainvar, 'modification_date',
                                    (mdvar,), VariableRef)
                select.add_restriction(rel)
                select.add_sort_var(mdvar, asc=False)
        return select.as_string()
1005

Adrien Di Mascio's avatar
Adrien Di Mascio committed
1006
1007
    # generic vocabulary methods ##############################################

1008
    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
1009
                         vocabconstraints=True, lt_infos={}):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1010
        """build a rql to fetch `targettype` entities unrelated to this entity
1011
1012
1013
1014
        using (rtype, role) relation.

        Consider relation permissions so that returned entities may be actually
        linked by `rtype`.
1015
1016
1017
1018
1019
1020

        `lt_infos` are supplementary informations, usually coming from __linkto
        parameter, that can help further restricting the results in case current
        entity is not yet created. It is a dict describing entities the current
        entity will be linked to, which keys are (rtype, role) tuples and values
        are a list of eids.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1021
1022
        """
        ordermethod = ordermethod or 'fetch_unrelated_order'
1023
1024
        rschema = self._cw.vreg.schema.rschema(rtype)
        rdef = rschema.role_rdef(self.e_schema, targettype, role)
1025
        rewriter = RQLRewriter(self._cw)
1026
        select = Select()
1027
        # initialize some variables according to the `role` of `self` in the
1028
1029
1030
        # relation (variable names must respect constraints conventions):
        # * variable for myself (`evar`)
        # * variable for searched entities (`searchvedvar`)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1031
        if role == 'subject':
1032