selectors.py
branchstable
changeset 5994 97c55baefa0c
parent 5944 b962dff47c36
child 6008 e1c43115af3b
equal deleted inserted replaced
5976:00b1b6b906cf 5994:97c55baefa0c
   167 
   167 
   168 Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
   168 Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
   169 or below the :func:`objectify_selector` decorator of your selector function so it gets
   169 or below the :func:`objectify_selector` decorator of your selector function so it gets
   170 traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
   170 traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
   171 
   171 
   172 .. autofunction:: cubicweb.selectors.lltrace
   172 .. autofunction:: cubicweb.appobject.lltrace
   173 
   173 
   174 .. note::
   174 .. note::
   175   Selectors __call__ should *always* return a positive integer, and shall never
   175   Selectors __call__ should *always* return a positive integer, and shall never
   176   return `None`.
   176   return `None`.
   177 
   177 
   181 Debugging selection
   181 Debugging selection
   182 ~~~~~~~~~~~~~~~~~~~
   182 ~~~~~~~~~~~~~~~~~~~
   183 
   183 
   184 Once in a while, one needs to understand why a view (or any application object)
   184 Once in a while, one needs to understand why a view (or any application object)
   185 is, or is not selected appropriately. Looking at which selectors fired (or did
   185 is, or is not selected appropriately. Looking at which selectors fired (or did
   186 not) is the way. The :class:`cubicweb.selectors.traced_selection` context
   186 not) is the way. The :class:`cubicweb.appobject.traced_selection` context
   187 manager to help with that, *if you're running your instance in debug mode*.
   187 manager to help with that, *if you're running your instance in debug mode*.
   188 
   188 
   189 .. autoclass:: cubicweb.selectors.traced_selection
   189 .. autoclass:: cubicweb.appobject.traced_selection
   190 
   190 
   191 
   191 
   192 .. |cubicweb| replace:: *CubicWeb*
   192 .. |cubicweb| replace:: *CubicWeb*
   193 """
   193 """
   194 
   194 
   200 from logilab.common.deprecation import class_renamed
   200 from logilab.common.deprecation import class_renamed
   201 from logilab.common.compat import all, any
   201 from logilab.common.compat import all, any
   202 from logilab.common.interface import implements as implements_iface
   202 from logilab.common.interface import implements as implements_iface
   203 
   203 
   204 from yams import BASE_TYPES
   204 from yams import BASE_TYPES
   205 
   205 from rql.nodes import Function
   206 from cubicweb import Unauthorized, NoSelectableObject, NotAnEntity, role
   206 
       
   207 from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
       
   208                       CW_EVENT_MANAGER, role)
   207 # even if not used, let yes here so it's importable through this module
   209 # even if not used, let yes here so it's importable through this module
   208 from cubicweb.appobject import Selector, objectify_selector, yes
   210 from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
   209 from cubicweb.vregistry import class_regid
       
   210 from cubicweb.cwconfig import CubicWebConfiguration
       
   211 from cubicweb.schema import split_expression
   211 from cubicweb.schema import split_expression
   212 
   212 
   213 # helpers for debugging selectors
   213 from cubicweb.appobject import traced_selection # XXX for bw compat
   214 SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
       
   215 TRACED_OIDS = None
       
   216 
       
   217 def _trace_selector(cls, selector, args, ret):
       
   218     # /!\ lltrace decorates pure function or __call__ method, this
       
   219     #     means argument order may be different
       
   220     if isinstance(cls, Selector):
       
   221         selname = str(cls)
       
   222         vobj = args[0]
       
   223     else:
       
   224         selname = selector.__name__
       
   225         vobj = cls
       
   226     if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
       
   227         #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
       
   228         print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
       
   229 
       
   230 def lltrace(selector):
       
   231     """use this decorator on your selectors so the becomes traceable with
       
   232     :class:`traced_selection`
       
   233     """
       
   234     # don't wrap selectors if not in development mode
       
   235     if CubicWebConfiguration.mode == 'system': # XXX config.debug
       
   236         return selector
       
   237     def traced(cls, *args, **kwargs):
       
   238         ret = selector(cls, *args, **kwargs)
       
   239         if TRACED_OIDS is not None:
       
   240             _trace_selector(cls, selector, args, ret)
       
   241         return ret
       
   242     traced.__name__ = selector.__name__
       
   243     traced.__doc__ = selector.__doc__
       
   244     return traced
       
   245 
       
   246 class traced_selection(object):
       
   247     """
       
   248     Typical usage is :
       
   249 
       
   250     .. sourcecode:: python
       
   251 
       
   252         >>> from cubicweb.selectors import traced_selection
       
   253         >>> with traced_selection():
       
   254         ...     # some code in which you want to debug selectors
       
   255         ...     # for all objects
       
   256 
       
   257     Don't forget the 'from __future__ import with_statement' at the module top-level
       
   258     if you're using python prior to 2.6.
       
   259 
       
   260     This will yield lines like this in the logs::
       
   261 
       
   262         selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
       
   263 
       
   264     You can also give to :class:`traced_selection` the identifiers of objects on
       
   265     which you want to debug selection ('oid1' and 'oid2' in the example above).
       
   266 
       
   267     .. sourcecode:: python
       
   268 
       
   269         >>> with traced_selection( ('regid1', 'regid2') ):
       
   270         ...     # some code in which you want to debug selectors
       
   271         ...     # for objects with __regid__ 'regid1' and 'regid2'
       
   272 
       
   273     A potentially usefull point to set up such a tracing function is
       
   274     the `cubicweb.vregistry.Registry.select` method body.
       
   275     """
       
   276 
       
   277     def __init__(self, traced='all'):
       
   278         self.traced = traced
       
   279 
       
   280     def __enter__(self):
       
   281         global TRACED_OIDS
       
   282         TRACED_OIDS = self.traced
       
   283 
       
   284     def __exit__(self, exctype, exc, traceback):
       
   285         global TRACED_OIDS
       
   286         TRACED_OIDS = None
       
   287         return traceback is None
       
   288 
       
   289 
   214 
   290 def score_interface(etypesreg, cls_or_inst, cls, iface):
   215 def score_interface(etypesreg, cls_or_inst, cls, iface):
   291     """Return XXX if the give object (maybe an instance or class) implements
   216     """Return XXX if the give object (maybe an instance or class) implements
   292     the interface.
   217     the interface.
   293     """
   218     """
   300             return 1
   225             return 1
   301         for index, basecls in enumerate(reversed(parents[:-1])):
   226         for index, basecls in enumerate(reversed(parents[:-1])):
   302             if iface is basecls:
   227             if iface is basecls:
   303                 return index + 3
   228                 return index + 3
   304         return 0
   229         return 0
       
   230     # XXX iface in implements deprecated in 3.9
   305     if implements_iface(cls_or_inst, iface):
   231     if implements_iface(cls_or_inst, iface):
   306         # implenting an interface takes precedence other special Any interface
   232         # implenting an interface takes precedence other special Any interface
   307         return 2
   233         return 2
   308     return 0
   234     return 0
   309 
   235 
   319     def __call__(self, cls, *args, **kwargs):
   245     def __call__(self, cls, *args, **kwargs):
   320         self.complete(cls)
   246         self.complete(cls)
   321         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
   247         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
   322 
   248 
   323 
   249 
   324 class ImplementsMixIn(object):
       
   325     """mix-in class for selectors checking implemented interfaces of something
       
   326     """
       
   327     def __init__(self, *expected_ifaces, **kwargs):
       
   328         super(ImplementsMixIn, self).__init__(**kwargs)
       
   329         self.expected_ifaces = expected_ifaces
       
   330 
       
   331     def __str__(self):
       
   332         return '%s(%s)' % (self.__class__.__name__,
       
   333                            ','.join(str(s) for s in self.expected_ifaces))
       
   334 
       
   335     def score_interfaces(self, req, cls_or_inst, cls):
       
   336         score = 0
       
   337         etypesreg = req.vreg['etypes']
       
   338         for iface in self.expected_ifaces:
       
   339             if isinstance(iface, basestring):
       
   340                 # entity type
       
   341                 try:
       
   342                     iface = etypesreg.etype_class(iface)
       
   343                 except KeyError:
       
   344                     continue # entity type not in the schema
       
   345             score += score_interface(etypesreg, cls_or_inst, cls, iface)
       
   346         return score
       
   347 
       
   348 
       
   349 class EClassSelector(Selector):
   250 class EClassSelector(Selector):
   350     """abstract class for selectors working on *entity class(es)* specified
   251     """abstract class for selectors working on *entity class(es)* specified
   351     explicitly or found of the result set.
   252     explicitly or found of the result set.
   352 
   253 
   353     Here are entity lookup / scoring rules:
   254     Here are entity lookup / scoring rules:
   373     def __init__(self, once_is_enough=False, accept_none=True):
   274     def __init__(self, once_is_enough=False, accept_none=True):
   374         self.once_is_enough = once_is_enough
   275         self.once_is_enough = once_is_enough
   375         self.accept_none = accept_none
   276         self.accept_none = accept_none
   376 
   277 
   377     @lltrace
   278     @lltrace
   378     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
   279     def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
       
   280                  **kwargs):
   379         if kwargs.get('entity'):
   281         if kwargs.get('entity'):
   380             return self.score_class(kwargs['entity'].__class__, req)
   282             return self.score_class(kwargs['entity'].__class__, req)
   381         if not rset:
   283         if not rset:
   382             return 0
   284             return 0
   383         score = 0
   285         score = 0
   384         if row is None:
   286         if row is None:
   385             if not self.accept_none:
   287             if accept_none is None:
       
   288                 accept_none = self.accept_none
       
   289             if not accept_none:
   386                 if any(rset[i][col] is None for i in xrange(len(rset))):
   290                 if any(rset[i][col] is None for i in xrange(len(rset))):
   387                     return 0
   291                     return 0
   388             for etype in rset.column_types(col):
   292             for etype in rset.column_types(col):
   389                 if etype is None: # outer join
   293                 if etype is None: # outer join
   390                     return 0
   294                     return 0
   440        while the former works on each *entity* (eg each row of the result set),
   344        while the former works on each *entity* (eg each row of the result set),
   441        which may be much more costly.
   345        which may be much more costly.
   442     """
   346     """
   443 
   347 
   444     @lltrace
   348     @lltrace
   445     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
   349     def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
       
   350                  **kwargs):
   446         if not rset and not kwargs.get('entity'):
   351         if not rset and not kwargs.get('entity'):
   447             return 0
   352             return 0
   448         score = 0
   353         score = 0
   449         if kwargs.get('entity'):
   354         if kwargs.get('entity'):
   450             score = self.score_entity(kwargs['entity'])
   355             score = self.score_entity(kwargs['entity'])
   451         elif row is None:
   356         elif row is None:
   452             col = col or 0
   357             col = col or 0
       
   358             if accept_none is None:
       
   359                 accept_none = self.accept_none
   453             for row, rowvalue in enumerate(rset.rows):
   360             for row, rowvalue in enumerate(rset.rows):
   454                 if rowvalue[col] is None: # outer join
   361                 if rowvalue[col] is None: # outer join
   455                     if not self.accept_none:
   362                     if not accept_none:
   456                         return 0
   363                         return 0
   457                     continue
   364                     continue
   458                 escore = self.score(req, rset, row, col)
   365                 escore = self.score(req, rset, row, col)
   459                 if not escore and not self.once_is_enough:
   366                 if not escore and not self.once_is_enough:
   460                     return 0
   367                     return 0
   480 
   387 
   481 class ExpectedValueSelector(Selector):
   388 class ExpectedValueSelector(Selector):
   482     """Take a list of expected values as initializer argument and store them
   389     """Take a list of expected values as initializer argument and store them
   483     into the :attr:`expected` set attribute.
   390     into the :attr:`expected` set attribute.
   484 
   391 
   485     You should implements the :meth:`_get_value(cls, req, **kwargs)` method
   392     You should implement the :meth:`_get_value(cls, req, **kwargs)` method
   486     which should return the value for the given context. The selector will then
   393     which should return the value for the given context. The selector will then
   487     return 1 if the value is expected, else 0.
   394     return 1 if the value is expected, else 0.
   488     """
   395     """
   489     def __init__(self, *expected):
   396     def __init__(self, *expected):
   490         assert expected, self
   397         assert expected, self
   526 
   433 
   527     Initializer arguments:
   434     Initializer arguments:
   528 
   435 
   529     * `registry`, a registry name
   436     * `registry`, a registry name
   530 
   437 
   531     * `regid`, an object identifier in this registry
   438     * `regids`, object identifiers in this registry, one of them should be
   532     """
   439       selectable.
   533     def __init__(self, registry, regid):
   440     """
       
   441     selectable_score = 1
       
   442     def __init__(self, registry, *regids):
   534         self.registry = registry
   443         self.registry = registry
   535         self.regid = regid
   444         self.regids = regids
   536 
   445 
       
   446     @lltrace
   537     def __call__(self, cls, req, **kwargs):
   447     def __call__(self, cls, req, **kwargs):
   538         try:
   448         for regid in self.regids:
   539             req.vreg[self.registry].select(self.regid, req, **kwargs)
   449             try:
   540             return 1
   450                 req.vreg[self.registry].select(regid, req, **kwargs)
   541         except NoSelectableObject:
   451                 return self.selectable_score
   542             return 0
   452             except NoSelectableObject:
   543 
   453                 return 0
       
   454 
       
   455 
       
   456 class adaptable(appobject_selectable):
       
   457     """Return 1 if another appobject is selectable using the same input context.
       
   458 
       
   459     Initializer arguments:
       
   460 
       
   461     * `regids`, adapter identifiers (e.g. interface names) to which the context
       
   462       (usually entities) should be adaptable. One of them should be selectable
       
   463       when multiple identifiers are given.
       
   464     """
       
   465     # being adaptable to an interface takes precedence other is_instance('Any'),
       
   466     # hence return 2 (is_instance('Any') score is 1)
       
   467     selectable_score = 2
       
   468     def __init__(self, *regids):
       
   469         super(adaptable, self).__init__('adapters', *regids)
       
   470 
       
   471     def __call__(self, cls, req, **kwargs):
       
   472         kwargs.setdefault('accept_none', False)
       
   473         return super(adaptable, self).__call__(cls, req, **kwargs)
   544 
   474 
   545 # rset selectors ##############################################################
   475 # rset selectors ##############################################################
   546 
   476 
   547 @objectify_selector
   477 @objectify_selector
   548 @lltrace
   478 @lltrace
   584 
   514 
   585 # XXX == multi_lines_rset(1)
   515 # XXX == multi_lines_rset(1)
   586 @objectify_selector
   516 @objectify_selector
   587 @lltrace
   517 @lltrace
   588 def one_line_rset(cls, req, rset=None, row=None, **kwargs):
   518 def one_line_rset(cls, req, rset=None, row=None, **kwargs):
   589     """Return 1 if the result set is of size 1 or if a specific row in the
   519     """Return 1 if the result set is of size 1, or greater but a specific row in
   590     result set is specified ('row' argument).
   520       the result set is specified ('row' argument).
   591     """
   521     """
   592     if rset is not None and (row is not None or rset.rowcount == 1):
   522     if rset is not None and (row is not None or rset.rowcount == 1):
   593         return 1
   523         return 1
   594     return 0
   524     return 0
   595 
   525 
   596 
   526 
   597 class multi_lines_rset(Selector):
   527 class multi_lines_rset(Selector):
   598     """If `nb`is specified, return 1 if the result set has exactly `nb` row of
   528     """If `nb` is specified, return 1 if the result set has exactly `nb` row of
   599     result. Else (`nb` is None), return 1 if the result set contains *at least*
   529     result. Else (`nb` is None), return 1 if the result set contains *at least*
   600     two rows.
   530     two rows.
   601     """
   531     """
   602     def __init__(self, nb=None):
   532     def __init__(self, nb=None):
   603         self.expected = nb
   533         self.expected = nb
   607             return num > 1
   537             return num > 1
   608         return num == self.expected
   538         return num == self.expected
   609 
   539 
   610     @lltrace
   540     @lltrace
   611     def __call__(self, cls, req, rset=None, **kwargs):
   541     def __call__(self, cls, req, rset=None, **kwargs):
   612         return rset is not None and self.match_expected(rset.rowcount)
   542         return int(rset is not None and self.match_expected(rset.rowcount))
   613 
   543 
   614 
   544 
   615 class multi_columns_rset(multi_lines_rset):
   545 class multi_columns_rset(multi_lines_rset):
   616     """If `nb`is specified, return 1 if the result set has exactly `nb` column
   546     """If `nb` is specified, return 1 if the result set has exactly `nb` column
   617     per row. Else (`nb` is None), return 1 if the result set contains *at least*
   547     per row. Else (`nb` is None), return 1 if the result set contains *at least*
   618     two columns per row. Return 0 for empty result set.
   548     two columns per row. Return 0 for empty result set.
   619     """
   549     """
   620 
   550 
   621     @lltrace
   551     @lltrace
   657 
   587 
   658 @objectify_selector
   588 @objectify_selector
   659 @lltrace
   589 @lltrace
   660 def sorted_rset(cls, req, rset=None, **kwargs):
   590 def sorted_rset(cls, req, rset=None, **kwargs):
   661     """Return 1 for sorted result set (e.g. from an RQL query containing an
   591     """Return 1 for sorted result set (e.g. from an RQL query containing an
   662     :ref:ORDERBY clause.
   592     :ref:ORDERBY clause), with exception that it will return 0 if the rset is
       
   593     'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
   663     """
   594     """
   664     if rset is None:
   595     if rset is None:
   665         return 0
   596         return 0
   666     rqlst = rset.syntax_tree()
   597     selects = rset.syntax_tree().children
   667     if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
   598     if (len(selects) > 1 or
       
   599         not selects[0].orderby or
       
   600         (isinstance(selects[0].orderby[0].term, Function) and
       
   601          selects[0].orderby[0].term.name == 'FTIRANK')
       
   602         ):
   668         return 0
   603         return 0
   669     return 2
   604     return 2
   670 
   605 
   671 
   606 
   672 # XXX == multi_etypes_rset(1)
   607 # XXX == multi_etypes_rset(1)
   710 # entity selectors #############################################################
   645 # entity selectors #############################################################
   711 
   646 
   712 class non_final_entity(EClassSelector):
   647 class non_final_entity(EClassSelector):
   713     """Return 1 for entity of a non final entity type(s). Remember, "final"
   648     """Return 1 for entity of a non final entity type(s). Remember, "final"
   714     entity types are String, Int, etc... This is equivalent to
   649     entity types are String, Int, etc... This is equivalent to
   715     `implements('Any')` but more optimized.
   650     `is_instance('Any')` but more optimized.
   716 
   651 
   717     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
   652     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
   718     class lookup / score rules according to the input context.
   653     class lookup / score rules according to the input context.
   719     """
   654     """
   720     def score(self, cls, req, etype):
   655     def score(self, cls, req, etype):
   724 
   659 
   725     def score_class(self, eclass, req):
   660     def score_class(self, eclass, req):
   726         return 1 # necessarily true if we're there
   661         return 1 # necessarily true if we're there
   727 
   662 
   728 
   663 
   729 class implements(ImplementsMixIn, EClassSelector):
   664 class implements(EClassSelector):
   730     """Return non-zero score for entity that are of the given type(s) or
   665     """Return non-zero score for entity that are of the given type(s) or
   731     implements at least one of the given interface(s). If multiple arguments are
   666     implements at least one of the given interface(s). If multiple arguments are
   732     given, matching one of them is enough.
   667     given, matching one of them is enough.
   733 
   668 
   734     Entity types should be given as string, the corresponding class will be
   669     Entity types should be given as string, the corresponding class will be
   737     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
   672     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
   738     class lookup / score rules according to the input context.
   673     class lookup / score rules according to the input context.
   739 
   674 
   740     .. note:: when interface is an entity class, the score will reflect class
   675     .. note:: when interface is an entity class, the score will reflect class
   741               proximity so the most specific object will be selected.
   676               proximity so the most specific object will be selected.
   742     """
   677 
       
   678     .. note:: deprecated in cubicweb >= 3.9, use either
       
   679               :class:`~cubicweb.selectors.is_instance` or
       
   680               :class:`~cubicweb.selectors.adaptable`.
       
   681     """
       
   682 
       
   683     def __init__(self, *expected_ifaces, **kwargs):
       
   684         emit_warn = kwargs.pop('warn', True)
       
   685         super(implements, self).__init__(**kwargs)
       
   686         self.expected_ifaces = expected_ifaces
       
   687         if emit_warn:
       
   688             warn('[3.9] implements selector is deprecated, use either '
       
   689                  'is_instance or adaptable', DeprecationWarning, stacklevel=2)
       
   690 
       
   691     def __str__(self):
       
   692         return '%s(%s)' % (self.__class__.__name__,
       
   693                            ','.join(str(s) for s in self.expected_ifaces))
       
   694 
   743     def score_class(self, eclass, req):
   695     def score_class(self, eclass, req):
   744         return self.score_interfaces(req, eclass, eclass)
   696         return self.score_interfaces(req, eclass, eclass)
       
   697 
       
   698     def score_interfaces(self, req, cls_or_inst, cls):
       
   699         score = 0
       
   700         etypesreg = req.vreg['etypes']
       
   701         for iface in self.expected_ifaces:
       
   702             if isinstance(iface, basestring):
       
   703                 # entity type
       
   704                 try:
       
   705                     iface = etypesreg.etype_class(iface)
       
   706                 except KeyError:
       
   707                     continue # entity type not in the schema
       
   708             score += score_interface(etypesreg, cls_or_inst, cls, iface)
       
   709         return score
       
   710 
       
   711 def _reset_is_instance_cache(vreg):
       
   712     vreg._is_instance_selector_cache = {}
       
   713 
       
   714 CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
       
   715 
       
   716 class is_instance(EClassSelector):
       
   717     """Return non-zero score for entity that is an instance of the one of given
       
   718     type(s). If multiple arguments are given, matching one of them is enough.
       
   719 
       
   720     Entity types should be given as string, the corresponding class will be
       
   721     fetched from the registry at selection time.
       
   722 
       
   723     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   724     class lookup / score rules according to the input context.
       
   725 
       
   726     .. note:: the score will reflect class proximity so the most specific object
       
   727               will be selected.
       
   728     """
       
   729 
       
   730     def __init__(self, *expected_etypes, **kwargs):
       
   731         super(is_instance, self).__init__(**kwargs)
       
   732         self.expected_etypes = expected_etypes
       
   733         for etype in self.expected_etypes:
       
   734             assert isinstance(etype, basestring), etype
       
   735 
       
   736     def __str__(self):
       
   737         return '%s(%s)' % (self.__class__.__name__,
       
   738                            ','.join(str(s) for s in self.expected_etypes))
       
   739 
       
   740     def score_class(self, eclass, req):
       
   741         return self.score_etypes(req, eclass, eclass)
       
   742 
       
   743     def score_etypes(self, req, cls_or_inst, cls):
       
   744         # cache on vreg to avoid reloading issues
       
   745         cache = req.vreg._is_instance_selector_cache
       
   746         try:
       
   747             expected_eclasses = cache[self]
       
   748         except KeyError:
       
   749             # turn list of entity types as string into a list of
       
   750             #  (entity class, parent classes)
       
   751             etypesreg = req.vreg['etypes']
       
   752             expected_eclasses = cache[self] = []
       
   753             for etype in self.expected_etypes:
       
   754                 try:
       
   755                     expected_eclasses.append(
       
   756                         (etypesreg.etype_class(etype),
       
   757                          etypesreg.parent_classes(etype))
       
   758                         )
       
   759                 except KeyError:
       
   760                     continue # entity type not in the schema
       
   761         score = 0
       
   762         for iface, parents in expected_eclasses:
       
   763             # adjust score according to class proximity
       
   764             if iface is cls:
       
   765                 score += len(parents) + 4
       
   766             elif iface is parents[-1]: # Any
       
   767                 score += 1
       
   768             else:
       
   769                 for index, basecls in enumerate(reversed(parents[:-1])):
       
   770                     if iface is basecls:
       
   771                         score += index + 3
       
   772                         break
       
   773         return score
   745 
   774 
   746 
   775 
   747 class score_entity(EntitySelector):
   776 class score_entity(EntitySelector):
   748     """Return score according to an arbitrary function given as argument which
   777     """Return score according to an arbitrary function given as argument which
   749     will be called with input content entity as argument.
   778     will be called with input content entity as argument.
   762                 return 0
   791                 return 0
   763             if isinstance(score, (int, long)):
   792             if isinstance(score, (int, long)):
   764                 return score
   793                 return score
   765             return 1
   794             return 1
   766         self.score_entity = intscore
   795         self.score_entity = intscore
       
   796 
       
   797 
       
   798 class has_mimetype(EntitySelector):
       
   799     """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
       
   800 
       
   801     You can give 'image/' to match any image for instance, or 'image/png' to match
       
   802     only PNG images.
       
   803     """
       
   804     def __init__(self, mimetype, once_is_enough=False):
       
   805         super(has_mimetype, self).__init__(once_is_enough)
       
   806         self.mimetype = mimetype
       
   807 
       
   808     def score_entity(self, entity):
       
   809         idownloadable = entity.cw_adapt_to('IDownloadable')
       
   810         if idownloadable is None:
       
   811             return 0
       
   812         mt = idownloadable.download_content_type()
       
   813         if not (mt and mt.startswith(self.mimetype)):
       
   814             return 0
       
   815         return 1
   767 
   816 
   768 
   817 
   769 class relation_possible(EntitySelector):
   818 class relation_possible(EntitySelector):
   770     """Return 1 for entity that supports the relation, provided that the
   819     """Return 1 for entity that supports the relation, provided that the
   771     request's user may do some `action` on it (see below).
   820     request's user may do some `action` on it (see below).
  1007                 score += 1
  1056                 score += 1
  1008             return score
  1057             return score
  1009         return self.score(req, rset, row, col)
  1058         return self.score(req, rset, row, col)
  1010 
  1059 
  1011     def score_entity(self, entity):
  1060     def score_entity(self, entity):
  1012         if entity.has_perm(self.action):
  1061         if entity.cw_has_perm(self.action):
  1013             return 1
  1062             return 1
  1014         return 0
  1063         return 0
  1015 
  1064 
  1016 
  1065 
  1017 class has_add_permission(EClassSelector):
  1066 class has_add_permission(EClassSelector):
  1231             if not param in req.form:
  1280             if not param in req.form:
  1232                 return 0
  1281                 return 0
  1233         return len(self.expected)
  1282         return len(self.expected)
  1234 
  1283 
  1235 
  1284 
  1236 class specified_etype_implements(implements):
  1285 class specified_etype_implements(is_instance):
  1237     """Return non-zero score if the entity type specified by an 'etype' key
  1286     """Return non-zero score if the entity type specified by an 'etype' key
  1238     searched in (by priority) input context kwargs and request form parameters
  1287     searched in (by priority) input context kwargs and request form parameters
  1239     match a known entity type (case insensitivly), and it's associated entity
  1288     match a known entity type (case insensitivly), and it's associated entity
  1240     class is of one of the type(s) given to the initializer or implements at
  1289     class is of one of the type(s) given to the initializer. If multiple
  1241     least one of the given interfaces. If multiple arguments are given, matching
  1290     arguments are given, matching one of them is enough.
  1242     one of them is enough.
  1291 
  1243 
  1292     .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
  1244     Entity types should be given as string, the corresponding class will be
  1293               should be given as string and the score will reflect class
  1245     fetched from the entity types registry at selection time.
       
  1246 
       
  1247     .. note:: when interface is an entity class, the score will reflect class
       
  1248               proximity so the most specific object will be selected.
  1294               proximity so the most specific object will be selected.
  1249 
  1295 
  1250     This selector is usually used by views holding entity creation forms (since
  1296     This selector is usually used by views holding entity creation forms (since
  1251     we've no result set to work on).
  1297     we've no result set to work on).
  1252     """
  1298     """
  1298         return 0
  1344         return 0
  1299 
  1345 
  1300 class is_in_state(score_entity):
  1346 class is_in_state(score_entity):
  1301     """return 1 if entity is in one of the states given as argument list
  1347     """return 1 if entity is in one of the states given as argument list
  1302 
  1348 
  1303     you should use this instead of your own score_entity x: x.state == 'bla'
  1349     you should use this instead of your own :class:`score_entity` selector to
  1304     selector to avoid some gotchas:
  1350     avoid some gotchas:
  1305 
  1351 
  1306     * possible views gives a fake entity with no state
  1352     * possible views gives a fake entity with no state
  1307     * you must use the latest tr info, not entity.state for repository side
  1353     * you must use the latest tr info, not entity.in_state for repository side
  1308       checking of the current state
  1354       checking of the current state
  1309     """
  1355     """
  1310     def __init__(self, *states):
  1356     def __init__(self, *states):
  1311         def score(entity, states=set(states)):
  1357         def score(entity, states=set(states)):
       
  1358             trinfo = entity.cw_adapt_to('IWorkflowable').latest_trinfo()
  1312             try:
  1359             try:
  1313                 return entity.latest_trinfo().new_state.name in states
  1360                 return trinfo.new_state.name in states
  1314             except AttributeError:
  1361             except AttributeError:
  1315                 return None
  1362                 return None
  1316         super(is_in_state, self).__init__(score)
  1363         super(is_in_state, self).__init__(score)
  1317 
  1364 
       
  1365 @objectify_selector
       
  1366 def debug_mode(cls, req, rset=None, **kwargs):
       
  1367     """Return 1 if running in debug mode"""
       
  1368     return req.vreg.config.debugmode and 1 or 0
  1318 
  1369 
  1319 ## deprecated stuff ############################################################
  1370 ## deprecated stuff ############################################################
  1320 
  1371 
  1321 entity_implements = class_renamed('entity_implements', implements)
  1372 entity_implements = class_renamed('entity_implements', is_instance)
  1322 
  1373 
  1323 class _but_etype(EntitySelector):
  1374 class _but_etype(EntitySelector):
  1324     """accept if the given entity types are not found in the result set.
  1375     """accept if the given entity types are not found in the result set.
  1325 
  1376 
  1326     See `EntitySelector` documentation for behaviour when row is not specified.
  1377     See `EntitySelector` documentation for behaviour when row is not specified.
  1334     def score(self, req, rset, row, col):
  1385     def score(self, req, rset, row, col):
  1335         if rset.description[row][col] in self.but_etypes:
  1386         if rset.description[row][col] in self.but_etypes:
  1336             return 0
  1387             return 0
  1337         return 1
  1388         return 1
  1338 
  1389 
  1339 but_etype = class_renamed('but_etype', _but_etype, 'use ~implements(*etypes) instead')
  1390 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
  1340 
  1391 
  1341 
  1392 
  1342 # XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
  1393 # XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
  1343 #     take care at the implementation though (looking for the 'row' argument's
  1394 #     take care at the implementation though (looking for the 'row' argument's
  1344 #     value)
  1395 #     value)