entity.py 48.7 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

31
from cubicweb import Unauthorized, typed_eid, neg_role
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
    @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"""
160
        # XXX update api and implementation to AST manipulation (see unrelated rql)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
161
162
        restrictions = restriction or []
        if settype:
163
            restrictions.append('%s is %s' % (mainvar, cls.__regid__))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
164
165
166
167
168
        if fetchattrs is None:
            fetchattrs = cls.fetch_attrs
        selection = [mainvar]
        orderby = []
        # start from 26 to avoid possible conflicts with X
169
        # XXX not enough to be sure it'll be no conflicts
Adrien Di Mascio's avatar
Adrien Di Mascio committed
170
171
172
173
174
175
176
177
        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
                destcls._fetch_restrictions(var, varmaker, destcls.fetch_attrs,
                                            selection, orderby, restrictions,
                                            user, ordermethod, visited=visited)
226
227
228
229
            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
230
231
        return selection, orderby, restrictions

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

249
250
251
252
253
254
255
256
    @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')
257
258
259
        >>> c = companycls.cw_instantiate(session.execute, name=u'Logilab')
        >>> p = personcls.cw_instantiate(session.execute, firstname=u'John', lastname=u'Doe',
        ...                              works_for=c)
260

261
262
        You can also set relation where the entity has 'object' role by
        prefixing the relation by 'reverse_'.
263
264
265
266
267
        """
        rql = 'INSERT %s X' % cls.__regid__
        relations = []
        restrictions = set()
        pending_relations = []
268
        eschema = cls.e_schema
269
        for attr, value in kwargs.items():
270
271
272
273
274
275
276
            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]
277
            if not rschema.final and isinstance(value, (tuple, list, set, frozenset)):
278
279
280
                if len(value) == 1:
                    value = iter(value).next()
                else:
281
                    # prepare IN clause
282
283
284
285
286
                    del kwargs[attr]
                    pending_relations.append( (attr, value) )
                    continue
            if hasattr(value, 'eid'): # non final relation
                rvar = attr.upper()
287
288
                if role == 'object':
                    relations.append('%s %s X' % (rvar, attr))
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
                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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
322
323
    def __repr__(self):
        return '<Entity %s %s %s at %s>' % (
324
            self.e_schema, self.eid, self.cw_attr_cache.keys(), id(self))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
325

326
327
328
    def __cmp__(self, other):
        raise NotImplementedError('comparison not implemented for %s' % self.__class__)

329
330
331
332
333
334
335
336
    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

337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
    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

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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
372
    @cached
373
    def cw_metainformation(self):
374
375
        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
376
377
        return res

378
    def cw_check_perm(self, action):
379
        self.e_schema.check_perm(self._cw, action, eid=self.eid)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
380

381
    def cw_has_perm(self, action):
382
        return self.e_schema.has_perm(self._cw, action, eid=self.eid)
383

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

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

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

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

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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
505
506
    # entity cloning ##########################################################

507
    def copy_relations(self, ceid): # XXX cw_copy_relations
Alexandre Fayolle's avatar
Alexandre Fayolle committed
508
509
510
        """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
511
512
513
514
515

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

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

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

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

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

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

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

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

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

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

Adrien Di Mascio's avatar
Adrien Di Mascio committed
764
765
    # generic vocabulary methods ##############################################

766
    def cw_unrelated_rql(self, rtype, targettype, role, ordermethod=None,
767
                         vocabconstraints=True):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
768
        """build a rql to fetch `targettype` entities unrelated to this entity
769
770
771
772
        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
773
774
775
        """
        ordermethod = ordermethod or 'fetch_unrelated_order'
        if isinstance(rtype, basestring):
776
            rtype = self._cw.vreg.schema.rschema(rtype)
777
778
779
780
781
782
783
        rdef = rtype.role_rdef(self.e_schema, targettype, role)
        rewriter = RQLRewriter(self._cw)
        # initialize some variables according to the `role` of `self` in the
        # relation:
        # * variable for myself (`evar`) and searched entities (`searchvedvar`)
        # * entity type of the subject (`subjtype`) and of the object
        #   (`objtype`) of the relation
Adrien Di Mascio's avatar
Adrien Di Mascio committed
784
785
786
787
788
789
        if role == 'subject':
            evar, searchedvar = 'S', 'O'
            subjtype, objtype = self.e_schema, targettype
        else:
            searchedvar, evar = 'S', 'O'
            objtype, subjtype = self.e_schema, targettype
790
        # initialize some variables according to `self` existance
791
792
793
794
795
796
797
798
799
800
        if rdef.role_cardinality(neg_role(role)) in '?1':
            # if cardinality in '1?', we want a target entity which isn't
            # already linked using this relation
            if searchedvar == 'S':
                restriction = ['NOT S %s ZZ' % rtype]
            else:
                restriction = ['NOT ZZ %s O' % rtype]
        elif self.has_eid():
            # elif we have an eid, we don't want a target entity which is
            # already linked to ourself through this relation
801
            restriction = ['NOT S %s O' % rtype]
802
803
804
805
        else:
            restriction = []
        if self.has_eid():
            restriction += ['%s eid %%(x)s' % evar]
806
807
            args = {'x': self.eid}
            if role == 'subject':
808
                sec_check_args = {'fromeid': self.eid}
809
            else:
810
811
                sec_check_args = {'toeid': self.eid}
            existant = None # instead of 'SO', improve perfs
Adrien Di Mascio's avatar
Adrien Di Mascio committed
812
        else:
813
            args = {}
814
815
816
817
818
819
820
821
            sec_check_args = {}
            existant = searchedvar
        # retreive entity class for targettype to compute base rql
        etypecls = self._cw.vreg['etypes'].etype_class(targettype)
        rql = etypecls.fetch_rql(self._cw.user, restriction,
                                 mainvar=searchedvar, ordermethod=ordermethod)
        select = self._cw.vreg.parse(self._cw, rql, args).children[0]
        # insert RQL expressions for schema constraints into the rql syntax tree
Adrien Di Mascio's avatar
Adrien Di Mascio committed
822
823
824
        if vocabconstraints:
            # RQLConstraint is a subclass for RQLVocabularyConstraint, so they
            # will be included as well
825
            cstrcls = RQLVocabularyConstraint
Adrien Di Mascio's avatar
Adrien Di Mascio committed
826
        else:
827
828
            cstrcls = RQLConstraint
        for cstr in rdef.constraints:
829
830
            # consider constraint.mainvars to check if constraint apply
            if isinstance(cstr, cstrcls) and searchedvar in cstr.mainvars:
831
832
                if not self.has_eid() and evar in cstr.mainvars:
                    continue
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
                # compute a varmap suitable to RQLRewriter.rewrite argument
                varmap = dict((v, v) for v in 'SO' if v in select.defined_vars
                              and v in cstr.mainvars)
                # rewrite constraint by constraint since we want a AND between
                # expressions.
                rewriter.rewrite(select, [(varmap, (cstr,))], select.solutions,
                                 args, existant)
        # insert security RQL expressions granting the permission to 'add' the
        # relation into the rql syntax tree, if necessary
        rqlexprs = rdef.get_rqlexprs('add')
        if rqlexprs and not rdef.has_perm(self._cw, 'add', **sec_check_args):
            # compute a varmap suitable to RQLRewriter.rewrite argument
            varmap = dict((v, v) for v in 'SO' if v in select.defined_vars)
            # rewrite all expressions at once since we want a OR between them.
            rewriter.rewrite(select, [(varmap, rqlexprs)], select.solutions,
                             args, existant)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
849
        # ensure we have an order defined
850
851
852
853
        if not select.orderby:
            select.add_sort_var(select.defined_vars[searchedvar])
        # we're done, turn the rql syntax tree as a string
        rql = select.as_string()
854
        return rql, args
855

Adrien Di Mascio's avatar
Adrien Di Mascio committed
856
    def unrelated(self, rtype, targettype, role='subject', limit=None,
857
                  ordermethod=None): # XXX .cw_unrelated
Adrien Di Mascio's avatar
Adrien Di Mascio committed
858
859
860
        """return a result set of target type objects that may be related
        by a given relation, with self as subject or object
        """
861
        try:
862
            rql, args = self.cw_unrelated_rql(rtype, targettype, role, ordermethod)
863
        except Unauthorized:
864
            return self._cw.empty_rset()
865
        # XXX should be set in unrelated rql when manipulating the AST
Adrien Di Mascio's avatar
Adrien Di Mascio committed
866
867
868
        if limit is not None:
            before, after = rql.split(' WHERE ', 1)
            rql = '%s LIMIT %s WHERE %s' % (before, limit, after)
869
        return self._cw.execute(rql, args)
870

871
    # relations cache handling #################################################
872

873
874
875
    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
876
        """
877
        return self._cw_related_cache.get('%s_%s' % (rtype, role))
878

879
    def _cw_relation_cache(self, rtype, role, entities=True, limit=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
880
881
882
        """return values for the given relation if it's cached on the instance,
        else raise `KeyError`
        """
883
        res = self._cw_related_cache['%s_%s' % (rtype, role)][entities]
Sylvain Thénault's avatar
oops    
Sylvain Thénault committed
884
        if limit is not None and limit < len(res):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
885
886
887
888
889
            if entities:
                res = res[:limit]
            else:
                res = res.limit(limit)
        return res
890

891
    def cw_set_relation_cache(self, rtype, role, rset):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
892
893
        """set cached values for the given relation"""
        if rset:
894
            related = list(rset.entities(0))
895
            rschema = self._cw.vreg.schema.rschema(rtype)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
896
            if role == 'subject':
Sylvain Thénault's avatar
Sylvain Thénault committed
897
                rcard = rschema.rdef(self.e_schema, related[0].e_schema).cardinality[1]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
898
899
                target = 'object'
            else:
Sylvain Thénault's avatar
Sylvain Thénault committed
900
                rcard = rschema.rdef(related[0].e_schema, self.e_schema).cardinality[0]
Adrien Di Mascio's avatar
Adrien Di Mascio committed
901
902
903
                target = 'subject'
            if rcard in '?1':
                for rentity in related:
904
                    rentity._cw_related_cache['%s_%s' % (rtype, target)] = (
905
                        self.as_rset(), (self,))
Adrien Di Mascio's avatar
Adrien Di Mascio committed
906
        else:
907
            related = ()
908
        self._cw_related_cache['%s_%s' % (rtype, role)] = (rset, related)
909

910
    def cw_clear_relation_cache(self, rtype=None, role=None):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
911
912
913
914
        """clear cached values for the given relation or the entire cache if
        no relation is given
        """
        if rtype is None:
915
            self._cw_related_cache = {}
916
            self._cw_adapters_cache = {}
Adrien Di Mascio's avatar
Adrien Di Mascio committed
917
918
        else:
            assert role
919
            self._cw_related_cache.pop('%s_%s' % (rtype, role), None)
920

921
    def clear_all_caches(self): # XXX cw_clear_all_caches
922
923
924
925
926
927
928
929
        """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
930
        self.cw_attr_cache.clear()
931
        # clear relations cache
932
        self.cw_clear_relation_cache()
933
934
935
936
937
        # rest path unique cache
        try:
            del self.__unique
        except AttributeError:
            pass
Sylvain Thénault's avatar
Sylvain Thénault committed
938

Adrien Di Mascio's avatar
Adrien Di Mascio committed
939
    # raw edition utilities ###################################################
940

941
    def set_attributes(self, **kwargs): # XXX cw_set_attributes
942
        _check_cw_unsafe(kwargs)
943
        assert kwargs
944
        assert self.cw_is_saved(), "should not call set_attributes while entity "\