entity.py 47 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
30
from rql.utils import rqlvar_maker

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

Sylvain Thénault's avatar
Sylvain Thénault committed
39
40
41
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
42
43
44
45
46
47

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

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


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

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

    :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
81

Adrien Di Mascio's avatar
Adrien Di Mascio committed
82
83
84
85
    :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
86
                         cardinality are always skipped.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
87
88
    """
    __registry__ = 'etypes'
89
    __select__ = yes()
90

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

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

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
    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
156
157
158
159
160
161
    @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:
162
            restrictions.append('%s is %s' % (mainvar, cls.__regid__))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
163
164
165
166
167
168
169
170
171
172
173
174
175
        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
176

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

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

247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
    @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 = []
264
        eschema = cls.e_schema
265
        for attr, value in kwargs.items():
266
267
268
269
270
271
272
273
            if attr.startswith('reverse_'):
                attr = attr[len('reverse_'):]
                role = 'object'
            else:
                role = 'subject'
            assert eschema.has_relation(attr, role)
            rschema = eschema.subjrels[attr] if role == 'subject' else eschema.objrels[attr]
            if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
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
301
302
303
304
305
306
307
                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

308
    def __init__(self, req, rset=None, row=None, col=0):
309
        AppObject.__init__(self, req, rset=rset, row=row, col=col)
310
        self._cw_related_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
311
312
313
314
        if rset is not None:
            self.eid = rset[row][col]
        else:
            self.eid = None
315
        self._cw_is_saved = True
316
        self.cw_attr_cache = {}
317

Adrien Di Mascio's avatar
Adrien Di Mascio committed
318
319
    def __repr__(self):
        return '<Entity %s %s %s at %s>' % (
320
            self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
321

322
323
324
    def __cmp__(self, other):
        raise NotImplementedError('comparison not implemented for %s' % self.__class__)

325
326
327
328
329
330
331
332
    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

333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
    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

350
    def has_eid(self): # XXX cw_has_eid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
351
352
353
354
        """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
355
            typed_eid(self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
356
357
358
359
            return True
        except (ValueError, TypeError):
            return False

360
    def cw_is_saved(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
361
        """during entity creation, there is some time during which the entity
362
363
364
        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
365
        """
366
        return self.has_eid() and self._cw_is_saved
367

Adrien Di Mascio's avatar
Adrien Di Mascio committed
368
    @cached
369
    def cw_metainformation(self):
370
371
        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
372
373
        return res

374
    def cw_check_perm(self, action):
375
        self.e_schema.check_perm(self._cw, action, eid=self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
376

377
    def cw_has_perm(self, action):
378
        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
379

380
    def view(self, __vid, __registry='views', w=None, initargs=None, **kwargs): # XXX cw_view
Adrien Di Mascio's avatar
Adrien Di Mascio committed
381
        """shortcut to apply a view on this entity"""
382
383
384
385
        if initargs is None:
            initargs = kwargs
        else:
            initargs.update(kwargs)
386
        view = self._cw.vreg[__registry].select(__vid, self._cw, rset=self.cw_rset,
387
                                                row=self.cw_row, col=self.cw_col,
388
                                                **initargs)
389
        return view.render(row=self.cw_row, col=self.cw_col, w=w, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
390

391
    def absolute_url(self, *args, **kwargs): # XXX cw_url
Adrien Di Mascio's avatar
Adrien Di Mascio committed
392
        """return an absolute url to view this entity"""
393
394
395
396
397
398
399
        # 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
400
401
402
        # 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
403
        use_ext_id = False
404
        if 'base_url' not in kwargs and \
405
               getattr(self._cw, 'search_state', ('normal',))[0] == 'normal':
406
407
408
409
            baseurl = self.cw_metainformation()['source'].get('base-url')
            if baseurl:
                kwargs['base_url'] = baseurl
                use_ext_id = True
410
        if method in (None, 'view'):
411
            try:
412
                kwargs['_restpath'] = self.rest_path(use_ext_id)
413
            except TypeError:
Sylvain Thénault's avatar
Sylvain Thénault committed
414
                warn('[3.4] %s: rest_path() now take use_ext_eid argument, '
415
                     'please update' % self.__regid__, DeprecationWarning)
416
                kwargs['_restpath'] = self.rest_path()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
417
418
        else:
            kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
419
        return self._cw.build_url(method, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
420

421
    def rest_path(self, use_ext_eid=False): # XXX cw_rest_path
Adrien Di Mascio's avatar
Adrien Di Mascio committed
422
423
424
        """returns a REST-like (relative) path for this entity"""
        mainattr, needcheck = self._rest_attr_info()
        etype = str(self.e_schema)
425
426
        path = etype.lower()
        if mainattr != 'eid':
427
            value = getattr(self, mainattr)
428
            if not can_use_rest_path(value):
429
430
431
432
                mainattr = 'eid'
                path += '/eid'
            elif needcheck:
                # make sure url is not ambiguous
433
434
435
436
437
438
                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]
439
440
441
                if nbresults != 1: # ambiguity?
                    mainattr = 'eid'
                    path += '/eid'
Adrien Di Mascio's avatar
Adrien Di Mascio committed
442
        if mainattr == 'eid':
443
            if use_ext_eid:
444
                value = self.cw_metainformation()['extid']
445
446
            else:
                value = self.eid
447
        return '%s/%s' % (path, self._cw.url_quote(value))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
448

449
    def cw_attr_metadata(self, attr, metadata):
450
        """return a metadata for an attribute (None if unspecified)"""
451
        value = getattr(self, '%s_%s' % (attr, metadata), None)
452
        if value is None and metadata == 'encoding':
453
            value = self._cw.vreg.property_value('ui.encoding')
454
        return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
455
456

    def printable_value(self, attr, value=_marker, attrtype=None,
457
                        format='text/html', displaytime=True): # XXX cw_printable_value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
458
459
460
461
462
        """return a displayable value (i.e. unicode string) which may contains
        html tags
        """
        attr = str(attr)
        if value is _marker:
463
            value = getattr(self, attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
464
465
466
467
468
469
        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
470
        props = self.e_schema.rdef(attr)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
471
472
473
        if attrtype == 'String':
            # internalinalized *and* formatted string such as schema
            # description...
Sylvain Thénault's avatar
Sylvain Thénault committed
474
            if props.internationalizable:
475
                value = self._cw._(value)
476
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
477
            if attrformat:
478
479
                return self._cw_mtc_transform(value, attrformat, format,
                                              self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
480
        elif attrtype == 'Bytes':
481
            attrformat = self.cw_attr_metadata(attr, 'format')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
482
            if attrformat:
483
484
485
                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
486
            return u''
487
        value = printable_value(self._cw, attrtype, value, props,
488
                                displaytime=displaytime)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
489
        if format == 'text/html':
Sylvain Thénault's avatar
Sylvain Thénault committed
490
            value = xml_escape(value)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
491
492
        return value

493
494
    def _cw_mtc_transform(self, data, format, target_format, encoding,
                          _engine=ENGINE):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
495
496
497
        trdata = TransformData(data, format, encoding, appobject=self)
        data = _engine.convert(trdata, target_format).decode()
        if format == 'text/html':
498
            data = soup2xhtml(data, self._cw.encoding)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
499
        return data
500

Adrien Di Mascio's avatar
Adrien Di Mascio committed
501
502
    # entity cloning ##########################################################

503
    def copy_relations(self, ceid): # XXX cw_copy_relations
Alexandre Fayolle's avatar
Alexandre Fayolle committed
504
505
506
        """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
507
508
509
510
511

        By default meta and composite relations are skipped.
        Overrides this if you want another behaviour
        """
        assert self.has_eid()
512
        execute = self._cw.execute
Adrien Di Mascio's avatar
Adrien Di Mascio committed
513
        for rschema in self.e_schema.subject_relations():
514
            if rschema.final or rschema.meta:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
515
                continue
516
517
            # skip already defined relations
            if getattr(self, rschema.type):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
518
519
520
                continue
            if rschema.type in self.skip_copy_for:
                continue
521
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
522
523
            rdef = self.e_schema.rdef(rschema)
            if rdef.composite:
524
525
526
                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
527
            if rdef.cardinality[1] in '?1':
528
                continue
Adrien Di Mascio's avatar
Adrien Di Mascio committed
529
            rql = 'SET X %s V WHERE X eid %%(x)s, Y eid %%(y)s, Y %s V' % (
530
                rschema.type, rschema.type)
531
            execute(rql, {'x': self.eid, 'y': ceid})
532
            self.cw_clear_relation_cache(rschema.type, 'subject')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
533
        for rschema in self.e_schema.object_relations():
534
535
536
            if rschema.meta:
                continue
            # skip already defined relations
Sylvain Thénault's avatar
Sylvain Thénault committed
537
            if self.related(rschema.type, 'object'):
538
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
539
            rdef = self.e_schema.rdef(rschema, 'object')
540
            # skip composite relation
Sylvain Thénault's avatar
Sylvain Thénault committed
541
            if rdef.composite:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
542
543
544
                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
545
            if rdef.cardinality[0] in '?1':
Adrien Di Mascio's avatar
Adrien Di Mascio committed
546
547
                continue
            rql = 'SET V %s X WHERE X eid %%(x)s, Y eid %%(y)s, V %s Y' % (
548
                rschema.type, rschema.type)
549
            execute(rql, {'x': self.eid, 'y': ceid})
550
            self.cw_clear_relation_cache(rschema.type, 'object')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
551
552
553
554

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

    @cached
555
    def as_rset(self): # XXX .cw_as_rset
Adrien Di Mascio's avatar
Adrien Di Mascio committed
556
557
        """returns a resultset containing `self` information"""
        rset = ResultSet([(self.eid,)], 'Any X WHERE X eid %(x)s',
558
                         {'x': self.eid}, [(self.__regid__,)])
559
560
        rset.req = self._cw
        return rset
561

562
    def _cw_to_complete_relations(self):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
563
564
        """by default complete final relations to when calling .complete()"""
        for rschema in self.e_schema.subject_relations():
565
            if rschema.final:
Adrien Di Mascio's avatar
Adrien Di Mascio committed
566
                continue
Sylvain Thénault's avatar
Sylvain Thénault committed
567
            targets = rschema.objects(self.e_schema)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
568
            if rschema.inlined:
569
                matching_groups = self._cw.user.matching_groups
570
571
572
                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
573
                    yield rschema, 'subject'
574

575
    def _cw_to_complete_attributes(self, skip_bytes=True, skip_pwd=True):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
576
577
578
579
580
581
582
583
        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
584
            rdef = rschema.rdef(self.e_schema, attrschema)
585
            if not self._cw.user.matching_groups(rdef.get_groups('read')) \
586
                   or (attrschema.type == 'Password' and skip_pwd):
587
                self.cw_attr_cache[attr] = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
588
589
                continue
            yield attr
590

591
    _cw_completed = False
592
    def complete(self, attributes=None, skip_bytes=True, skip_pwd=True): # XXX cw_complete
Adrien Di Mascio's avatar
Adrien Di Mascio committed
593
594
595
596
597
598
599
600
        """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()
601
602
603
604
        if self._cw_completed:
            return
        if attributes is None:
            self._cw_completed = True
Adrien Di Mascio's avatar
Adrien Di Mascio committed
605
606
607
608
        varmaker = rqlvar_maker()
        V = varmaker.next()
        rql = ['WHERE %s eid %%(x)s' % V]
        selected = []
609
        for attr in (attributes or self._cw_to_complete_attributes(skip_bytes, skip_pwd)):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
610
            # if attribute already in entity, nothing to do
611
            if self.cw_attr_cache.has_key(attr):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
612
613
614
615
616
617
618
                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
619
620
621
        # 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
622
            # fetch additional relations (restricted to 0..1 relations)
623
            for rschema, role in self._cw_to_complete_relations():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
624
                rtype = rschema.type
625
                if self.cw_relation_cached(rtype, role):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
626
                    continue
627
628
629
630
631
632
                # 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
633
                var = varmaker.next()
634
635
636
                # 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
637
638
639
640
641
642
                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))
643
644
645
646
647
            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
648
649
            # handle attributes
            for i in xrange(1, lastattr):
650
                self.cw_attr_cache[str(selected[i-1][0])] = rset[i]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
651
652
            # handle relations
            for i in xrange(lastattr, len(rset)):
653
                rtype, role = selected[i-1][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
654
655
656
                value = rset[i]
                if value is None:
                    rrset = ResultSet([], rql, {'x': self.eid})
657
                    rrset.req = self._cw
Adrien Di Mascio's avatar
Adrien Di Mascio committed
658
                else:
659
                    rrset = self._cw.eid_rset(value)
660
                self.cw_set_relation_cache(rtype, role, rrset)
661

662
    def cw_attr_value(self, name):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
663
664
665
666
667
668
669
        """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:
670
            return self.cw_attr_cache[name]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
671
        except KeyError:
672
            if not self.cw_is_saved():
Adrien Di Mascio's avatar
Adrien Di Mascio committed
673
674
675
                return None
            rql = "Any A WHERE X eid %%(x)s, X %s A" % name
            try:
676
                rset = self._cw.execute(rql, {'x': self.eid})
Adrien Di Mascio's avatar
Adrien Di Mascio committed
677
            except Unauthorized:
678
                self.cw_attr_cache[name] = value = None
Adrien Di Mascio's avatar
Adrien Di Mascio committed
679
680
681
            else:
                assert rset.rowcount <= 1, (self, rql, rset.rowcount)
                try:
682
                    self.cw_attr_cache[name] = value = rset.rows[0][0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
683
684
685
686
687
                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':
688
                        self.cw_attr_cache[name] = value = self._cw._('unaccessible')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
689
                    else:
690
691
                        self.cw_attr_cache[name] = value = None
            return value
Adrien Di Mascio's avatar
Adrien Di Mascio committed
692

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
696
697
698
699
700
        :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:
701
            return self._cw_relation_cache(rtype, role, entities, limit)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
702
703
        except KeyError:
            pass
704
705
706
        if not self.has_eid():
            if entities:
                return []
707
            return self._cw.empty_rset()
708
        rql = self.cw_related_rql(rtype, role)
709
        rset = self._cw.execute(rql, {'x': self.eid})
710
        self.cw_set_relation_cache(rtype, role, rset)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
711
712
        return self.related(rtype, role, limit, entities)

713
    def cw_related_rql(self, rtype, role='subject', targettypes=None):
714
        rschema = self._cw.vreg.schema[rtype]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
715
        if role == 'subject':
716
            restriction = 'E eid %%(x)s, E %s X' % rtype
717
718
            if targettypes is None:
                targettypes = rschema.objects(self.e_schema)
719
            else:
720
                restriction += ', X is IN (%s)' % ','.join(targettypes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
721
722
            card = greater_card(rschema, (self.e_schema,), targettypes, 0)
        else:
723
            restriction = 'E eid %%(x)s, X %s E' % rtype
724
725
            if targettypes is None:
                targettypes = rschema.subjects(self.e_schema)
726
            else:
727
                restriction += ', X is IN (%s)' % ','.join(targettypes)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
728
729
            card = greater_card(rschema, targettypes, (self.e_schema,), 1)
        if len(targettypes) > 1:
730
            fetchattrs_list = []
Adrien Di Mascio's avatar
Adrien Di Mascio committed
731
            for ttype in targettypes:
732
                etypecls = self._cw.vreg['etypes'].etype_class(ttype)
733
734
                fetchattrs_list.append(set(etypecls.fetch_attrs))
            fetchattrs = reduce(set.intersection, fetchattrs_list)
735
            rql = etypecls.fetch_rql(self._cw.user, [restriction], fetchattrs,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
736
737
                                     settype=False)
        else:
738
739
            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
740
741
742
743
744
745
746
        # 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:
747
748
749
750
751
752
753
754
755
756
757
            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
758
        return rql
759

Adrien Di Mascio's avatar
Adrien Di Mascio committed
760
761
    # generic vocabulary methods ##############################################

762
    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
763
764
                      vocabconstraints=True):
        """build a rql to fetch `targettype` entities unrelated to this entity
765
766
767
768
        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
769
770
771
        """
        ordermethod = ordermethod or 'fetch_unrelated_order'
        if isinstance(rtype, basestring):
772
            rtype = self._cw.vreg.schema.rschema(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
773
774
775
776
777
778
779
780
        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]
781
782
783
784
785
            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
786
787
        else:
            restriction = []
788
789
            args = {}
            securitycheck_args = {}
Sylvain Thénault's avatar
Sylvain Thénault committed
790
791
        rdef = rtype.role_rdef(self.e_schema, targettype, role)
        insertsecurity = (rdef.has_local_role('add') and not
792
                          rdef.has_perm(self._cw, 'add', **securitycheck_args))
793
        # XXX consider constraint.mainvars to check if constraint apply
Adrien Di Mascio's avatar
Adrien Di Mascio committed
794
795
796
        if vocabconstraints:
            # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
            # will be included as well
Sylvain Thénault's avatar
Sylvain Thénault committed
797
            restriction += [cstr.restriction for cstr in rdef.constraints
Adrien Di Mascio's avatar
Adrien Di Mascio committed
798
799
                            if isinstance(cstr, RQLVocabularyConstraint)]
        else:
Sylvain Thénault's avatar
Sylvain Thénault committed
800
            restriction += [cstr.restriction for cstr in rdef.constraints
Adrien Di Mascio's avatar
Adrien Di Mascio committed
801
                            if isinstance(cstr, RQLConstraint)]
802
803
        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
804
805
806
807
808
                                 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)
809
        if insertsecurity:
Sylvain Thénault's avatar
Sylvain Thénault committed
810
            rqlexprs = rdef.get_rqlexprs('add')
811
812
            rewriter = RQLRewriter(self._cw)
            rqlst = self._cw.vreg.parse(self._cw, rql, args)
813
814
815
816
            if not self.has_eid():
                existant = searchedvar
            else:
                existant = None # instead of 'SO', improve perfs
817
            for select in rqlst.children:
818
819
820
821
822
                varmap = {}
                for var in 'SO':
                    if var in select.defined_vars:
                        varmap[var] = var
                rewriter.rewrite(select, [(varmap, rqlexprs)],
823
                                 select.solutions, args, existant)
824
825
            rql = rqlst.as_string()
        return rql, args
826

Adrien Di Mascio's avatar
Adrien Di Mascio committed
827
    def unrelated(self, rtype, targettype, role='subject', limit=None,
828
                  ordermethod=None): # XXX .cw_unrelated
Adrien Di Mascio's avatar
Adrien Di Mascio committed
829
830
831
        """return a result set of target type objects that may be related
        by a given relation, with self as subject or object
        """
832
        try:
833
            rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
834
        except Unauthorized:
835
            return self._cw.empty_rset()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
836
837
838
        if limit is not None:
            before, after = rql.split(' WHERE ', 1)
            rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
839
        return self._cw.execute(rql, args)
840

841
    # relations cache handling #################################################
842

843
844
845
    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
846
        """
847
        return self._cw_related_cache.get('%s_%s' % (rtype, role))
848

849
    def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
850
851
852
        """return values for the given relation if it's cached on the instance,
        else raise `KeyError`
        """
853
        res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
Sylvain Thénault's avatar
oops    
Sylvain Thénault committed
854
        if limit is not None and limit < len(res):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
855
856
857
858
859
            if entities:
                res = res[:limit]
            else:
                res = res.limit(limit)
        return res
860

861
    def cw_set_relation_cache(self, rtype, role, rset):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
862
863
        """set cached values for the given relation"""
        if rset:
864
            related = list(rset.entities(0))
865
            rschema = self._cw.vreg.schema.rschema(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
866
            if role == 'subject':
Sylvain Thénault's avatar
Sylvain Thénault committed
867
                rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
868
869
                target = 'object'
            else:
Sylvain Thénault's avatar
Sylvain Thénault committed
870
                rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
871
872
873
                target = 'subject'
            if rcard in '?1':
                for rentity in related:
874
                    rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
875
                        self.as_rset(), (self,))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
876
        else:
877
            related = ()
878
        self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
879

880
    def cw_clear_relation_cache(self, rtype=None, role=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
881
882
883
884
        """clear cached values for the given relation or the entire cache if
        no relation is given
        """
        if rtype is None:
885
            self._cw_related_cache = {}
886
            self._cw_adapters_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
887
888
        else:
            assert role
889
            self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
890

891
    def clear_all_caches(self): # XXX cw_clear_all_caches
892
893
894
895
896
897
898
899
        """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 cache
        self._cw_completed = False
900
        self.cw_attr_cache.clear()
901
        # clear relations cache
902
        self.cw_clear_relation_cache()
903
904
905
906
907
        # rest path unique cache
        try:
            del self.__unique
        except AttributeError:
            pass
Sylvain Thénault's avatar
Sylvain Thénault committed
908

Adrien Di Mascio's avatar
Adrien Di Mascio committed
909
    # raw edition utilities ###################################################
910

911
    def set_attributes(self, **kwargs): # XXX cw_set_attributes
912
        _check_cw_unsafe(kwargs)
913
        assert kwargs
914
        assert self.cw_is_saved(), "should not call set_attributes while entity "\
915
               "hasn't been saved yet"
Adrien Di Mascio's avatar
Adrien Di Mascio committed
916
917
918
        relations = []
        for key in kwargs:
            relations.append('X %s %%(%s)s' % (key, key))
919
        # and now update the database
Adrien Di Mascio's avatar
Adrien Di Mascio committed
920
        kwargs['x'] = self.eid
921
        self._cw.execute('SET %s WHERE X eid %%(x)s' % ','.join(relations),
922
                         kwargs)
923
924
        kwargs.pop('x')
        # update current local object _after_ the rql query to avoid
925
926
927
        # interferences between the query execution itself and the cw_edited /
        # skip_security machinery
        self.cw_attr_cache.update(kwargs)
928

929
    def set_relations(self, **kwargs): # XXX cw_set_relations
930
931
932
        """add relations to the given object. To set a relation where this entity
        is the object of the relation, use 'reverse_'<relation> as argument name.

933
        Values may be an entity, a list of entities, or None (meaning that all
934
935
936
        relations of the given type from or to this object should be deleted).
        """
        # XXX update cache
937
        _check_cw_unsafe(kwargs)
938
939
940
941
942
        for attr, values in kwargs.iteritems():
            if attr.startswith('reverse_'):
                restr = 'Y %s X' % attr[len('reverse_'):]
            else:
                restr = 'X %s Y' % attr
943
            if values is None:
944
                self._cw.execute('DELETE %s WHERE X e