cubicweb/cwvreg.py
changeset 11057 0b59724cb3f2
parent 10907 9ae707db5265
child 11249 0ff4c02a1871
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """
       
    19 Cubicweb registries
       
    20 """
       
    21 
       
    22 __docformat__ = "restructuredtext en"
       
    23 from cubicweb import _
       
    24 
       
    25 import sys
       
    26 from os.path import join, dirname, realpath
       
    27 from warnings import warn
       
    28 from datetime import datetime, date, time, timedelta
       
    29 from functools import reduce
       
    30 
       
    31 from six import text_type, binary_type
       
    32 
       
    33 from logilab.common.decorators import cached, clear_cache
       
    34 from logilab.common.deprecation import deprecated, class_deprecated
       
    35 from logilab.common.modutils import cleanup_sys_modules
       
    36 from logilab.common.registry import (
       
    37     RegistryStore, Registry, obj_registries,
       
    38     ObjectNotFound, RegistryNotFound)
       
    39 
       
    40 from rql import RQLHelper
       
    41 from yams.constraints import BASE_CONVERTERS
       
    42 
       
    43 from cubicweb import (CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, CW_EVENT_MANAGER,
       
    44                       onevent, Binary, UnknownProperty, UnknownEid)
       
    45 from cubicweb.predicates import appobject_selectable, _reset_is_instance_cache
       
    46 
       
    47 
       
    48 @onevent('before-registry-reload')
       
    49 def cleanup_uicfg_compat():
       
    50     """ backward compat: those modules are now refering to app objects in
       
    51     cw.web.views.uicfg and import * from backward compat. On registry reload, we
       
    52     should pop those modules from the cache so references are properly updated on
       
    53     subsequent reload
       
    54     """
       
    55     if 'cubicweb.web' in sys.modules:
       
    56         if getattr(sys.modules['cubicweb.web'], 'uicfg', None):
       
    57             del sys.modules['cubicweb.web'].uicfg
       
    58         if getattr(sys.modules['cubicweb.web'], 'uihelper', None):
       
    59             del sys.modules['cubicweb.web'].uihelper
       
    60     sys.modules.pop('cubicweb.web.uicfg', None)
       
    61     sys.modules.pop('cubicweb.web.uihelper', None)
       
    62 
       
    63 
       
    64 def require_appobject(obj):
       
    65     """return appobjects required by the given object by searching for
       
    66     `appobject_selectable` predicate
       
    67     """
       
    68     impl = obj.__select__.search_selector(appobject_selectable)
       
    69     if impl:
       
    70         return (impl.registry, impl.regids)
       
    71     return None
       
    72 
       
    73 
       
    74 class CWRegistry(Registry):
       
    75     def __init__(self, vreg):
       
    76         """
       
    77         :param vreg: the :py:class:`CWRegistryStore` managing this registry.
       
    78         """
       
    79         super(CWRegistry, self).__init__(True)
       
    80         self.vreg = vreg
       
    81 
       
    82     @property
       
    83     def schema(self):
       
    84         """The :py:class:`cubicweb.schema.CubicWebSchema`
       
    85         """
       
    86         return self.vreg.schema
       
    87 
       
    88     def poss_visible_objects(self, *args, **kwargs):
       
    89         """return an ordered list of possible app objects in a given registry,
       
    90         supposing they support the 'visible' and 'order' properties (as most
       
    91         visualizable objects)
       
    92         """
       
    93         return sorted([x for x in self.possible_objects(*args, **kwargs)
       
    94                        if x.cw_propval('visible')],
       
    95                       key=lambda x: x.cw_propval('order'))
       
    96 
       
    97 
       
    98 def related_appobject(obj, appobjectattr='__appobject__'):
       
    99     """ adapts any object to a potential appobject bound to it
       
   100     through the __appobject__ attribute
       
   101     """
       
   102     return getattr(obj, appobjectattr, obj)
       
   103 
       
   104 
       
   105 class InstancesRegistry(CWRegistry):
       
   106 
       
   107     def selected(self, winner, args, kwargs):
       
   108         """overriden to avoid the default 'instanciation' behaviour, ie
       
   109         `winner(*args, **kwargs)`
       
   110         """
       
   111         return winner
       
   112 
       
   113 
       
   114 class ETypeRegistry(CWRegistry):
       
   115 
       
   116     def clear_caches(self):
       
   117         clear_cache(self, 'etype_class')
       
   118         clear_cache(self, 'parent_classes')
       
   119         _reset_is_instance_cache(self.vreg)
       
   120 
       
   121     def initialization_completed(self):
       
   122         """on registration completed, clear etype_class internal cache
       
   123         """
       
   124         super(ETypeRegistry, self).initialization_completed()
       
   125         # clear etype cache if you don't want to run into deep weirdness
       
   126         self.clear_caches()
       
   127         # rebuild all classes to avoid potential memory fragmentation
       
   128         # (see #2719113)
       
   129         for eschema in self.vreg.schema.entities():
       
   130             self.etype_class(eschema)
       
   131 
       
   132     def register(self, obj, **kwargs):
       
   133         obj = related_appobject(obj)
       
   134         oid = kwargs.get('oid') or obj.__regid__
       
   135         if oid != 'Any' and not oid in self.schema:
       
   136             self.error('don\'t register %s, %s type not defined in the '
       
   137                        'schema', obj, oid)
       
   138             return
       
   139         kwargs['clear'] = True
       
   140         super(ETypeRegistry, self).register(obj, **kwargs)
       
   141 
       
   142     def iter_classes(self):
       
   143         for etype in self.vreg.schema.entities():
       
   144             yield self.etype_class(etype)
       
   145 
       
   146     @cached
       
   147     def parent_classes(self, etype):
       
   148         if etype == 'Any':
       
   149             return (), self.etype_class('Any')
       
   150         parents = tuple(self.etype_class(e.type)
       
   151                         for e in self.schema.eschema(etype).ancestors())
       
   152         return parents, self.etype_class('Any')
       
   153 
       
   154     @cached
       
   155     def etype_class(self, etype):
       
   156         """return an entity class for the given entity type.
       
   157 
       
   158         Try to find out a specific class for this kind of entity or default to a
       
   159         dump of the nearest parent class (in yams inheritance) registered.
       
   160 
       
   161         Fall back to 'Any' if not yams parent class found.
       
   162         """
       
   163         etype = str(etype)
       
   164         if etype == 'Any':
       
   165             objects = self['Any']
       
   166             assert len(objects) == 1, objects
       
   167             return objects[0]
       
   168         eschema = self.schema.eschema(etype)
       
   169         baseschemas = [eschema] + eschema.ancestors()
       
   170         # browse ancestors from most specific to most generic and try to find an
       
   171         # associated custom entity class
       
   172         for baseschema in baseschemas:
       
   173             try:
       
   174                 btype = ETYPE_NAME_MAP[baseschema]
       
   175             except KeyError:
       
   176                 btype = str(baseschema)
       
   177             try:
       
   178                 objects = self[btype]
       
   179                 assert len(objects) == 1, objects
       
   180                 if btype == etype:
       
   181                     cls = objects[0]
       
   182                 else:
       
   183                     # recurse to ensure issubclass(etype_class('Child'),
       
   184                     #                              etype_class('Parent'))
       
   185                     cls = self.etype_class(btype)
       
   186                 break
       
   187             except ObjectNotFound:
       
   188                 pass
       
   189         else:
       
   190             # no entity class for any of the ancestors, fallback to the default
       
   191             # one
       
   192             objects = self['Any']
       
   193             assert len(objects) == 1, objects
       
   194             cls = objects[0]
       
   195         # make a copy event if cls.__regid__ == etype, else we may have pb for
       
   196         # client application using multiple connections to different
       
   197         # repositories (eg shingouz)
       
   198         # __autogenerated__ attribute is just a marker
       
   199         cls = type(str(etype), (cls,), {'__autogenerated__': True,
       
   200                                         '__doc__': cls.__doc__,
       
   201                                         '__module__': cls.__module__})
       
   202         cls.__regid__ = etype
       
   203         cls.__initialize__(self.schema)
       
   204         return cls
       
   205 
       
   206     def fetch_attrs(self, targettypes):
       
   207         """return intersection of fetch_attrs of each entity type in
       
   208         `targettypes`
       
   209         """
       
   210         fetchattrs_list = []
       
   211         for ttype in targettypes:
       
   212             etypecls = self.etype_class(ttype)
       
   213             fetchattrs_list.append(set(etypecls.fetch_attrs))
       
   214         return reduce(set.intersection, fetchattrs_list)
       
   215 
       
   216 
       
   217 class ViewsRegistry(CWRegistry):
       
   218 
       
   219     def main_template(self, req, oid='main-template', rset=None, **kwargs):
       
   220         """display query by calling the given template (default to main),
       
   221         and returning the output as a string instead of requiring the [w]rite
       
   222         method as argument
       
   223         """
       
   224         obj = self.select(oid, req, rset=rset, **kwargs)
       
   225         res = obj.render(**kwargs)
       
   226         if isinstance(res, text_type):
       
   227             return res.encode(req.encoding)
       
   228         assert isinstance(res, binary_type)
       
   229         return res
       
   230 
       
   231     def possible_views(self, req, rset=None, **kwargs):
       
   232         """return an iterator on possible views for this result set
       
   233 
       
   234         views returned are classes, not instances
       
   235         """
       
   236         for vid, views in self.items():
       
   237             if vid[0] == '_':
       
   238                 continue
       
   239             views = [view for view in views
       
   240                      if not isinstance(view, class_deprecated)]
       
   241             try:
       
   242                 view = self._select_best(views, req, rset=rset, **kwargs)
       
   243                 if view is not None and view.linkable():
       
   244                     yield view
       
   245             except Exception:
       
   246                 self.exception('error while trying to select %s view for %s',
       
   247                                vid, rset)
       
   248 
       
   249 
       
   250 class ActionsRegistry(CWRegistry):
       
   251     def poss_visible_objects(self, *args, **kwargs):
       
   252         """return an ordered list of possible actions"""
       
   253         return sorted(self.possible_objects(*args, **kwargs),
       
   254                       key=lambda x: x.order)
       
   255 
       
   256     def possible_actions(self, req, rset=None, **kwargs):
       
   257         if rset is None:
       
   258             actions = self.poss_visible_objects(req, rset=rset, **kwargs)
       
   259         else:
       
   260             actions = rset.possible_actions(**kwargs) # cached implementation
       
   261         result = {}
       
   262         for action in actions:
       
   263             result.setdefault(action.category, []).append(action)
       
   264         return result
       
   265 
       
   266 
       
   267 class CtxComponentsRegistry(CWRegistry):
       
   268     def poss_visible_objects(self, *args, **kwargs):
       
   269         """return an ordered list of possible components"""
       
   270         context = kwargs.pop('context')
       
   271         if '__cache' in kwargs:
       
   272             cache = kwargs.pop('__cache')
       
   273         elif kwargs.get('rset') is None:
       
   274             cache = args[0]
       
   275         else:
       
   276             cache = kwargs['rset']
       
   277         try:
       
   278             cached = cache.__components_cache
       
   279         except AttributeError:
       
   280             ctxcomps = super(CtxComponentsRegistry, self).poss_visible_objects(
       
   281                 *args, **kwargs)
       
   282             if cache is None:
       
   283                 components = []
       
   284                 for component in ctxcomps:
       
   285                     cctx = component.cw_propval('context')
       
   286                     if cctx == context:
       
   287                         component.cw_extra_kwargs['context'] = cctx
       
   288                         components.append(component)
       
   289                 return components
       
   290             cached = cache.__components_cache = {}
       
   291             for component in ctxcomps:
       
   292                 cctx = component.cw_propval('context')
       
   293                 component.cw_extra_kwargs['context'] = cctx
       
   294                 cached.setdefault(cctx, []).append(component)
       
   295         thisctxcomps = cached.get(context, ())
       
   296         # XXX set context for bw compat (should now be taken by comp.render())
       
   297         for component in thisctxcomps:
       
   298             component.cw_extra_kwargs['context'] = context
       
   299         return thisctxcomps
       
   300 
       
   301 
       
   302 class BwCompatCWRegistry(object):
       
   303     def __init__(self, vreg, oldreg, redirecttoreg):
       
   304         self.vreg = vreg
       
   305         self.oldreg = oldreg
       
   306         self.redirecto = redirecttoreg
       
   307 
       
   308     def __getattr__(self, attr):
       
   309         warn('[3.10] you should now use the %s registry instead of the %s registry'
       
   310              % (self.redirecto, self.oldreg), DeprecationWarning, stacklevel=2)
       
   311         return getattr(self.vreg[self.redirecto], attr)
       
   312 
       
   313     def clear(self): pass
       
   314     def initialization_completed(self): pass
       
   315 
       
   316 
       
   317 class CWRegistryStore(RegistryStore):
       
   318     """Central registry for the cubicweb instance, extending the generic
       
   319     RegistryStore with some cubicweb specific stuff.
       
   320 
       
   321     This is one of the central object in cubicweb instance, coupling
       
   322     dynamically loaded objects with the schema and the configuration objects.
       
   323 
       
   324     It specializes the RegistryStore by adding some convenience methods to access to
       
   325     stored objects. Currently we have the following registries of objects known
       
   326     by the web instance (library may use some others additional registries):
       
   327 
       
   328     * 'etypes', entity type classes
       
   329 
       
   330     * 'views', views and templates (e.g. layout views)
       
   331 
       
   332     * 'components', non contextual components, like magic search, url evaluators
       
   333 
       
   334     * 'ctxcomponents', contextual components like boxes and dynamic section
       
   335 
       
   336     * 'actions', contextual actions, eg links to display in predefined places in
       
   337       the ui
       
   338 
       
   339     * 'forms', describing logic of HTML form
       
   340 
       
   341     * 'formrenderers', rendering forms to html
       
   342 
       
   343     * 'controllers', primary objects to handle request publishing, directly
       
   344       plugged into the application
       
   345     """
       
   346 
       
   347     REGISTRY_FACTORY = {None: CWRegistry,
       
   348                         'etypes': ETypeRegistry,
       
   349                         'views': ViewsRegistry,
       
   350                         'actions': ActionsRegistry,
       
   351                         'ctxcomponents': CtxComponentsRegistry,
       
   352                         'uicfg': InstancesRegistry,
       
   353                         }
       
   354 
       
   355     def __init__(self, config, initlog=True):
       
   356         if initlog:
       
   357             # first init log service
       
   358             config.init_log()
       
   359         super(CWRegistryStore, self).__init__(config.debugmode)
       
   360         self.config = config
       
   361         # need to clean sys.path this to avoid import confusion pb (i.e.  having
       
   362         # the same module loaded as 'cubicweb.web.views' subpackage and as
       
   363         # views' or 'web.views' subpackage. This is mainly for testing purpose,
       
   364         # we should'nt need this in production environment
       
   365         for webdir in (join(dirname(realpath(__file__)), 'web'),
       
   366                        join(dirname(__file__), 'web')):
       
   367             if webdir in sys.path:
       
   368                 sys.path.remove(webdir)
       
   369         if CW_SOFTWARE_ROOT in sys.path:
       
   370             sys.path.remove(CW_SOFTWARE_ROOT)
       
   371         self.schema = None
       
   372         self.initialized = False
       
   373         self['boxes'] = BwCompatCWRegistry(self, 'boxes', 'ctxcomponents')
       
   374         self['contentnavigation'] = BwCompatCWRegistry(self, 'contentnavigation', 'ctxcomponents')
       
   375 
       
   376     def setdefault(self, regid):
       
   377         try:
       
   378             return self[regid]
       
   379         except RegistryNotFound:
       
   380             self[regid] = self.registry_class(regid)(self)
       
   381             return self[regid]
       
   382 
       
   383     def items(self):
       
   384         return [item for item in super(CWRegistryStore, self).items()
       
   385                 if not item[0] in ('propertydefs', 'propertyvalues')]
       
   386     def iteritems(self):
       
   387         return (item for item in super(CWRegistryStore, self).items()
       
   388                 if not item[0] in ('propertydefs', 'propertyvalues'))
       
   389 
       
   390     def values(self):
       
   391         return [value for key, value in self.items()]
       
   392     def itervalues(self):
       
   393         return (value for key, value in self.items())
       
   394 
       
   395     def reset(self):
       
   396         CW_EVENT_MANAGER.emit('before-registry-reset', self)
       
   397         super(CWRegistryStore, self).reset()
       
   398         self._needs_appobject = {}
       
   399         # two special registries, propertydefs which care all the property
       
   400         # definitions, and propertyvals which contains values for those
       
   401         # properties
       
   402         if not self.initialized:
       
   403             self['propertydefs'] = {}
       
   404             self['propertyvalues'] = self.eprop_values = {}
       
   405             for key, propdef in self.config.cwproperty_definitions():
       
   406                 self.register_property(key, **propdef)
       
   407         CW_EVENT_MANAGER.emit('after-registry-reset', self)
       
   408 
       
   409     def register_all(self, objects, modname, butclasses=()):
       
   410         butclasses = set(related_appobject(obj)
       
   411                          for obj in butclasses)
       
   412         objects = [related_appobject(obj) for obj in objects]
       
   413         super(CWRegistryStore, self).register_all(objects, modname, butclasses)
       
   414 
       
   415     def register_and_replace(self, obj, replaced):
       
   416         obj = related_appobject(obj)
       
   417         replaced = related_appobject(replaced)
       
   418         super(CWRegistryStore, self).register_and_replace(obj, replaced)
       
   419 
       
   420     def set_schema(self, schema):
       
   421         """set instance'schema and load application objects"""
       
   422         self._set_schema(schema)
       
   423         # now we can load application's web objects
       
   424         self.reload(self.config.appobjects_path(), force_reload=False)
       
   425         # map lowered entity type names to their actual name
       
   426         self.case_insensitive_etypes = {}
       
   427         for eschema in self.schema.entities():
       
   428             etype = str(eschema)
       
   429             self.case_insensitive_etypes[etype.lower()] = etype
       
   430             clear_cache(eschema, 'ordered_relations')
       
   431             clear_cache(eschema, 'meta_attributes')
       
   432 
       
   433     def reload_if_needed(self):
       
   434         path = self.config.appobjects_path()
       
   435         if self.is_reload_needed(path):
       
   436             self.reload(path)
       
   437 
       
   438     def _cleanup_sys_modules(self, path):
       
   439         """Remove submodules of `directories` from `sys.modules` and cleanup
       
   440         CW_EVENT_MANAGER accordingly.
       
   441 
       
   442         We take care to properly remove obsolete registry callbacks.
       
   443 
       
   444         """
       
   445         caches = {}
       
   446         callbackdata = CW_EVENT_MANAGER.callbacks.values()
       
   447         for callbacklist in callbackdata:
       
   448             for callback in callbacklist:
       
   449                 func = callback[0]
       
   450                 # for non-function callable, we do nothing interesting
       
   451                 module = getattr(func, '__module__', None)
       
   452                 caches[id(callback)] = module
       
   453         deleted_modules = set(cleanup_sys_modules(path))
       
   454         for callbacklist in callbackdata:
       
   455             for callback in callbacklist[:]:
       
   456                 module = caches[id(callback)]
       
   457                 if module and module in deleted_modules:
       
   458                     callbacklist.remove(callback)
       
   459 
       
   460     def reload(self, path, force_reload=True):
       
   461         """modification detected, reset and reload the vreg"""
       
   462         CW_EVENT_MANAGER.emit('before-registry-reload')
       
   463         if force_reload:
       
   464             self._cleanup_sys_modules(path)
       
   465             cubes = self.config.cubes()
       
   466             # if the fs code use some cubes not yet registered into the instance
       
   467             # we should cleanup sys.modules for those as well to avoid potential
       
   468             # bad class reference pb after reloading
       
   469             cfg = self.config
       
   470             for cube in cfg.expand_cubes(cubes, with_recommends=True):
       
   471                 if not cube in cubes:
       
   472                     cpath = cfg.build_appobjects_cube_path([cfg.cube_dir(cube)])
       
   473                     self._cleanup_sys_modules(cpath)
       
   474         self.register_objects(path)
       
   475         CW_EVENT_MANAGER.emit('after-registry-reload')
       
   476 
       
   477     def load_file(self, filepath, modname):
       
   478         # override to allow some instrumentation (eg localperms)
       
   479         modpath = modname.split('.')
       
   480         try:
       
   481             self.currently_loading_cube = modpath[modpath.index('cubes') + 1]
       
   482         except ValueError:
       
   483             self.currently_loading_cube = 'cubicweb'
       
   484         return super(CWRegistryStore, self).load_file(filepath, modname)
       
   485 
       
   486     def _set_schema(self, schema):
       
   487         """set instance'schema"""
       
   488         self.schema = schema
       
   489         clear_cache(self, 'rqlhelper')
       
   490 
       
   491     def update_schema(self, schema):
       
   492         """update .schema attribute on registered objects, necessary for some
       
   493         tests
       
   494         """
       
   495         self.schema = schema
       
   496         for registry, regcontent in self.items():
       
   497             for objects in regcontent.values():
       
   498                 for obj in objects:
       
   499                     obj.schema = schema
       
   500 
       
   501     def register(self, obj, *args, **kwargs):
       
   502         """register `obj` application object into `registryname` or
       
   503         `obj.__registry__` if not specified, with identifier `oid` or
       
   504         `obj.__regid__` if not specified.
       
   505 
       
   506         If `clear` is true, all objects with the same identifier will be
       
   507         previously unregistered.
       
   508         """
       
   509         obj = related_appobject(obj)
       
   510         super(CWRegistryStore, self).register(obj, *args, **kwargs)
       
   511         depends_on = require_appobject(obj)
       
   512         if depends_on is not None:
       
   513             self._needs_appobject[obj] = depends_on
       
   514 
       
   515     def register_objects(self, path):
       
   516         """overriden to give cubicweb's extrapath (eg cubes package's __path__)
       
   517         """
       
   518         super(CWRegistryStore, self).register_objects(
       
   519             path, self.config.extrapath)
       
   520 
       
   521     def initialization_completed(self):
       
   522         """cw specific code once vreg initialization is completed:
       
   523 
       
   524         * remove objects requiring a missing appobject, unless
       
   525           config.cleanup_unused_appobjects is false
       
   526         * init rtags
       
   527         """
       
   528         # we may want to keep interface dependent objects (e.g.for i18n
       
   529         # catalog generation)
       
   530         if self.config.cleanup_unused_appobjects:
       
   531             # remove appobjects which depend on other, unexistant appobjects
       
   532             for obj, (regname, regids) in self._needs_appobject.items():
       
   533                 try:
       
   534                     registry = self[regname]
       
   535                 except RegistryNotFound:
       
   536                     self.debug('unregister %s (no registry %s)', obj, regname)
       
   537                     self.unregister(obj)
       
   538                     continue
       
   539                 for regid in regids:
       
   540                     if registry.get(regid):
       
   541                         break
       
   542                 else:
       
   543                     self.debug('unregister %s (no %s object in registry %s)',
       
   544                                registry.objid(obj), ' or '.join(regids), regname)
       
   545                     self.unregister(obj)
       
   546         super(CWRegistryStore, self).initialization_completed()
       
   547         if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode
       
   548             for rtags in self['uicfg'].values():
       
   549                 for rtag in rtags:
       
   550                     # don't check rtags if we don't want to cleanup_unused_appobjects
       
   551                     rtag.init(self.schema, check=self.config.cleanup_unused_appobjects)
       
   552 
       
   553     # rql parsing utilities ####################################################
       
   554 
       
   555     @property
       
   556     @cached
       
   557     def rqlhelper(self):
       
   558         return RQLHelper(self.schema,
       
   559                          special_relations={'eid': 'uid', 'has_text': 'fti'})
       
   560 
       
   561     def solutions(self, req, rqlst, args):
       
   562         def type_from_eid(eid, req=req):
       
   563             return req.entity_metas(eid)['type']
       
   564         return self.rqlhelper.compute_solutions(rqlst, {'eid': type_from_eid}, args)
       
   565 
       
   566     def parse(self, req, rql, args=None):
       
   567         rqlst = self.rqlhelper.parse(rql)
       
   568         try:
       
   569             self.solutions(req, rqlst, args)
       
   570         except UnknownEid:
       
   571             for select in rqlst.children:
       
   572                 select.solutions = []
       
   573         return rqlst
       
   574 
       
   575     # properties handling #####################################################
       
   576 
       
   577     def user_property_keys(self, withsitewide=False):
       
   578         if withsitewide:
       
   579             return sorted(k for k in self['propertydefs']
       
   580                           if not k.startswith('sources.'))
       
   581         return sorted(k for k, kd in self['propertydefs'].items()
       
   582                       if not kd['sitewide'] and not k.startswith('sources.'))
       
   583 
       
   584     def register_property(self, key, type, help, default=None, vocabulary=None,
       
   585                           sitewide=False):
       
   586         """register a given property"""
       
   587         properties = self['propertydefs']
       
   588         assert type in YAMS_TO_PY, 'unknown type %s' % type
       
   589         properties[key] = {'type': type, 'vocabulary': vocabulary,
       
   590                            'default': default, 'help': help,
       
   591                            'sitewide': sitewide}
       
   592 
       
   593     def property_info(self, key):
       
   594         """return dictionary containing description associated to the given
       
   595         property key (including type, defaut value, help and a site wide
       
   596         boolean)
       
   597         """
       
   598         try:
       
   599             return self['propertydefs'][key]
       
   600         except KeyError:
       
   601             if key.startswith('system.version.'):
       
   602                 soft = key.split('.')[-1]
       
   603                 return {'type': 'String', 'sitewide': True,
       
   604                         'default': None, 'vocabulary': None,
       
   605                         'help': _('%s software version of the database') % soft}
       
   606             raise UnknownProperty('unregistered property %r' % key)
       
   607 
       
   608     def property_value(self, key):
       
   609         try:
       
   610             return self['propertyvalues'][key]
       
   611         except KeyError:
       
   612             return self.property_info(key)['default']
       
   613 
       
   614     def typed_value(self, key, value):
       
   615         """value is a unicode string, return it correctly typed. Let potential
       
   616         type error propagates.
       
   617         """
       
   618         pdef = self.property_info(key)
       
   619         try:
       
   620             value = YAMS_TO_PY[pdef['type']](value)
       
   621         except (TypeError, ValueError):
       
   622             raise ValueError(_('bad value'))
       
   623         vocab = pdef['vocabulary']
       
   624         if vocab is not None:
       
   625             if callable(vocab):
       
   626                 vocab = vocab(None) # XXX need a req object
       
   627             if not value in vocab:
       
   628                 raise ValueError(_('unauthorized value'))
       
   629         return value
       
   630 
       
   631     def init_properties(self, propvalues):
       
   632         """init the property values registry using the given set of couple (key, value)
       
   633         """
       
   634         self.initialized = True
       
   635         values = self['propertyvalues']
       
   636         for key, val in propvalues:
       
   637             try:
       
   638                 values[key] = self.typed_value(key, val)
       
   639             except ValueError as ex:
       
   640                 self.warning('%s (you should probably delete that property '
       
   641                              'from the database)', ex)
       
   642             except UnknownProperty as ex:
       
   643                 self.warning('%s (you should probably delete that property '
       
   644                              'from the database)', ex)
       
   645 
       
   646 
       
   647 # XXX unify with yams.constraints.BASE_CONVERTERS?
       
   648 YAMS_TO_PY = BASE_CONVERTERS.copy()
       
   649 YAMS_TO_PY.update({
       
   650     'Bytes':      Binary,
       
   651     'Date':       date,
       
   652     'Datetime':   datetime,
       
   653     'TZDatetime': datetime,
       
   654     'Time':       time,
       
   655     'TZTime':     time,
       
   656     'Interval':   timedelta,
       
   657     })