entity.py 47.6 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# 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 copy import copy
23
24
from warnings import warn

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
Sylvain Thénault's avatar
Sylvain Thénault committed
28
from logilab.mtconverter import TransformData, TransformError, xml_escape
29

Adrien Di Mascio's avatar
Adrien Di Mascio committed
30
31
from rql.utils import rqlvar_maker

Sylvain Thénault's avatar
Sylvain Thénault committed
32
from cubicweb import Unauthorized, typed_eid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
33
from cubicweb.rset import ResultSet
34
from cubicweb.selectors import yes
35
from cubicweb.appobject import AppObject
36
from cubicweb.req import _check_cw_unsafe
Sylvain Thénault's avatar
Sylvain Thénault committed
37
from cubicweb.schema import RQLVocabularyConstraint, RQLConstraint
38
from cubicweb.rqlrewrite import RQLRewriter
39

Sylvain Thénault's avatar
Sylvain Thénault committed
40
41
42
from cubicweb.uilib import printable_value, soup2xhtml
from cubicweb.mixins import MI_REL_TRIGGERS
from cubicweb.mttransforms import ENGINE
Adrien Di Mascio's avatar
Adrien Di Mascio committed
43
44
45
46
47
48

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

54
55
56
57
58
59
60
61
62
63
64
65
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


Adrien Di Mascio's avatar
Adrien Di Mascio committed
66

67
class Entity(AppObject):
68
69
    """an entity instance has e_schema automagically set on
    the class and instances has access to their issuing cursor.
70

71
72
73
74
    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
75
76
77
78
79
80
81
82

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

    :type rest_var: str
    :cvar rest_var: indicates which attribute should be used to build REST urls
                    If None is specified, the first non-meta attribute will
                    be used
83

Adrien Di Mascio's avatar
Adrien Di Mascio committed
84
85
86
87
    :type skip_copy_for: list
    :cvar skip_copy_for: a list of relations 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
88
                         cardinality are always skipped.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
89
90
    """
    __registry__ = 'etypes'
91
    __select__ = yes()
92

93
    # class attributes that must be set in class definition
Adrien Di Mascio's avatar
Adrien Di Mascio committed
94
    rest_attr = None
95
    fetch_attrs = None
96
    skip_copy_for = ('in_state',)
97
98
    # class attributes set automatically at registration time
    e_schema = None
99

Adrien Di Mascio's avatar
Adrien Di Mascio committed
100
    @classmethod
101
    def __initialize__(cls, schema):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
102
103
104
        """initialize a specific entity class by adding descriptors to access
        entity type's attributes and relations
        """
105
        etype = cls.__regid__
Adrien Di Mascio's avatar
Adrien Di Mascio committed
106
        assert etype != 'Any', etype
107
        cls.e_schema = eschema = schema.eschema(etype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
108
        for rschema, _ in eschema.attribute_definitions():
109
            if rschema.type == 'eid':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
110
                continue
111
            setattr(cls, rschema.type, Attribute(rschema.type))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
112
        mixins = []
113
114
115
        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
116
117
118
119
120
                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)
121
            if role == 'subject':
122
                attr = rschema.type
Adrien Di Mascio's avatar
Adrien Di Mascio committed
123
            else:
124
                attr = 'reverse_%s' % rschema.type
125
            setattr(cls, attr, Relation(rschema, role))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
126
        if mixins:
127
128
129
130
131
132
133
134
135
136
137
138
            # 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)
139

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
    fetch_attrs = ('modification_date',)
    @classmethod
    def fetch_order(cls, attr, var):
        """class method used to control sort order when multiple entities of
        this type are fetched
        """
        return cls.fetch_unrelated_order(attr, var)

    @classmethod
    def fetch_unrelated_order(cls, attr, var):
        """class method used to control sort order when multiple entities of
        this type are fetched to use in edition (eg propose them to create a
        new relation on an edited entity).
        """
        if attr == 'modification_date':
            return '%s DESC' % var
        return None

Adrien Di Mascio's avatar
Adrien Di Mascio committed
158
159
160
161
162
163
    @classmethod
    def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
                  settype=True, ordermethod='fetch_order'):
        """return a rql to fetch all entities of the class type"""
        restrictions = restriction or []
        if settype:
164
            restrictions.append('%s is %s' % (mainvar, cls.__regid__))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
165
166
167
168
169
170
171
172
173
174
175
176
177
        if fetchattrs is None:
            fetchattrs = cls.fetch_attrs
        selection = [mainvar]
        orderby = []
        # start from 26 to avoid possible conflicts with X
        varmaker = rqlvar_maker(index=26)
        cls._fetch_restrictions(mainvar, varmaker, fetchattrs, selection,
                                orderby, restrictions, user, ordermethod)
        rql = 'Any %s' % ','.join(selection)
        if orderby:
            rql +=  ' ORDERBY %s' % ','.join(orderby)
        rql += ' WHERE %s' % ', '.join(restrictions)
        return rql
178

Adrien Di Mascio's avatar
Adrien Di Mascio committed
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
    @classmethod
    def _fetch_restrictions(cls, mainvar, varmaker, fetchattrs,
                            selection, orderby, restrictions, user,
                            ordermethod='fetch_order', visited=None):
        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:
194
                rschema = eschema.subjrels[attr]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
195
196
            except KeyError:
                cls.warning('skipping fetch_attr %s defined in %s (not found in schema)',
197
                            attr, cls.__regid__)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
198
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
199
200
            rdef = eschema.rdef(attr)
            if not user.matching_groups(rdef.get_groups('read')):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
201
202
203
204
205
                continue
            var = varmaker.next()
            selection.append(var)
            restriction = '%s %s %s' % (mainvar, attr, var)
            restrictions.append(restriction)
206
            if not rschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
207
208
                # XXX this does not handle several destination types
                desttype = rschema.objects(eschema.type)[0]
Sylvain Thénault's avatar
Sylvain Thénault committed
209
                card = rdef.cardinality[0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
210
                if card not in '?1':
211
212
                    cls.warning('bad relation %s specified in fetch attrs for %s',
                                 attr, cls)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
213
214
215
                    selection.pop()
                    restrictions.pop()
                    continue
216
217
218
219
220
                # 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.
                restrictions[-1] += '?'
Sylvain Thénault's avatar
Sylvain Thénault committed
221
                # XXX user._cw.vreg iiiirk
222
                destcls = user._cw.vreg['etypes'].etype_class(desttype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
223
224
225
226
227
228
229
230
                destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
                                            selection, orderby, restrictions,
                                            user, ordermethod, visited=visited)
            orderterm = getattr(cls, ordermethod)(attr, var)
            if orderterm:
                orderby.append(orderterm)
        return selection, orderby, restrictions

231
232
233
234
235
236
237
238
239
    @classmethod
    @cached
    def _rest_attr_info(cls):
        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():
240
                if rschema.final and rschema != 'eid' and cls.e_schema.has_unique_values(rschema):
241
242
243
244
245
246
247
                    mainattr = str(rschema)
                    needcheck = False
                    break
        if mainattr == 'eid':
            needcheck = False
        return mainattr, needcheck

248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
    @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(req.execute, name=u'Logilab')
        >>> personcls.cw_instantiate(req.execute, firstname=u'John', lastname=u'Doe',
        ...                          works_for=c)

        """
        rql = 'INSERT %s X' % cls.__regid__
        relations = []
        restrictions = set()
        pending_relations = []
        for attr, value in kwargs.items():
            if isinstance(value, (tuple, list, set, frozenset)):
                if len(value) == 1:
                    value = iter(value).next()
                else:
                    del kwargs[attr]
                    pending_relations.append( (attr, value) )
                    continue
            if hasattr(value, 'eid'): # non final relation
                rvar = attr.upper()
                # XXX safer detection of object relation
                if attr.startswith('reverse_'):
                    relations.append('%s %s X' % (rvar, attr[len('reverse_'):]))
                else:
                    relations.append('X %s %s' % (attr, rvar))
                restriction = '%s eid %%(%s)s' % (rvar, attr)
                if not restriction in restrictions:
                    restrictions.add(restriction)
                kwargs[attr] = value.eid
            else: # attribute
                relations.append('X %s %%(%s)s' % (attr, attr))
        if relations:
            rql = '%s: %s' % (rql, ', '.join(relations))
        if restrictions:
            rql = '%s WHERE %s' % (rql, ', '.join(restrictions))
        created = execute(rql, kwargs).get_entity(0, 0)
        for attr, values in pending_relations:
            if attr.startswith('reverse_'):
                restr = 'Y %s X' % attr[len('reverse_'):]
            else:
                restr = 'X %s Y' % attr
            execute('SET %s WHERE X eid %%(x)s, Y eid IN (%s)' % (
                restr, ','.join(str(r.eid) for r in values)),
                    {'x': created.eid}, build_descr=False)
        return created

301
    def __init__(self, req, rset=None, row=None, col=0):
302
        AppObject.__init__(self, req, rset=rset, row=row, col=col)
303
        self._cw_related_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
304
305
306
307
        if rset is not None:
            self.eid = rset[row][col]
        else:
            self.eid = None
308
        self._cw_is_saved = True
309
        self.cw_attr_cache = {}
310

Adrien Di Mascio's avatar
Adrien Di Mascio committed
311
312
    def __repr__(self):
        return '<Entity %s %s %s at %s>' % (
313
            self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
314

315
316
317
318
319
320
321
322
    def __json_encode__(self):
        """custom json dumps hook to dump the entity's eid
        which is not part of dict structure itself
        """
        dumpable = dict(self)
        dumpable['eid'] = self.eid
        return dumpable

Adrien Di Mascio's avatar
Adrien Di Mascio committed
323
324
325
326
327
328
    def __nonzero__(self):
        return True

    def __hash__(self):
        return id(self)

Sylvain Thénault's avatar
Sylvain Thénault committed
329
    def __cmp__(self, other):
330
331
        raise NotImplementedError('comparison not implemented for %s' % self.__class__)

332
333
334
335
336
337
    def __contains__(self, key):
        return key in self.cw_attr_cache

    def __iter__(self):
        return iter(self.cw_attr_cache)

338
339
340
341
342
    def __getitem__(self, key):
        if key == 'eid':
            warn('[3.7] entity["eid"] is deprecated, use entity.eid instead',
                 DeprecationWarning, stacklevel=2)
            return self.eid
343
        return self.cw_attr_cache[key]
344

345
346
347
    def __setitem__(self, attr, value):
        """override __setitem__ to update self.edited_attributes.

348
        Typically, a before_[update|add]_hook could do::
349
350
351

            entity['generated_attr'] = generated_value

352
353
354
        and this way, edited_attributes will be updated accordingly. Also, add
        the attribute to skip_security since we don't want to check security
        for such attributes set by hooks.
355
356
357
358
359
360
        """
        if attr == 'eid':
            warn('[3.7] entity["eid"] = value is deprecated, use entity.eid = value instead',
                 DeprecationWarning, stacklevel=2)
            self.eid = value
        else:
361
            self.cw_attr_cache[attr] = value
362
363
364
365
            # don't add attribute into skip_security if already in edited
            # attributes, else we may accidentaly skip a desired security check
            if hasattr(self, 'edited_attributes') and \
                   attr not in self.edited_attributes:
366
                self.edited_attributes.add(attr)
367
                self._cw_skip_security_attributes.add(attr)
368

369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
    def __delitem__(self, attr):
        """override __delitem__ to update self.edited_attributes on cleanup of
        undesired changes introduced in the entity's dict. For example, see the
        code snippet below from the `forge` cube:

        .. sourcecode:: python

            edited = self.entity.edited_attributes
            has_load_left = 'load_left' in edited
            if 'load' in edited and self.entity.load_left is None:
                self.entity.load_left = self.entity['load']
            elif not has_load_left and edited:
                # cleanup, this may cause undesired changes
                del self.entity['load_left']

        """
385
        del self.cw_attr_cache[attr]
386
387
388
        if hasattr(self, 'edited_attributes'):
            self.edited_attributes.remove(attr)

389
390
391
    def clear(self):
        self.cw_attr_cache.clear()

392
393
394
    def get(self, key, default=None):
        return self.cw_attr_cache.get(key, default)

395
    def setdefault(self, attr, default):
396
        """override setdefault to update self.edited_attributes"""
397
        value = self.cw_attr_cache.setdefault(attr, default)
398
399
400
401
        # don't add attribute into skip_security if already in edited
        # attributes, else we may accidentaly skip a desired security check
        if hasattr(self, 'edited_attributes') and \
               attr not in self.edited_attributes:
402
            self.edited_attributes.add(attr)
403
            self._cw_skip_security_attributes.add(attr)
404
        return value
405

406
407
408
409
410
    def pop(self, attr, default=_marker):
        """override pop to update self.edited_attributes on cleanup of
        undesired changes introduced in the entity's dict. See `__delitem__`
        """
        if default is _marker:
411
            value = self.cw_attr_cache.pop(attr)
412
        else:
413
            value = self.cw_attr_cache.pop(attr, default)
414
415
416
417
        if hasattr(self, 'edited_attributes') and attr in self.edited_attributes:
            self.edited_attributes.remove(attr)
        return value

418
419
420
421
422
423
    def update(self, values):
        """override update to update self.edited_attributes. See `__setitem__`
        """
        for attr, value in values.items():
            self[attr] = value # use self.__setitem__ implementation

424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
    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

441
    def has_eid(self): # XXX cw_has_eid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
442
443
444
445
        """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
446
            typed_eid(self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
447
448
449
450
            return True
        except (ValueError, TypeError):
            return False

451
    def cw_is_saved(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
452
        """during entity creation, there is some time during which the entity
453
454
455
        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
456
        """
457
        return self.has_eid() and self._cw_is_saved
458

Adrien Di Mascio's avatar
Adrien Di Mascio committed
459
    @cached
460
    def cw_metainformation(self):
461
462
        res = dict(zip(('type', 'source', 'extid'), self._cw.describe(self.eid)))
        res['source'] = self._cw.source_defs()[res['source']]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
463
464
        return res

465
    def cw_check_perm(self, action):
466
        self.e_schema.check_perm(self._cw, action, eid=self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
467

468
    def cw_has_perm(self, action):
469
        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
470

471
    def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
Adrien Di Mascio's avatar
Adrien Di Mascio committed
472
        """shortcut to apply a view on this entity"""
473
474
475
476
        if initargs is None:
            initargs = kwargs
        else:
            initargs.update(kwargs)
477
        view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
478
                                                row=self.cw_row, col=self.cw_col,
479
                                                **initargs)
480
        return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
481

482
    def absolute_url(self, *args, **kwargs): # XXX cw_url
Adrien Di Mascio's avatar
Adrien Di Mascio committed
483
        """return an absolute url to view this entity"""
484
485
486
487
488
489
490
        # 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
491
492
493
        # 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
494
        if getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
495
            kwargs['base_url'] = self.cw_metainformation()['source'].get('base-url')
496
497
498
            use_ext_id = True
        else:
            use_ext_id = False
499
        if method in (None, 'view'):
500
            try:
501
                kwargs['_restpath'] = self.rest_path(use_ext_id)
502
            except TypeError:
Sylvain Thénault's avatar
Sylvain Thénault committed
503
                warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
504
                     'please update' % self.__regid__, DeprecationWarning)
505
                kwargs['_restpath'] = self.rest_path()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
506
507
        else:
            kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
508
        return self._cw.build_url(method, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
509

510
    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
Adrien Di Mascio's avatar
Adrien Di Mascio committed
511
512
513
        """returns a REST-like (relative) path for this entity"""
        mainattr, needcheck = self._rest_attr_info()
        etype = str(self.e_schema)
514
515
        path = etype.lower()
        if mainattr != 'eid':
516
            value = getattr(self, mainattr)
517
            if not can_use_rest_path(value):
518
519
520
521
                mainattr = 'eid'
                path += '/eid'
            elif needcheck:
                # make sure url is not ambiguous
522
523
524
525
526
527
                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]
528
529
530
                if nbresults != 1: # ambiguity?
                    mainattr = 'eid'
                    path += '/eid'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
531
        if mainattr == 'eid':
532
            if use_ext_eid:
533
                value = self.cw_metainformation()['extid']
534
535
            else:
                value = self.eid
536
        return '%s/%s' % (path, self._cw.url_quote(value))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
537

538
    def cw_attr_metadata(self, attr, metadata):
539
        """return a metadata for an attribute (None if unspecified)"""
540
        value = getattr(self, '%s_%s' % (attr, metadata), None)
541
        if value is None and metadata == 'encoding':
542
            value = self._cw.vreg.property_value('ui.encoding')
543
        return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
544
545

    def printable_value(self, attr, value=_marker, attrtype=None,
546
                        format='text/html', displaytime=True): # XXX cw_printable_value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
547
548
549
550
551
        """return a displayable value (i.e. unicode string) which may contains
        html tags
        """
        attr = str(attr)
        if value is _marker:
552
            value = getattr(self, attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
553
554
555
556
557
558
        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
559
        props = self.e_schema.rdef(attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
560
561
562
        if attrtype == 'String':
            # internalinalized *and* formatted string such as schema
            # description...
Sylvain Thénault's avatar
Sylvain Thénault committed
563
            if props.internationalizable:
564
                value = self._cw._(value)
565
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
566
            if attrformat:
567
568
                return self._cw_mtc_transform(value, attrformat, format,
                                              self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
569
        elif attrtype == 'Bytes':
570
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
571
            if attrformat:
572
573
574
                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
575
            return u''
576
        value = printable_value(self._cw, attrtype, value, props,
577
                                displaytime=displaytime)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
578
        if format == 'text/html':
Sylvain Thénault's avatar
Sylvain Thénault committed
579
            value = xml_escape(value)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
580
581
        return value

582
583
    def _cw_mtc_transform(self, data, format, target_format, encoding,
                          _engine=ENGINE):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
584
585
586
        trdata = TransformData(data, format, encoding, appobject=self)
        data = _engine.convert(trdata, target_format).decode()
        if format == 'text/html':
587
            data = soup2xhtml(data, self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
588
        return data
589

Adrien Di Mascio's avatar
Adrien Di Mascio committed
590
591
    # entity cloning ##########################################################

592
593
594
595
596
597
    def cw_copy(self):
        thecopy = copy(self)
        thecopy.cw_attr_cache = copy(self.cw_attr_cache)
        thecopy._cw_related_cache = {}
        return thecopy

598
    def copy_relations(self, ceid): # XXX cw_copy_relations
Alexandre Fayolle's avatar
Alexandre Fayolle committed
599
600
601
        """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
602
603
604
605
606

        By default meta and composite relations are skipped.
        Overrides this if you want another behaviour
        """
        assert self.has_eid()
607
        execute = self._cw.execute
Adrien Di Mascio's avatar
Adrien Di Mascio committed
608
        for rschema in self.e_schema.subject_relations():
609
            if rschema.final or rschema.meta:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
610
                continue
611
612
            # skip already defined relations
            if getattr(self, rschema.type):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
613
614
615
                continue
            if rschema.type in self.skip_copy_for:
                continue
616
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
617
618
            rdef = self.e_schema.rdef(rschema)
            if rdef.composite:
619
620
621
                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
622
            if rdef.cardinality[1] in '?1':
623
                continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
624
            rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
625
                rschema.type, rschema.type)
626
            execute(rql, {'x': self.eid, 'y': ceid})
627
            self.cw_clear_relation_cache(rschema.type, 'subject')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
628
        for rschema in self.e_schema.object_relations():
629
630
631
            if rschema.meta:
                continue
            # skip already defined relations
Sylvain Thénault's avatar
Sylvain Thénault committed
632
            if self.related(rschema.type, 'object'):
633
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
634
            rdef = self.e_schema.rdef(rschema, 'object')
635
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
636
            if rdef.composite:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
637
638
639
                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
640
            if rdef.cardinality[0] in '?1':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
641
642
                continue
            rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
643
                rschema.type, rschema.type)
644
            execute(rql, {'x': self.eid, 'y': ceid})
645
            self.cw_clear_relation_cache(rschema.type, 'object')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
646
647
648
649

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

    @cached
650
    def as_rset(self): # XXX .cw_as_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
651
652
        """returns a resultset containing `self` information"""
        rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
653
                         {'x': self.eid}, [(self.__regid__,)])
654
655
        rset.req = self._cw
        return rset
656

657
    def _cw_to_complete_relations(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
658
659
        """by default complete final relations to when calling .complete()"""
        for rschema in self.e_schema.subject_relations():
660
            if rschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
661
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
662
            targets = rschema.objects(self.e_schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
663
            if rschema.inlined:
664
                matching_groups = self._cw.user.matching_groups
665
666
667
                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
668
                    yield rschema, 'subject'
669

670
    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
671
672
673
674
675
676
677
678
        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
679
            rdef = rschema.rdef(self.e_schema, attrschema)
680
            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
681
                   or (attrschema.type == 'Password' and skip_pwd):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
682
683
684
                self[attr] = None
                continue
            yield attr
685

686
    _cw_completed = False
687
    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
Adrien Di Mascio's avatar
Adrien Di Mascio committed
688
689
690
691
692
693
694
695
        """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()
696
697
698
699
        if self._cw_completed:
            return
        if attributes is None:
            self._cw_completed = True
Adrien Di Mascio's avatar
Adrien Di Mascio committed
700
701
702
703
        varmaker = rqlvar_maker()
        V = varmaker.next()
        rql = ['WHERE %s eid %%(x)s' % V]
        selected = []
704
        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
705
            # if attribute already in entity, nothing to do
706
            if self.cw_attr_cache.has_key(attr):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
707
708
709
710
711
712
713
714
715
                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))
        # +1 since this doen't include the main variable
        lastattr = len(selected) + 1
        if attributes is None:
            # fetch additional relations (restricted to 0..1 relations)
716
            for rschema, role in self._cw_to_complete_relations():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
717
                rtype = rschema.type
718
                if self.cw_relation_cached(rtype, role):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
719
                    continue
720
721
722
723
724
725
                # 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
726
                var = varmaker.next()
727
728
729
                # 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
730
731
732
733
734
735
                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))
736
            rset = self._cw.execute(rql, {'x': self.eid}, build_descr=False)[0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
737
738
739
740
741
            # handle attributes
            for i in xrange(1, lastattr):
                self[str(selected[i-1][0])] = rset[i]
            # handle relations
            for i in xrange(lastattr, len(rset)):
742
                rtype, role = selected[i-1][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
743
744
745
                value = rset[i]
                if value is None:
                    rrset = ResultSet([], rql, {'x': self.eid})
746
                    rrset.req = self._cw
Adrien Di Mascio's avatar
Adrien Di Mascio committed
747
                else:
748
                    rrset = self._cw.eid_rset(value)
749
                self.cw_set_relation_cache(rtype, role, rrset)
750

751
    def cw_attr_value(self, name):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
752
753
754
755
756
757
758
        """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:
759
            value = self.cw_attr_cache[name]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
760
        except KeyError:
761
            if not self.cw_is_saved():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
762
763
764
                return None
            rql = "Any A WHERE X eid %%(x)s, X %s A" % name
            try:
765
                rset = self._cw.execute(rql, {'x': self.eid})
Adrien Di Mascio's avatar
Adrien Di Mascio committed
766
767
768
769
770
771
772
773
774
775
776
            except Unauthorized:
                self[name] = value = None
            else:
                assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                try:
                    self[name] = value = rset.rows[0][0]
                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':
Sylvain Thénault's avatar
remark    
Sylvain Thénault committed
777
                        # XXX (syt) imo emtpy string is better
778
                        self[name] = value = self._cw._('unaccessible')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
779
780
781
782
                    else:
                        self[name] = value = None
        return value

783
    def related(self, rtype, role='subject', limit=None, entities=False): # XXX .cw_related
Adrien Di Mascio's avatar
Adrien Di Mascio committed
784
        """returns a resultset of related entities
785

Adrien Di Mascio's avatar
Adrien Di Mascio committed
786
787
788
789
790
        :param role: is 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
        """
        try:
791
            return self._cw_relation_cache(rtype, role, entities, limit)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
792
793
        except KeyError:
            pass
794
795
796
797
        if not self.has_eid():
            if entities:
                return []
            return self.empty_rset()
798
        rql = self.cw_related_rql(rtype, role)
799
        rset = self._cw.execute(rql, {'x': self.eid})
800
        self.cw_set_relation_cache(rtype, role, rset)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
801
802
        return self.related(rtype, role, limit, entities)

803
    def cw_related_rql(self, rtype, role='subject', targettypes=None):
804
        rschema = self._cw.vreg.schema[rtype]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
805
        if role == 'subject':
806
            restriction = 'E eid %%(x)s, E %s X' % rtype
807
808
            if targettypes is None:
                targettypes = rschema.objects(self.e_schema)
809
            else:
810
                restriction += ', X is IN (%s)' % ','.join(targettypes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
811
812
            card = greater_card(rschema, (self.e_schema,), targettypes, 0)
        else:
813
            restriction = 'E eid %%(x)s, X %s E' % rtype
814
815
            if targettypes is None:
                targettypes = rschema.subjects(self.e_schema)
816
            else:
817
                restriction += ', X is IN (%s)' % ','.join(targettypes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
818
819
            card = greater_card(rschema, targettypes, (self.e_schema,), 1)
        if len(targettypes) > 1:
820
            fetchattrs_list = []
Adrien Di Mascio's avatar
Adrien Di Mascio committed
821
            for ttype in targettypes:
822
                etypecls = self._cw.vreg['etypes'].etype_class(ttype)
823
824
                fetchattrs_list.append(set(etypecls.fetch_attrs))
            fetchattrs = reduce(set.intersection, fetchattrs_list)
825
            rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
826
827
                                     settype=False)
        else:
828
829
            etypecls = self._cw.vreg['etypes'].etype_class(targettypes[0])
            rql = etypecls.fetch_rql(self._cw.user, [restriction], settype=False)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
830
831
832
833
834
835
836
        # optimisation: remove ORDERBY if cardinality is 1 or ? (though
        # greater_card return 1 for those both cases)
        if card == '1':
            if ' ORDERBY ' in rql:
                rql = '%s WHERE %s' % (rql.split(' ORDERBY ', 1)[0],
                                       rql.split(' WHERE ', 1)[1])
        elif not ' ORDERBY ' in rql:
837
838
839
840
841
842
843
844
845
846
847
            args = rql.split(' WHERE ', 1)
            # if modification_date already retreived, we should use it instead
            # of adding another variable for sort. This should be be problematic
            # but it's actually with sqlserver, see ticket #694445
            if 'X modification_date ' in args[1]:
                var = args[1].split('X modification_date ', 1)[1].split(',', 1)[0]
                args.insert(1, var.strip())
                rql = '%s ORDERBY %s DESC WHERE %s' % tuple(args)
            else:
                rql = '%s ORDERBY Z DESC WHERE X modification_date Z, %s' % \
                      tuple(args)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
848
        return rql
849

Adrien Di Mascio's avatar
Adrien Di Mascio committed
850
851
    # generic vocabulary methods ##############################################

852
    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
853
854
                      vocabconstraints=True):
        """build a rql to fetch `targettype` entities unrelated to this entity
855
856
857
858
        using (rtype, role) relation.

        Consider relation permissions so that returned entities may be actually
        linked by `rtype`.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
859
860
861
        """
        ordermethod = ordermethod or 'fetch_unrelated_order'
        if isinstance(rtype, basestring):
862
            rtype = self._cw.vreg.schema.rschema(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
863
864
865
866
867
868
869
870
        if role == 'subject':
            evar, searchedvar = 'S', 'O'
            subjtype, objtype = self.e_schema, targettype
        else:
            searchedvar, evar = 'S', 'O'
            objtype, subjtype = self.e_schema, targettype
        if self.has_eid():
            restriction = ['NOT S %s O' % rtype, '%s eid %%(x)s' % evar]
871
872
873
874
875
            args = {'x': self.eid}
            if role == 'subject':
                securitycheck_args = {'fromeid': self.eid}
            else:
                securitycheck_args = {'toeid': self.eid}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
876
877
        else:
            restriction = []
878
879
            args = {}
            securitycheck_args = {}
Sylvain Thénault's avatar
Sylvain Thénault committed
880
881
        rdef = rtype.role_rdef(self.e_schema, targettype, role)
        insertsecurity = (rdef.has_local_role('add') and not
882
                          rdef.has_perm(self._cw, 'add', **securitycheck_args))
883
        # XXX consider constraint.mainvars to check if constraint apply
Adrien Di Mascio's avatar
Adrien Di Mascio committed
884
885
886
        if vocabconstraints:
            # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
            # will be included as well
Sylvain Thénault's avatar
Sylvain Thénault committed
887
            restriction += [cstr.restriction for cstr in rdef.constraints
Adrien Di Mascio's avatar
Adrien Di Mascio committed
888
889
                            if isinstance(cstr, RQLVocabularyConstraint)]
        else:
Sylvain Thénault's avatar
Sylvain Thénault committed
890
            restriction += [cstr.restriction for cstr in rdef.constraints
Adrien Di Mascio's avatar
Adrien Di Mascio committed
891
                            if isinstance(cstr, RQLConstraint)]
892
893
        etypecls = self._cw.vreg['etypes'].etype_class(targettype)
        rql = etypecls.fetch_rql(self._cw.user, restriction,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
894
895
896
897
898
                                 mainvar=searchedvar, ordermethod=ordermethod)
        # ensure we have an order defined
        if not ' ORDERBY ' in rql:
            before, after = rql.split(' WHERE ', 1)
            rql = '%s ORDERBY %s WHERE %s' % (before, searchedvar, after)
899
        if insertsecurity:
Sylvain Thénault's avatar
Sylvain Thénault committed
900
            rqlexprs = rdef.get_rqlexprs('add')
901
902
            rewriter = RQLRewriter(self._cw)
            rqlst = self._cw.vreg.parse(self._cw, rql, args)
903
904
905
906
            if not self.has_eid():
                existant = searchedvar
            else:
                existant = None # instead of 'SO', improve perfs
907
908
            for select in rqlst.children:
                rewriter.rewrite(select, [((searchedvar, searchedvar), rqlexprs)],
909
                                 select.solutions, args, existant)
910
911
            rql = rqlst.as_string()
        return rql, args
912

Adrien Di Mascio's avatar
Adrien Di Mascio committed
913
    def unrelated(self, rtype, targettype, role='subject', limit=None,
914
                  ordermethod=None): # XXX .cw_unrelated
Adrien Di Mascio's avatar
Adrien Di Mascio committed
915
916
917
        """return a result set of target type objects that may be related
        by a given relation, with self as subject or object
        """
918
        try:
919
            rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
920
        except Unauthorized:
921
            return self._cw.empty_rset()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
922
923
924
        if limit is not None:
            before, after = rql.split(' WHERE ', 1)
            rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
925
        return self._cw.execute(rql, args)
926

927
    # relations cache handling #################################################
928

929
930
931
    def cw_relation_cached(self, rtype, role):
        """return None if the given relation isn't already cached on the
        instance, else the content of the cache (a 2-uple (rset, entities)).
Adrien Di Mascio's avatar
Adrien Di Mascio committed
932
        """
933
        return self._cw_related_cache.get('%s_%s' % (rtype, role))
934

935
    def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
936
937
938
        """return values for the given relation if it's cached on the instance,
        else raise `KeyError`
        """
939
        res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
Sylvain Thénault's avatar
oops    
Sylvain Thénault committed
940
        if limit is not None and limit < len(res):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
941
942
943
944
945
            if entities:
                res = res[:limit]
            else:
                res = res.limit(limit)
        return res
946

947
    def cw_set_relation_cache(self, rtype, role, rset):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
948
949
        """set cached values for the given relation"""
        if rset:
950
            related = list(rset.entities(0))
951
            rschema = self._cw.vreg.schema.rschema(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
952
            if role == 'subject':
Sylvain Thénault's avatar
Sylvain Thénault committed
953
                rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
954
955
                target = 'object'
            else:
Sylvain Thénault's avatar
Sylvain Thénault committed
956
                rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
957
958
959
                target = 'subject'
            if rcard in '?1':
                for rentity in related:
960
                    rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
961
                        self.as_rset(), (self,))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
962
        else:
963
            related = ()
964
        self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
965

966
    def cw_clear_relation_cache(self, rtype=None, role=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
967
968
969
970
        """clear cached values for the given relation or the entire cache if
        no relation is given
        """
        if rtype is None:
971
            self._cw_related_cache = {}
972
            self._cw_adapters_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
973
974
        else:
            assert role
975
            self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
976

977
    def clear_all_caches(self): # XXX cw_clear_all_caches
978
979
980
981
982
983
984
        """flush all caches on this entity. Further attributes/relations access
        will triggers new database queries to get back values.

        If you use custom caches on your entity class (take care to @cached!),
        you should override this method to clear them as well.
        """
        # clear attributes cach