cwvreg.py 15.6 KB
Newer Older
Adrien Di Mascio's avatar
Adrien Di Mascio committed
1
2
3
"""extend the generic VRegistry with some cubicweb specific stuff

:organization: Logilab
4
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
Adrien Di Mascio's avatar
Adrien Di Mascio committed
5
6
7
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""
__docformat__ = "restructuredtext en"
8
_ = unicode
Adrien Di Mascio's avatar
Adrien Di Mascio committed
9
10
11
12
13

from logilab.common.decorators import cached, clear_cache

from rql import RQLHelper

14
from cubicweb import Binary, UnknownProperty, UnknownEid
Adrien Di Mascio's avatar
Adrien Di Mascio committed
15
from cubicweb.vregistry import VRegistry, ObjectNotFound, NoSelectableObject
16
from cubicweb.rtags import RTAGS
Adrien Di Mascio's avatar
Adrien Di Mascio committed
17
18


sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
19
def use_interfaces(obj):
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
20
21
22
    """return interfaces used by the given object by searchinf for implements
    selectors, with a bw compat fallback to accepts_interfaces attribute
    """
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
23
24
25
    from cubicweb.selectors import implements
    try:
        # XXX deprecated
26
        return sorted(obj.accepts_interfaces)
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
27
28
29
30
31
32
33
    except AttributeError:
        try:
            impl = obj.__select__.search_selector(implements)
            if impl:
                return sorted(impl.expected_ifaces)
        except AttributeError:
            pass # old-style vobject classes with no accepts_interfaces
34
35
36
        except:
            print 'bad selector %s on %s' % (obj.__select__, obj)
            raise
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
37
38
        return ()

Adrien Di Mascio's avatar
Adrien Di Mascio committed
39
40
41

class CubicWebRegistry(VRegistry):
    """extend the generic VRegistry with some cubicweb specific stuff"""
42

43
44
45
46
    def __init__(self, config, debug=None, initlog=True):
        if initlog:
            # first init log service
            config.init_log(debug=debug)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
47
48
49
50
        super(CubicWebRegistry, self).__init__(config)
        self.schema = None
        self.reset()
        self.initialized = False
51

Adrien Di Mascio's avatar
Adrien Di Mascio committed
52
53
54
55
56
    def items(self):
        return [item for item in self._registries.items()
                if not item[0] in ('propertydefs', 'propertyvalues')]

    def values(self):
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
57
        return [value for key, value in self._registries.items()
Adrien Di Mascio's avatar
Adrien Di Mascio committed
58
                if not key in ('propertydefs', 'propertyvalues')]
59

Adrien Di Mascio's avatar
Adrien Di Mascio committed
60
61
62
    def reset(self):
        self._registries = {}
        self._lastmodifs = {}
63
64
65
66
        self._needs_iface = {}
        # two special registries, propertydefs which care all the property
        # definitions, and propertyvals which contains values for those
        # properties
Adrien Di Mascio's avatar
Adrien Di Mascio committed
67
68
69
70
        self._registries['propertydefs'] = {}
        self._registries['propertyvalues'] = self.eprop_values = {}
        for key, propdef in self.config.eproperty_definitions():
            self.register_property(key, **propdef)
71

Adrien Di Mascio's avatar
Adrien Di Mascio committed
72
73
74
75
76
77
    def set_schema(self, schema):
        """set application'schema and load application objects"""
        self.schema = schema
        clear_cache(self, 'rqlhelper')
        # now we can load application's web objects
        self.register_objects(self.config.vregistry_path())
78

Adrien Di Mascio's avatar
Adrien Di Mascio committed
79
80
81
82
83
84
85
86
87
88
89
    def update_schema(self, schema):
        """update .schema attribute on registered objects, necessary for some
        tests
        """
        self.schema = schema
        for registry, regcontent in self._registries.items():
            if registry in ('propertydefs', 'propertyvalues'):
                continue
            for objects in regcontent.values():
                for obj in objects:
                    obj.schema = schema
90
91
92
93
94

    def register_if_interface_found(self, obj, ifaces, **kwargs):
        """register an object but remove it if no entity class implements one of
        the given interfaces
        """
95
        self.register(obj, **kwargs)
96
        if not isinstance(ifaces,  (tuple, list)):
97
            self._needs_iface[obj] = (ifaces,)
98
        else:
99
            self._needs_iface[obj] = ifaces
100
101

    def register(self, obj, **kwargs):
102
        if kwargs.get('registryname', obj.__registry__) == 'etypes':
103
            if obj.id != 'Any' and not obj.id in self.schema:
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
104
105
                self.error('don\'t register %s, %s type not defined in the '
                           'schema', obj, obj.id)
106
                return
107
            kwargs['clear'] = True
108
109
        super(CubicWebRegistry, self).register(obj, **kwargs)
        # XXX bw compat
110
        ifaces = use_interfaces(obj)
111
        if ifaces:
112
            self._needs_iface[obj] = ifaces
113

Adrien Di Mascio's avatar
Adrien Di Mascio committed
114
    def register_objects(self, path, force_reload=None):
115
116
        """overriden to remove objects requiring a missing interface"""
        if super(CubicWebRegistry, self).register_objects(path, force_reload):
sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
117
            self.initialization_completed()
118
119
            # call vreg_initialization_completed on appobjects and print
            # registry content
120
121
122
123
124
125
            for registry, objects in self.items():
                self.debug('available in registry %s: %s', registry,
                           sorted(objects))
                for appobjects in objects.itervalues():
                    for appobject in appobjects:
                        appobject.vreg_initialization_completed()
126
            # don't check rtags if we don't want to cleanup_interface_sobjects
127
            for rtag in RTAGS:
128
129
                rtag.init(self.schema,
                          check=self.config.cleanup_interface_sobjects)
130

sylvain.thenault@logilab.fr's avatar
sylvain.thenault@logilab.fr committed
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
    def initialization_completed(self):
        # clear etype cache if you don't want to run into deep weirdness
        clear_cache(self, 'etype_class')
        # we may want to keep interface dependent objects (e.g.for i18n
        # catalog generation)
        if self.config.cleanup_interface_sobjects:
            # remove vobjects that don't support any available interface
            implemented_interfaces = set()
            if 'Any' in self.get('etypes', ()):
                for etype in self.schema.entities():
                    cls = self.etype_class(etype)
                    for iface in cls.__implements__:
                        implemented_interfaces.update(iface.__mro__)
                    implemented_interfaces.update(cls.__mro__)
            for obj, ifaces in self._needs_iface.items():
                ifaces = frozenset(isinstance(iface, basestring)
                                   and iface in self.schema
                                   and self.etype_class(iface)
                                   or iface
                                   for iface in ifaces)
                if not ('Any' in ifaces or ifaces & implemented_interfaces):
                    self.debug('kicking vobject %s (no implemented '
                               'interface among %s)', obj, ifaces)
                    self.unregister(obj)
        # clear needs_iface so we don't try to remove some not-anymore-in
        # objects on automatic reloading
        self._needs_iface.clear()

Adrien Di Mascio's avatar
Adrien Di Mascio committed
159
160
161
162
163
164
165
    @cached
    def etype_class(self, etype):
        """return an entity class for the given entity type.
        Try to find out a specific class for this kind of entity or
        default to a dump of the class registered for 'Any'
        """
        etype = str(etype)
166
167
        if etype == 'Any':
            return self.select(self.registry_objects('etypes', 'Any'), 'Any')
Adrien Di Mascio's avatar
Adrien Di Mascio committed
168
169
170
171
172
173
174
        eschema = self.schema.eschema(etype)
        baseschemas = [eschema] + eschema.ancestors()
        # browse ancestors from most specific to most generic and
        # try to find an associated custom entity class
        for baseschema in baseschemas:
            btype = str(baseschema)
            try:
175
176
                cls = self.select(self.registry_objects('etypes', btype), etype)
                break
Adrien Di Mascio's avatar
Adrien Di Mascio committed
177
178
            except ObjectNotFound:
                pass
179
180
181
182
183
        else:
            # no entity class for any of the ancestors, fallback to the default
            # one
            cls = self.select(self.registry_objects('etypes', 'Any'), etype)
        return cls
184

Adrien Di Mascio's avatar
Adrien Di Mascio committed
185
186
187
188
189
    def render(self, registry, oid, req, **context):
        """select an object in a given registry and render it

        - registry: the registry's name
        - oid : the view to call
190
        - req : the HTTP request
Adrien Di Mascio's avatar
Adrien Di Mascio committed
191
192
193
194
195
196
197
        """
        objclss = self.registry_objects(registry, oid)
        try:
            rset = context.pop('rset')
        except KeyError:
            rset = None
        selected = self.select(objclss, req, rset, **context)
198
        return selected.render(**context)
199

Adrien Di Mascio's avatar
Adrien Di Mascio committed
200
    def main_template(self, req, oid='main-template', **context):
Adrien Di Mascio's avatar
Adrien Di Mascio committed
201
202
203
204
        """display query by calling the given template (default to main),
        and returning the output as a string instead of requiring the [w]rite
        method as argument
        """
205
        res = self.render('views', oid, req, **context)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
206
207
208
209
210
211
212
213
214
215
216
217
        if isinstance(res, unicode):
            return res.encode(req.encoding)
        assert isinstance(res, str)
        return res

    def possible_vobjects(self, registry, *args, **kwargs):
        """return an ordered list of possible app objects in a given registry,
        supposing they support the 'visible' and 'order' properties (as most
        visualizable objects)
        """
        return [x for x in sorted(self.possible_objects(registry, *args, **kwargs),
                                  key=lambda x: x.propval('order'))
218
                if x.propval('visible')]
219

Adrien Di Mascio's avatar
Adrien Di Mascio committed
220
221
    def possible_actions(self, req, rset, **kwargs):
        if rset is None:
222
            actions = self.possible_vobjects('actions', req, rset, **kwargs)
Adrien Di Mascio's avatar
Adrien Di Mascio committed
223
        else:
224
            actions = rset.possible_actions(**kwargs) # cached implementation
Adrien Di Mascio's avatar
Adrien Di Mascio committed
225
226
227
228
        result = {}
        for action in actions:
            result.setdefault(action.category, []).append(action)
        return result
229

Adrien Di Mascio's avatar
Adrien Di Mascio committed
230
231
232
233
234
235
236
237
238
239
240
241
242
243
    def possible_views(self, req, rset, **kwargs):
        """return an iterator on possible views for this result set

        views returned are classes, not instances
        """
        for vid, views in self.registry('views').items():
            if vid[0] == '_':
                continue
            try:
                view = self.select(views, req, rset, **kwargs)
                if view.linkable():
                    yield view
            except NoSelectableObject:
                continue
244
245
246
            except Exception:
                self.exception('error while trying to list possible %s views for %s',
                               vid, rset)
247

Adrien Di Mascio's avatar
Adrien Di Mascio committed
248
249
250
251
252
253
254
255
256
257
258
259
260
    def select_box(self, oid, *args, **kwargs):
        """return the most specific view according to the result set"""
        try:
            return self.select_object('boxes', oid, *args, **kwargs)
        except NoSelectableObject:
            return

    def select_action(self, oid, *args, **kwargs):
        """return the most specific view according to the result set"""
        try:
            return self.select_object('actions', oid, *args, **kwargs)
        except NoSelectableObject:
            return
261

Adrien Di Mascio's avatar
Adrien Di Mascio committed
262
263
264
265
266
267
268
269
270
271
272
273
    def select_component(self, cid, *args, **kwargs):
        """return the most specific component according to the result set"""
        try:
            return self.select_object('components', cid, *args, **kwargs)
        except (NoSelectableObject, ObjectNotFound):
            return

    def select_view(self, __vid, req, rset, **kwargs):
        """return the most specific view according to the result set"""
        views = self.registry_objects('views', __vid)
        return self.select(views, req, rset, **kwargs)

274

Adrien Di Mascio's avatar
Adrien Di Mascio committed
275
276
277
278
279
280
281
282
283
284
285
286
287
    # properties handling #####################################################

    def user_property_keys(self, withsitewide=False):
        if withsitewide:
            return sorted(self['propertydefs'])
        return sorted(k for k, kd in self['propertydefs'].iteritems()
                      if not kd['sitewide'])

    def register_property(self, key, type, help, default=None, vocabulary=None,
                          sitewide=False):
        """register a given property"""
        properties = self._registries['propertydefs']
        assert type in YAMS_TO_PY
288
        properties[key] = {'type': type, 'vocabulary': vocabulary,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
                           'default': default, 'help': help,
                           'sitewide': sitewide}

    def property_info(self, key):
        """return dictionary containing description associated to the given
        property key (including type, defaut value, help and a site wide
        boolean)
        """
        try:
            return self._registries['propertydefs'][key]
        except KeyError:
            if key.startswith('system.version.'):
                soft = key.split('.')[-1]
                return {'type': 'String', 'sitewide': True,
                        'default': None, 'vocabulary': None,
                        'help': _('%s software version of the database') % soft}
            raise UnknownProperty('unregistered property %r' % key)
306

Adrien Di Mascio's avatar
Adrien Di Mascio committed
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
    def property_value(self, key):
        try:
            return self._registries['propertyvalues'][key]
        except KeyError:
            return self._registries['propertydefs'][key]['default']

    def typed_value(self, key, value):
        """value is an unicode string, return it correctly typed. Let potential
        type error propagates.
        """
        pdef = self.property_info(key)
        try:
            value = YAMS_TO_PY[pdef['type']](value)
        except (TypeError, ValueError):
            raise ValueError(_('bad value'))
        vocab = pdef['vocabulary']
        if vocab is not None:
            if callable(vocab):
                vocab = vocab(key, None) # XXX need a req object
            if not value in vocab:
                raise ValueError(_('unauthorized value'))
        return value
329

Adrien Di Mascio's avatar
Adrien Di Mascio committed
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
    def init_properties(self, propvalues):
        """init the property values registry using the given set of couple (key, value)
        """
        self.initialized = True
        values = self._registries['propertyvalues']
        for key, val in propvalues:
            try:
                values[key] = self.typed_value(key, val)
            except ValueError:
                self.warning('%s (you should probably delete that property '
                             'from the database)', ex)
            except UnknownProperty, ex:
                self.warning('%s (you should probably delete that property '
                             'from the database)', ex)

    def parse(self, session, rql, args=None):
        rqlst = self.rqlhelper.parse(rql)
        def type_from_eid(eid, session=session):
            return session.describe(eid)[0]
349
350
351
352
353
        try:
            self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
        except UnknownEid:
            for select in rqlst.children:
                select.solutions = []
Adrien Di Mascio's avatar
Adrien Di Mascio committed
354
355
356
357
358
359
360
361
        return rqlst

    @property
    @cached
    def rqlhelper(self):
        return RQLHelper(self.schema,
                         special_relations={'eid': 'uid', 'has_text': 'fti'})

362

Adrien Di Mascio's avatar
Adrien Di Mascio committed
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
class MulCnxCubicWebRegistry(CubicWebRegistry):
    """special registry to be used when an application has to deal with
    connections to differents repository. This class add some additional wrapper
    trying to hide buggy class attributes since classes are not designed to be
    shared.
    """
    def etype_class(self, etype):
        """return an entity class for the given entity type.
        Try to find out a specific class for this kind of entity or
        default to a dump of the class registered for 'Any'
        """
        usercls = super(MulCnxCubicWebRegistry, self).etype_class(etype)
        usercls.e_schema = self.schema.eschema(etype)
        return usercls

    def select(self, vobjects, *args, **kwargs):
        """return an instance of the most specific object according
        to parameters

        raise NoSelectableObject if not object apply
        """
        for vobject in vobjects:
            vobject.vreg = self
            vobject.schema = self.schema
            vobject.config = self.config
        return super(MulCnxCubicWebRegistry, self).select(vobjects, *args, **kwargs)
389

390
from datetime import datetime, date, time, timedelta
Adrien Di Mascio's avatar
Adrien Di Mascio committed
391
392
393
394
395
396
397
398

YAMS_TO_PY = {
    'Boolean':  bool,
    'String' :  unicode,
    'Password': str,
    'Bytes':    Binary,
    'Int':      int,
    'Float':    float,
399
400
401
402
    'Date':     date,
    'Datetime': datetime,
    'Time':     time,
    'Interval': timedelta,
Adrien Di Mascio's avatar
Adrien Di Mascio committed
403
404
    }