selectors.py
branchstable
changeset 8463 a964c40adbe3
parent 8397 42ec34b3ced9
child 8596 bd4f5052a532
equal deleted inserted replaced
8461:8af7c6d86efb 8463:a964c40adbe3
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
     3 #
     3 #
     4 # This file is part of CubicWeb.
     4 # This file is part of CubicWeb.
     5 #
     5 #
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    14 # details.
    14 # details.
    15 #
    15 #
    16 # You should have received a copy of the GNU Lesser General Public License along
    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/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """.. _Selectors:
       
    19 
    18 
    20 Selectors
    19 from warnings import warn
    21 ---------
       
    22 
    20 
    23 Using and combining existant selectors
    21 from logilab.common.deprecation import deprecated, class_renamed
    24 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    25 
    22 
    26 You can combine selectors using the `&`, `|` and `~` operators.
    23 from cubicweb.predicates import *
    27 
       
    28 When two selectors are combined using the `&` operator, it means that
       
    29 both should return a positive score. On success, the sum of scores is
       
    30 returned.
       
    31 
       
    32 When two selectors are combined using the `|` operator, it means that
       
    33 one of them should return a positive score. On success, the first
       
    34 positive score is returned.
       
    35 
       
    36 You can also "negate" a selector by precedeing it by the `~` unary operator.
       
    37 
       
    38 Of course you can use parenthesis to balance expressions.
       
    39 
       
    40 Example
       
    41 ~~~~~~~
       
    42 
       
    43 The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
       
    44 the blog entity itself.
       
    45 
       
    46 To do that, one defines a method on entity classes that returns the
       
    47 RSS stream url for a given entity. The default implementation on
       
    48 :class:`~cubicweb.entities.AnyEntity` (the generic entity class used
       
    49 as base for all others) and a specific implementation on `Blog` will
       
    50 do what we want.
       
    51 
       
    52 But when we have a result set containing several `Blog` entities (or
       
    53 different entities), we don't know on which entity to call the
       
    54 aforementioned method. In this case, we keep the generic behaviour.
       
    55 
       
    56 Hence we have two cases here, one for a single-entity rsets, the other for
       
    57 multi-entities rsets.
       
    58 
       
    59 In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
       
    60 
       
    61 .. sourcecode:: python
       
    62 
       
    63   class RSSIconBox(box.Box):
       
    64     ''' just display the RSS icon on uniform result set '''
       
    65     __select__ = box.Box.__select__ & non_final_entity()
       
    66 
       
    67 It takes into account:
       
    68 
       
    69 * the inherited selection criteria (one has to look them up in the class
       
    70   hierarchy to know the details)
       
    71 
       
    72 * :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
       
    73   containing non final entities (a 'final entity' being synonym for entity
       
    74   attributes type, eg `String`, `Int`, etc)
       
    75 
       
    76 This matches our second case. Hence we have to provide a specific component for
       
    77 the first case:
       
    78 
       
    79 .. sourcecode:: python
       
    80 
       
    81   class EntityRSSIconBox(RSSIconBox):
       
    82     '''just display the RSS icon on uniform result set for a single entity'''
       
    83     __select__ = RSSIconBox.__select__ & one_line_rset()
       
    84 
       
    85 Here, one adds the :class:`~cubicweb.selectors.one_line_rset` selector, which
       
    86 filters result sets of size 1. Thus, on a result set containing multiple
       
    87 entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
       
    88 selectable. However for a result set with one entity, the `EntityRSSIconBox`
       
    89 class will have a higher score than `RSSIconBox`, which is what we wanted.
       
    90 
       
    91 Of course, once this is done, you have to:
       
    92 
       
    93 * fill in the call method of `EntityRSSIconBox`
       
    94 
       
    95 * provide the default implementation of the method returning the RSS stream url
       
    96   on :class:`~cubicweb.entities.AnyEntity`
       
    97 
       
    98 * redefine this method on `Blog`.
       
    99 
    24 
   100 
    25 
   101 When to use selectors?
    26 warn('[3.15] cubicweb.selectors renamed into cubicweb.predicates',
   102 ~~~~~~~~~~~~~~~~~~~~~~
    27      DeprecationWarning, stacklevel=2)
   103 
    28 
   104 Selectors are to be used whenever arises the need of dispatching on the shape or
    29 # XXX pre 3.15 bw compat
   105 content of a result set or whatever else context (value in request form params,
    30 from cubicweb.appobject import (objectify_selector, traced_selection,
   106 authenticated user groups, etc...). That is, almost all the time.
    31                                 lltrace, yes)
   107 
    32 
   108 Here is a quick example:
    33 ExpectedValueSelector = class_renamed('ExpectedValueSelector',
       
    34                                       ExpectedValuePredicate)
       
    35 EClassSelector = class_renamed('EClassSelector', EClassPredicate)
       
    36 EntitySelector = class_renamed('EntitySelector', EntityPredicate)
   109 
    37 
   110 .. sourcecode:: python
    38 # XXX pre 3.7? bw compat
   111 
       
   112     class UserLink(component.Component):
       
   113 	'''if the user is the anonymous user, build a link to login else a link
       
   114 	to the connected user object with a logout link
       
   115 	'''
       
   116 	__regid__ = 'loggeduserlink'
       
   117 
       
   118 	def call(self):
       
   119 	    if self._cw.session.anonymous_session:
       
   120 		# display login link
       
   121 		...
       
   122 	    else:
       
   123 		# display a link to the connected user object with a loggout link
       
   124 		...
       
   125 
       
   126 The proper way to implement this with |cubicweb| is two have two different
       
   127 classes sharing the same identifier but with different selectors so you'll get
       
   128 the correct one according to the context.
       
   129 
       
   130 .. sourcecode:: python
       
   131 
       
   132     class UserLink(component.Component):
       
   133 	'''display a link to the connected user object with a loggout link'''
       
   134 	__regid__ = 'loggeduserlink'
       
   135 	__select__ = component.Component.__select__ & authenticated_user()
       
   136 
       
   137 	def call(self):
       
   138             # display useractions and siteactions
       
   139 	    ...
       
   140 
       
   141     class AnonUserLink(component.Component):
       
   142 	'''build a link to login'''
       
   143 	__regid__ = 'loggeduserlink'
       
   144 	__select__ = component.Component.__select__ & anonymous_user()
       
   145 
       
   146 	def call(self):
       
   147 	    # display login link
       
   148             ...
       
   149 
       
   150 The big advantage, aside readability once you're familiar with the
       
   151 system, is that your cube becomes much more easily customizable by
       
   152 improving componentization.
       
   153 
       
   154 
       
   155 .. _CustomSelectors:
       
   156 
       
   157 Defining your own selectors
       
   158 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   159 
       
   160 .. autodocstring:: cubicweb.appobject::objectify_selector
       
   161 
       
   162 In other cases, you can take a look at the following abstract base classes:
       
   163 
       
   164 .. autoclass:: cubicweb.selectors.ExpectedValueSelector
       
   165 .. autoclass:: cubicweb.selectors.EClassSelector
       
   166 .. autoclass:: cubicweb.selectors.EntitySelector
       
   167 
       
   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
       
   170 traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
       
   171 
       
   172 .. autofunction:: cubicweb.appobject.lltrace
       
   173 
       
   174 .. note::
       
   175   Selectors __call__ should *always* return a positive integer, and shall never
       
   176   return `None`.
       
   177 
       
   178 
       
   179 .. _DebuggingSelectors:
       
   180 
       
   181 Debugging selection
       
   182 ~~~~~~~~~~~~~~~~~~~
       
   183 
       
   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
       
   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*.
       
   188 
       
   189 .. autoclass:: cubicweb.appobject.traced_selection
       
   190 
       
   191 """
       
   192 
       
   193 __docformat__ = "restructuredtext en"
       
   194 
       
   195 import logging
       
   196 from warnings import warn
       
   197 from operator import eq
       
   198 
       
   199 from logilab.common.deprecation import class_renamed, deprecated
       
   200 from logilab.common.compat import all, any
       
   201 from logilab.common.interface import implements as implements_iface
       
   202 
       
   203 from yams.schema import BASE_TYPES, role_name
       
   204 from rql.nodes import Function
       
   205 
       
   206 from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
       
   207                       CW_EVENT_MANAGER, role)
       
   208 # even if not used, let yes here so it's importable through this module
       
   209 from cubicweb.uilib import eid_param
       
   210 from cubicweb.appobject import Selector, objectify_selector, lltrace, yes
       
   211 from cubicweb.schema import split_expression
       
   212 
       
   213 from cubicweb.appobject import traced_selection # XXX for bw compat
       
   214 
       
   215 def score_interface(etypesreg, eclass, iface):
       
   216     """Return XXX if the give object (maybe an instance or class) implements
       
   217     the interface.
       
   218     """
       
   219     if getattr(iface, '__registry__', None) == 'etypes':
       
   220         # adjust score if the interface is an entity class
       
   221         parents, any = etypesreg.parent_classes(eclass.__regid__)
       
   222         if iface is eclass:
       
   223             return len(parents) + 4
       
   224         if iface is any: # Any
       
   225             return 1
       
   226         for index, basecls in enumerate(reversed(parents)):
       
   227             if iface is basecls:
       
   228                 return index + 3
       
   229         return 0
       
   230     # XXX iface in implements deprecated in 3.9
       
   231     if implements_iface(eclass, iface):
       
   232         # implementing an interface takes precedence other special Any interface
       
   233         return 2
       
   234     return 0
       
   235 
       
   236 
       
   237 # abstract selectors / mixin helpers ###########################################
       
   238 
       
   239 class PartialSelectorMixIn(object):
       
   240     """convenience mix-in for selectors that will look into the containing
       
   241     class to find missing information.
       
   242 
       
   243     cf. `cubicweb.web.action.LinkToEntityAction` for instance
       
   244     """
       
   245     def __call__(self, cls, *args, **kwargs):
       
   246         self.complete(cls)
       
   247         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
       
   248 
       
   249 
       
   250 class EClassSelector(Selector):
       
   251     """abstract class for selectors working on *entity class(es)* specified
       
   252     explicitly or found of the result set.
       
   253 
       
   254     Here are entity lookup / scoring rules:
       
   255 
       
   256     * if `entity` is specified, return score for this entity's class
       
   257 
       
   258     * elif `rset`, `select` and `filtered_variable` are specified, return score
       
   259       for the possible classes for variable in the given rql :class:`Select`
       
   260       node
       
   261 
       
   262     * elif `rset` and `row` are specified, return score for the class of the
       
   263       entity found in the specified cell, using column specified by `col` or 0
       
   264 
       
   265     * elif `rset` is specified return score for each entity class found in the
       
   266       column specified specified by the `col` argument or in column 0 if not
       
   267       specified
       
   268 
       
   269     When there are several classes to be evaluated, return the sum of scores for
       
   270     each entity class unless:
       
   271 
       
   272       - `mode` == 'all' (the default) and some entity class is scored
       
   273         to 0, in which case 0 is returned
       
   274 
       
   275       - `mode` == 'any', in which case the first non-zero score is
       
   276         returned
       
   277 
       
   278       - `accept_none` is False and some cell in the column has a None value
       
   279         (this may occurs with outer join)
       
   280     """
       
   281     def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
       
   282         if once_is_enough is not None:
       
   283             warn("[3.14] once_is_enough is deprecated, use mode='any'",
       
   284                  DeprecationWarning, stacklevel=2)
       
   285             if once_is_enough:
       
   286                 mode = 'any'
       
   287         assert mode in ('any', 'all'), 'bad mode %s' % mode
       
   288         self.once_is_enough = mode == 'any'
       
   289         self.accept_none = accept_none
       
   290 
       
   291     @lltrace
       
   292     def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
       
   293                  select=None, filtered_variable=None,
       
   294                  accept_none=None,
       
   295                  **kwargs):
       
   296         if entity is not None:
       
   297             return self.score_class(entity.__class__, req)
       
   298         if not rset:
       
   299             return 0
       
   300         if select is not None and filtered_variable is not None:
       
   301             etypes = set(sol[filtered_variable.name] for sol in select.solutions)
       
   302         elif row is None:
       
   303             if accept_none is None:
       
   304                 accept_none = self.accept_none
       
   305             if not accept_none and \
       
   306                    any(rset[i][col] is None for i in xrange(len(rset))):
       
   307                 return 0
       
   308             etypes = rset.column_types(col)
       
   309         else:
       
   310             etype = rset.description[row][col]
       
   311             # may have None in rset.description on outer join
       
   312             if etype is None or rset.rows[row][col] is None:
       
   313                 return 0
       
   314             etypes = (etype,)
       
   315         score = 0
       
   316         for etype in etypes:
       
   317             escore = self.score(cls, req, etype)
       
   318             if not escore and not self.once_is_enough:
       
   319                 return 0
       
   320             elif self.once_is_enough:
       
   321                 return escore
       
   322             score += escore
       
   323         return score
       
   324 
       
   325     def score(self, cls, req, etype):
       
   326         if etype in BASE_TYPES:
       
   327             return 0
       
   328         return self.score_class(req.vreg['etypes'].etype_class(etype), req)
       
   329 
       
   330     def score_class(self, eclass, req):
       
   331         raise NotImplementedError()
       
   332 
       
   333 
       
   334 class EntitySelector(EClassSelector):
       
   335     """abstract class for selectors working on *entity instance(s)* specified
       
   336     explicitly or found of the result set.
       
   337 
       
   338     Here are entity lookup / scoring rules:
       
   339 
       
   340     * if `entity` is specified, return score for this entity
       
   341 
       
   342     * elif `row` is specified, return score for the entity found in the
       
   343       specified cell, using column specified by `col` or 0
       
   344 
       
   345     * else return the sum of scores for each entity found in the column
       
   346       specified specified by the `col` argument or in column 0 if not specified,
       
   347       unless:
       
   348 
       
   349       - `mode` == 'all' (the default) and some entity class is scored
       
   350         to 0, in which case 0 is returned
       
   351 
       
   352       - `mode` == 'any', in which case the first non-zero score is
       
   353         returned
       
   354 
       
   355       - `accept_none` is False and some cell in the column has a None value
       
   356         (this may occurs with outer join)
       
   357 
       
   358     .. Note::
       
   359        using :class:`EntitySelector` or :class:`EClassSelector` as base selector
       
   360        class impacts performance, since when no entity or row is specified the
       
   361        later works on every different *entity class* found in the result set,
       
   362        while the former works on each *entity* (eg each row of the result set),
       
   363        which may be much more costly.
       
   364     """
       
   365 
       
   366     @lltrace
       
   367     def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
       
   368                  **kwargs):
       
   369         if not rset and not kwargs.get('entity'):
       
   370             return 0
       
   371         score = 0
       
   372         if kwargs.get('entity'):
       
   373             score = self.score_entity(kwargs['entity'])
       
   374         elif row is None:
       
   375             col = col or 0
       
   376             if accept_none is None:
       
   377                 accept_none = self.accept_none
       
   378             for row, rowvalue in enumerate(rset.rows):
       
   379                 if rowvalue[col] is None: # outer join
       
   380                     if not accept_none:
       
   381                         return 0
       
   382                     continue
       
   383                 escore = self.score(req, rset, row, col)
       
   384                 if not escore and not self.once_is_enough:
       
   385                     return 0
       
   386                 elif self.once_is_enough:
       
   387                     return escore
       
   388                 score += escore
       
   389         else:
       
   390             col = col or 0
       
   391             etype = rset.description[row][col]
       
   392             if etype is not None: # outer join
       
   393                 score = self.score(req, rset, row, col)
       
   394         return score
       
   395 
       
   396     def score(self, req, rset, row, col):
       
   397         try:
       
   398             return self.score_entity(rset.get_entity(row, col))
       
   399         except NotAnEntity:
       
   400             return 0
       
   401 
       
   402     def score_entity(self, entity):
       
   403         raise NotImplementedError()
       
   404 
       
   405 
       
   406 class ExpectedValueSelector(Selector):
       
   407     """Take a list of expected values as initializer argument and store them
       
   408     into the :attr:`expected` set attribute. You may also give a set as single
       
   409     argument, which will then be referenced as set of expected values,
       
   410     allowing modifications to the given set to be considered.
       
   411 
       
   412     You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
       
   413     :meth:`_get_value(cls, req, **kwargs)` method which should respectively
       
   414     return the set of values or the unique possible value for the given context.
       
   415 
       
   416     You may also specify a `mode` behaviour as argument, as explained below.
       
   417 
       
   418     Returned score is:
       
   419 
       
   420     - 0 if `mode` == 'all' (the default) and at least one expected
       
   421       values isn't found
       
   422 
       
   423     - 0 if `mode` == 'any' and no expected values isn't found at all
       
   424 
       
   425     - else the number of matching values
       
   426 
       
   427     Notice `mode` = 'any' with a single expected value has no effect at all.
       
   428     """
       
   429     def __init__(self, *expected, **kwargs):
       
   430         assert expected, self
       
   431         if len(expected) == 1 and isinstance(expected[0], set):
       
   432             self.expected = expected[0]
       
   433         else:
       
   434             self.expected = frozenset(expected)
       
   435         mode = kwargs.pop('mode', 'all')
       
   436         assert mode in ('any', 'all'), 'bad mode %s' % mode
       
   437         self.once_is_enough = mode == 'any'
       
   438         assert not kwargs, 'unexpected arguments %s' % kwargs
       
   439 
       
   440     def __str__(self):
       
   441         return '%s(%s)' % (self.__class__.__name__,
       
   442                            ','.join(sorted(str(s) for s in self.expected)))
       
   443 
       
   444     @lltrace
       
   445     def __call__(self, cls, req, **kwargs):
       
   446         values = self._values_set(cls, req, **kwargs)
       
   447         matching = len(values & self.expected)
       
   448         if self.once_is_enough:
       
   449             return matching
       
   450         if matching == len(self.expected):
       
   451             return matching
       
   452         return 0
       
   453 
       
   454     def _values_set(self, cls, req, **kwargs):
       
   455         return frozenset( (self._get_value(cls, req, **kwargs),) )
       
   456 
       
   457     def _get_value(self, cls, req, **kwargs):
       
   458         raise NotImplementedError()
       
   459 
       
   460 
       
   461 # bare selectors ##############################################################
       
   462 
       
   463 class match_kwargs(ExpectedValueSelector):
       
   464     """Return non-zero score if parameter names specified as initializer
       
   465     arguments are specified in the input context.
       
   466 
       
   467 
       
   468     Return a score corresponding to the number of expected parameters.
       
   469 
       
   470     When multiple parameters are expected, all of them should be found in
       
   471     the input context unless `mode` keyword argument is given to 'any',
       
   472     in which case a single matching parameter is enough.
       
   473     """
       
   474 
       
   475     def _values_set(self, cls, req, **kwargs):
       
   476         return frozenset(kwargs)
       
   477 
       
   478 
       
   479 class appobject_selectable(Selector):
       
   480     """Return 1 if another appobject is selectable using the same input context.
       
   481 
       
   482     Initializer arguments:
       
   483 
       
   484     * `registry`, a registry name
       
   485 
       
   486     * `regids`, object identifiers in this registry, one of them should be
       
   487       selectable.
       
   488     """
       
   489     selectable_score = 1
       
   490     def __init__(self, registry, *regids):
       
   491         self.registry = registry
       
   492         self.regids = regids
       
   493 
       
   494     @lltrace
       
   495     def __call__(self, cls, req, **kwargs):
       
   496         for regid in self.regids:
       
   497             try:
       
   498                 req.vreg[self.registry].select(regid, req, **kwargs)
       
   499                 return self.selectable_score
       
   500             except NoSelectableObject:
       
   501                 continue
       
   502         return 0
       
   503 
       
   504 
       
   505 class adaptable(appobject_selectable):
       
   506     """Return 1 if another appobject is selectable using the same input context.
       
   507 
       
   508     Initializer arguments:
       
   509 
       
   510     * `regids`, adapter identifiers (e.g. interface names) to which the context
       
   511       (usually entities) should be adaptable. One of them should be selectable
       
   512       when multiple identifiers are given.
       
   513     """
       
   514     def __init__(self, *regids):
       
   515         super(adaptable, self).__init__('adapters', *regids)
       
   516 
       
   517     def __call__(self, cls, req, **kwargs):
       
   518         kwargs.setdefault('accept_none', False)
       
   519         # being adaptable to an interface should takes precedence other is_instance('Any'),
       
   520         # but not other explicit is_instance('SomeEntityType'), and:
       
   521         # * is_instance('Any') score is 1
       
   522         # * is_instance('SomeEntityType') score is at least 2
       
   523         score = super(adaptable, self).__call__(cls, req, **kwargs)
       
   524         if score >= 2:
       
   525             return score - 0.5
       
   526         if score == 1:
       
   527             return score + 0.5
       
   528         return score
       
   529 
       
   530 
       
   531 class configuration_values(Selector):
       
   532     """Return 1 if the instance has an option set to a given value(s) in its
       
   533     configuration file.
       
   534     """
       
   535     # XXX this selector could be evaluated on startup
       
   536     def __init__(self, key, values):
       
   537         self._key = key
       
   538         if not isinstance(values, (tuple, list)):
       
   539             values = (values,)
       
   540         self._values = frozenset(values)
       
   541 
       
   542     @lltrace
       
   543     def __call__(self, cls, req, **kwargs):
       
   544         try:
       
   545             return self._score
       
   546         except AttributeError:
       
   547             if req is None:
       
   548                 config = kwargs['repo'].config
       
   549             else:
       
   550                 config = req.vreg.config
       
   551             self._score = config[self._key] in self._values
       
   552         return self._score
       
   553 
       
   554 
       
   555 # rset selectors ##############################################################
       
   556 
       
   557 @objectify_selector
       
   558 @lltrace
       
   559 def none_rset(cls, req, rset=None, **kwargs):
       
   560     """Return 1 if the result set is None (eg usually not specified)."""
       
   561     if rset is None:
       
   562         return 1
       
   563     return 0
       
   564 
       
   565 
       
   566 # XXX == ~ none_rset
       
   567 @objectify_selector
       
   568 @lltrace
       
   569 def any_rset(cls, req, rset=None, **kwargs):
       
   570     """Return 1 for any result set, whatever the number of rows in it, even 0."""
       
   571     if rset is not None:
       
   572         return 1
       
   573     return 0
       
   574 
       
   575 
       
   576 @objectify_selector
       
   577 @lltrace
       
   578 def nonempty_rset(cls, req, rset=None, **kwargs):
       
   579     """Return 1 for result set containing one ore more rows."""
       
   580     if rset is not None and rset.rowcount:
       
   581         return 1
       
   582     return 0
       
   583 
       
   584 
       
   585 # XXX == ~ nonempty_rset
       
   586 @objectify_selector
       
   587 @lltrace
       
   588 def empty_rset(cls, req, rset=None, **kwargs):
       
   589     """Return 1 for result set which doesn't contain any row."""
       
   590     if rset is not None and rset.rowcount == 0:
       
   591         return 1
       
   592     return 0
       
   593 
       
   594 
       
   595 # XXX == multi_lines_rset(1)
       
   596 @objectify_selector
       
   597 @lltrace
       
   598 def one_line_rset(cls, req, rset=None, row=None, **kwargs):
       
   599     """Return 1 if the result set is of size 1, or greater but a specific row in
       
   600       the result set is specified ('row' argument).
       
   601     """
       
   602     if rset is None and 'entity' in kwargs:
       
   603         return 1
       
   604     if rset is not None and (row is not None or rset.rowcount == 1):
       
   605         return 1
       
   606     return 0
       
   607 
       
   608 
       
   609 class multi_lines_rset(Selector):
       
   610     """Return 1 if the operator expression matches between `num` elements
       
   611     in the result set and the `expected` value if defined.
       
   612 
       
   613     By default, multi_lines_rset(expected) matches equality expression:
       
   614         `nb` row(s) in result set equals to expected value
       
   615     But, you can perform richer comparisons by overriding default operator:
       
   616         multi_lines_rset(expected, operator.gt)
       
   617 
       
   618     If `expected` is None, return 1 if the result set contains *at least*
       
   619     two rows.
       
   620     If rset is None, return 0.
       
   621     """
       
   622     def __init__(self, expected=None, operator=eq):
       
   623         self.expected = expected
       
   624         self.operator = operator
       
   625 
       
   626     def match_expected(self, num):
       
   627         if self.expected is None:
       
   628             return num > 1
       
   629         return self.operator(num, self.expected)
       
   630 
       
   631     @lltrace
       
   632     def __call__(self, cls, req, rset=None, **kwargs):
       
   633         return int(rset is not None and self.match_expected(rset.rowcount))
       
   634 
       
   635 
       
   636 class multi_columns_rset(multi_lines_rset):
       
   637     """If `nb` is specified, return 1 if the result set has exactly `nb` column
       
   638     per row. Else (`nb` is None), return 1 if the result set contains *at least*
       
   639     two columns per row. Return 0 for empty result set.
       
   640     """
       
   641 
       
   642     @lltrace
       
   643     def __call__(self, cls, req, rset=None, **kwargs):
       
   644         # 'or 0' since we *must not* return None. Also don't use rset.rows so
       
   645         # this selector will work if rset is a simple list of list.
       
   646         return rset and self.match_expected(len(rset[0])) or 0
       
   647 
       
   648 
       
   649 class paginated_rset(Selector):
       
   650     """Return 1 or more for result set with more rows than one or more page
       
   651     size.  You can specify expected number of pages to the initializer (default
       
   652     to one), and you'll get that number of pages as score if the result set is
       
   653     big enough.
       
   654 
       
   655     Page size is searched in (respecting order):
       
   656     * a `page_size` argument
       
   657     * a `page_size` form parameters
       
   658     * the `navigation.page-size` property (see :ref:`PersistentProperties`)
       
   659     """
       
   660     def __init__(self, nbpages=1):
       
   661         assert nbpages > 0
       
   662         self.nbpages = nbpages
       
   663 
       
   664     @lltrace
       
   665     def __call__(self, cls, req, rset=None, **kwargs):
       
   666         if rset is None:
       
   667             return 0
       
   668         page_size = kwargs.get('page_size')
       
   669         if page_size is None:
       
   670             page_size = req.form.get('page_size')
       
   671             if page_size is None:
       
   672                 page_size = req.property_value('navigation.page-size')
       
   673             else:
       
   674                 page_size = int(page_size)
       
   675         if rset.rowcount <= (page_size*self.nbpages):
       
   676             return 0
       
   677         return self.nbpages
       
   678 
       
   679 
       
   680 @objectify_selector
       
   681 @lltrace
       
   682 def sorted_rset(cls, req, rset=None, **kwargs):
       
   683     """Return 1 for sorted result set (e.g. from an RQL query containing an
       
   684     ORDERBY clause), with exception that it will return 0 if the rset is
       
   685     'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
       
   686     """
       
   687     if rset is None:
       
   688         return 0
       
   689     selects = rset.syntax_tree().children
       
   690     if (len(selects) > 1 or
       
   691         not selects[0].orderby or
       
   692         (isinstance(selects[0].orderby[0].term, Function) and
       
   693          selects[0].orderby[0].term.name == 'FTIRANK')
       
   694         ):
       
   695         return 0
       
   696     return 2
       
   697 
       
   698 
       
   699 # XXX == multi_etypes_rset(1)
       
   700 @objectify_selector
       
   701 @lltrace
       
   702 def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
       
   703     """Return 1 if the result set contains entities which are all of the same
       
   704     type in the column specified by the `col` argument of the input context, or
       
   705     in column 0.
       
   706     """
       
   707     if rset is None:
       
   708         return 0
       
   709     if len(rset.column_types(col)) != 1:
       
   710         return 0
       
   711     return 1
       
   712 
       
   713 
       
   714 class multi_etypes_rset(multi_lines_rset):
       
   715     """If `nb` is specified, return 1 if the result set contains `nb` different
       
   716     types of entities in the column specified by the `col` argument of the input
       
   717     context, or in column 0. If `nb` is None, return 1 if the result set contains
       
   718     *at least* two different types of entities.
       
   719     """
       
   720 
       
   721     @lltrace
       
   722     def __call__(self, cls, req, rset=None, col=0, **kwargs):
       
   723         # 'or 0' since we *must not* return None
       
   724         return rset and self.match_expected(len(rset.column_types(col))) or 0
       
   725 
       
   726 
       
   727 @objectify_selector
       
   728 def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
       
   729     """Return positive score if the result set at the specified row / col
       
   730     contains the eid of the logged user.
       
   731     """
       
   732     if rset is None:
       
   733         return 0
       
   734     return req.user.eid == rset[row or 0][col]
       
   735 
       
   736 
       
   737 # entity selectors #############################################################
       
   738 
       
   739 class non_final_entity(EClassSelector):
       
   740     """Return 1 for entity of a non final entity type(s). Remember, "final"
       
   741     entity types are String, Int, etc... This is equivalent to
       
   742     `is_instance('Any')` but more optimized.
       
   743 
       
   744     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   745     class lookup / score rules according to the input context.
       
   746     """
       
   747     def score(self, cls, req, etype):
       
   748         if etype in BASE_TYPES:
       
   749             return 0
       
   750         return 1
       
   751 
       
   752     def score_class(self, eclass, req):
       
   753         return 1 # necessarily true if we're there
       
   754 
       
   755 
       
   756 class implements(EClassSelector):
       
   757     """Return non-zero score for entity that are of the given type(s) or
       
   758     implements at least one of the given interface(s). If multiple arguments are
       
   759     given, matching one of them is enough.
       
   760 
       
   761     Entity types should be given as string, the corresponding class will be
       
   762     fetched from the entity types registry at selection time.
       
   763 
       
   764     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   765     class lookup / score rules according to the input context.
       
   766 
       
   767     .. note:: when interface is an entity class, the score will reflect class
       
   768               proximity so the most specific object will be selected.
       
   769 
       
   770     .. note:: deprecated in cubicweb >= 3.9, use either
       
   771               :class:`~cubicweb.selectors.is_instance` or
       
   772               :class:`~cubicweb.selectors.adaptable`.
       
   773     """
       
   774 
       
   775     def __init__(self, *expected_ifaces, **kwargs):
       
   776         emit_warn = kwargs.pop('warn', True)
       
   777         super(implements, self).__init__(**kwargs)
       
   778         self.expected_ifaces = expected_ifaces
       
   779         if emit_warn:
       
   780             warn('[3.9] implements selector is deprecated, use either '
       
   781                  'is_instance or adaptable', DeprecationWarning, stacklevel=2)
       
   782 
       
   783     def __str__(self):
       
   784         return '%s(%s)' % (self.__class__.__name__,
       
   785                            ','.join(str(s) for s in self.expected_ifaces))
       
   786 
       
   787     def score_class(self, eclass, req):
       
   788         score = 0
       
   789         etypesreg = req.vreg['etypes']
       
   790         for iface in self.expected_ifaces:
       
   791             if isinstance(iface, basestring):
       
   792                 # entity type
       
   793                 try:
       
   794                     iface = etypesreg.etype_class(iface)
       
   795                 except KeyError:
       
   796                     continue # entity type not in the schema
       
   797             score += score_interface(etypesreg, eclass, iface)
       
   798         return score
       
   799 
       
   800 def _reset_is_instance_cache(vreg):
       
   801     vreg._is_instance_selector_cache = {}
       
   802 
       
   803 CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
       
   804 
       
   805 class is_instance(EClassSelector):
       
   806     """Return non-zero score for entity that is an instance of the one of given
       
   807     type(s). If multiple arguments are given, matching one of them is enough.
       
   808 
       
   809     Entity types should be given as string, the corresponding class will be
       
   810     fetched from the registry at selection time.
       
   811 
       
   812     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   813     class lookup / score rules according to the input context.
       
   814 
       
   815     .. note:: the score will reflect class proximity so the most specific object
       
   816               will be selected.
       
   817     """
       
   818 
       
   819     def __init__(self, *expected_etypes, **kwargs):
       
   820         super(is_instance, self).__init__(**kwargs)
       
   821         self.expected_etypes = expected_etypes
       
   822         for etype in self.expected_etypes:
       
   823             assert isinstance(etype, basestring), etype
       
   824 
       
   825     def __str__(self):
       
   826         return '%s(%s)' % (self.__class__.__name__,
       
   827                            ','.join(str(s) for s in self.expected_etypes))
       
   828 
       
   829     def score_class(self, eclass, req):
       
   830         # cache on vreg to avoid reloading issues
       
   831         try:
       
   832             cache = req.vreg._is_instance_selector_cache
       
   833         except AttributeError:
       
   834             # XXX 'before-registry-reset' not called for db-api connections
       
   835             cache = req.vreg._is_instance_selector_cache = {}
       
   836         try:
       
   837             expected_eclasses = cache[self]
       
   838         except KeyError:
       
   839             # turn list of entity types as string into a list of
       
   840             #  (entity class, parent classes)
       
   841             etypesreg = req.vreg['etypes']
       
   842             expected_eclasses = cache[self] = []
       
   843             for etype in self.expected_etypes:
       
   844                 try:
       
   845                     expected_eclasses.append(etypesreg.etype_class(etype))
       
   846                 except KeyError:
       
   847                     continue # entity type not in the schema
       
   848         parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
       
   849         score = 0
       
   850         for expectedcls in expected_eclasses:
       
   851             # adjust score according to class proximity
       
   852             if expectedcls is eclass:
       
   853                 score += len(parents) + 4
       
   854             elif expectedcls is any: # Any
       
   855                 score += 1
       
   856             else:
       
   857                 for index, basecls in enumerate(reversed(parents)):
       
   858                     if expectedcls is basecls:
       
   859                         score += index + 3
       
   860                         break
       
   861         return score
       
   862 
       
   863 
       
   864 class score_entity(EntitySelector):
       
   865     """Return score according to an arbitrary function given as argument which
       
   866     will be called with input content entity as argument.
       
   867 
       
   868     This is a very useful selector that will usually interest you since it
       
   869     allows a lot of things without having to write a specific selector.
       
   870 
       
   871     The function can return arbitrary value which will be casted to an integer
       
   872     value at the end.
       
   873 
       
   874     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
   875     lookup / score rules according to the input context.
       
   876     """
       
   877     def __init__(self, scorefunc, once_is_enough=None, mode='all'):
       
   878         super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
   879         def intscore(*args, **kwargs):
       
   880             score = scorefunc(*args, **kwargs)
       
   881             if not score:
       
   882                 return 0
       
   883             if isinstance(score, (int, long)):
       
   884                 return score
       
   885             return 1
       
   886         self.score_entity = intscore
       
   887 
       
   888 
       
   889 class has_mimetype(EntitySelector):
       
   890     """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
       
   891 
       
   892     You can give 'image/' to match any image for instance, or 'image/png' to match
       
   893     only PNG images.
       
   894     """
       
   895     def __init__(self, mimetype, once_is_enough=None, mode='all'):
       
   896         super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
   897         self.mimetype = mimetype
       
   898 
       
   899     def score_entity(self, entity):
       
   900         idownloadable = entity.cw_adapt_to('IDownloadable')
       
   901         if idownloadable is None:
       
   902             return 0
       
   903         mt = idownloadable.download_content_type()
       
   904         if not (mt and mt.startswith(self.mimetype)):
       
   905             return 0
       
   906         return 1
       
   907 
       
   908 
       
   909 class relation_possible(EntitySelector):
       
   910     """Return 1 for entity that supports the relation, provided that the
       
   911     request's user may do some `action` on it (see below).
       
   912 
       
   913     The relation is specified by the following initializer arguments:
       
   914 
       
   915     * `rtype`, the name of the relation
       
   916 
       
   917     * `role`, the role of the entity in the relation, either 'subject' or
       
   918       'object', default to 'subject'
       
   919 
       
   920     * `target_etype`, optional name of an entity type that should be supported
       
   921       at the other end of the relation
       
   922 
       
   923     * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
       
   924       default to 'read') which must be granted to the user, else a 0 score will
       
   925       be returned. Give None if you don't want any permission checking.
       
   926 
       
   927     * `strict`, boolean (default to False) telling what to do when the user has
       
   928       not globally the permission for the action (eg the action is not granted
       
   929       to one of the user's groups)
       
   930 
       
   931       - when strict is False, if there are some local role defined for this
       
   932         action (e.g. using rql expressions), then the permission will be
       
   933         considered as granted
       
   934 
       
   935       - when strict is True, then the permission will be actually checked for
       
   936         each entity
       
   937 
       
   938     Setting `strict` to True impacts performance for large result set since
       
   939     you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour
       
   940     while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
       
   941     one. See those classes documentation for entity lookup / score rules
       
   942     according to the input context.
       
   943     """
       
   944 
       
   945     def __init__(self, rtype, role='subject', target_etype=None,
       
   946                  action='read', strict=False, **kwargs):
       
   947         super(relation_possible, self).__init__(**kwargs)
       
   948         self.rtype = rtype
       
   949         self.role = role
       
   950         self.target_etype = target_etype
       
   951         self.action = action
       
   952         self.strict = strict
       
   953 
       
   954     # hack hack hack
       
   955     def __call__(self, cls, req, **kwargs):
       
   956         # hack hack hack
       
   957         if self.strict:
       
   958             return EntitySelector.__call__(self, cls, req, **kwargs)
       
   959         return EClassSelector.__call__(self, cls, req, **kwargs)
       
   960 
       
   961     def score(self, *args):
       
   962         if self.strict:
       
   963             return EntitySelector.score(self, *args)
       
   964         return EClassSelector.score(self, *args)
       
   965 
       
   966     def _get_rschema(self, eclass):
       
   967         eschema = eclass.e_schema
       
   968         try:
       
   969             if self.role == 'object':
       
   970                 return eschema.objrels[self.rtype]
       
   971             else:
       
   972                 return eschema.subjrels[self.rtype]
       
   973         except KeyError:
       
   974             return None
       
   975 
       
   976     def score_class(self, eclass, req):
       
   977         rschema = self._get_rschema(eclass)
       
   978         if rschema is None:
       
   979             return 0 # relation not supported
       
   980         eschema = eclass.e_schema
       
   981         if self.target_etype is not None:
       
   982             try:
       
   983                 rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
       
   984             except KeyError:
       
   985                 return 0
       
   986             if self.action and not rdef.may_have_permission(self.action, req):
       
   987                 return 0
       
   988             teschema = req.vreg.schema.eschema(self.target_etype)
       
   989             if not teschema.may_have_permission('read', req):
       
   990                 return 0
       
   991         elif self.action:
       
   992             return rschema.may_have_permission(self.action, req, eschema, self.role)
       
   993         return 1
       
   994 
       
   995     def score_entity(self, entity):
       
   996         rschema = self._get_rschema(entity)
       
   997         if rschema is None:
       
   998             return 0 # relation not supported
       
   999         if self.action:
       
  1000             if self.target_etype is not None:
       
  1001                 rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
       
  1002             if self.role == 'subject':
       
  1003                 if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
       
  1004                     return 0
       
  1005             elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
       
  1006                 return 0
       
  1007         if self.target_etype is not None:
       
  1008             req = entity._cw
       
  1009             teschema = req.vreg.schema.eschema(self.target_etype)
       
  1010             if not teschema.may_have_permission('read', req):
       
  1011                 return 0
       
  1012         return 1
       
  1013 
       
  1014 
       
  1015 class partial_relation_possible(PartialSelectorMixIn, relation_possible):
       
  1016     """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for
       
  1017     attributes of the selected class to get information which is otherwise
       
  1018     expected by the initializer, except for `action` and `strict` which are kept
       
  1019     as initializer arguments.
       
  1020 
       
  1021     This is useful to predefine selector of an abstract class designed to be
       
  1022     customized.
       
  1023     """
       
  1024     def __init__(self, action='read', **kwargs):
       
  1025         super(partial_relation_possible, self).__init__(None, None, None,
       
  1026                                                         action, **kwargs)
       
  1027 
       
  1028     def complete(self, cls):
       
  1029         self.rtype = cls.rtype
       
  1030         self.role = role(cls)
       
  1031         self.target_etype = getattr(cls, 'target_etype', None)
       
  1032 
       
  1033 
       
  1034 class has_related_entities(EntitySelector):
       
  1035     """Return 1 if entity support the specified relation and has some linked
       
  1036     entities by this relation , optionaly filtered according to the specified
       
  1037     target type.
       
  1038 
       
  1039     The relation is specified by the following initializer arguments:
       
  1040 
       
  1041     * `rtype`, the name of the relation
       
  1042 
       
  1043     * `role`, the role of the entity in the relation, either 'subject' or
       
  1044       'object', default to 'subject'.
       
  1045 
       
  1046     * `target_etype`, optional name of an entity type that should be found
       
  1047       at the other end of the relation
       
  1048 
       
  1049     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
  1050     lookup / score rules according to the input context.
       
  1051     """
       
  1052     def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
       
  1053         super(has_related_entities, self).__init__(**kwargs)
       
  1054         self.rtype = rtype
       
  1055         self.role = role
       
  1056         self.target_etype = target_etype
       
  1057 
       
  1058     def score_entity(self, entity):
       
  1059         relpossel = relation_possible(self.rtype, self.role, self.target_etype)
       
  1060         if not relpossel.score_class(entity.__class__, entity._cw):
       
  1061             return 0
       
  1062         rset = entity.related(self.rtype, self.role)
       
  1063         if self.target_etype:
       
  1064             return any(r for r in rset.description if r[0] == self.target_etype)
       
  1065         return rset and 1 or 0
       
  1066 
       
  1067 
       
  1068 class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
       
  1069     """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
       
  1070     for attributes of the selected class to get information which is otherwise
       
  1071     expected by the initializer.
       
  1072 
       
  1073     This is useful to predefine selector of an abstract class designed to be
       
  1074     customized.
       
  1075     """
       
  1076     def __init__(self, **kwargs):
       
  1077         super(partial_has_related_entities, self).__init__(None, None, None,
       
  1078                                                            **kwargs)
       
  1079 
       
  1080     def complete(self, cls):
       
  1081         self.rtype = cls.rtype
       
  1082         self.role = role(cls)
       
  1083         self.target_etype = getattr(cls, 'target_etype', None)
       
  1084 
       
  1085 
       
  1086 class has_permission(EntitySelector):
       
  1087     """Return non-zero score if request's user has the permission to do the
       
  1088     requested action on the entity. `action` is an entity schema action (eg one
       
  1089     of 'read', 'add', 'delete', 'update').
       
  1090 
       
  1091     Here are entity lookup / scoring rules:
       
  1092 
       
  1093     * if `entity` is specified, check permission is granted for this entity
       
  1094 
       
  1095     * elif `row` is specified, check permission is granted for the entity found
       
  1096       in the specified cell
       
  1097 
       
  1098     * else check permission is granted for each entity found in the column
       
  1099       specified specified by the `col` argument or in column 0
       
  1100     """
       
  1101     def __init__(self, action):
       
  1102         self.action = action
       
  1103 
       
  1104     # don't use EntitySelector.__call__ but this optimized implementation to
       
  1105     # avoid considering each entity when it's not necessary
       
  1106     @lltrace
       
  1107     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
  1108         if kwargs.get('entity'):
       
  1109             return self.score_entity(kwargs['entity'])
       
  1110         if rset is None:
       
  1111             return 0
       
  1112         if row is None:
       
  1113             score = 0
       
  1114             need_local_check = []
       
  1115             geteschema = req.vreg.schema.eschema
       
  1116             user = req.user
       
  1117             action = self.action
       
  1118             for etype in rset.column_types(0):
       
  1119                 if etype in BASE_TYPES:
       
  1120                     return 0
       
  1121                 eschema = geteschema(etype)
       
  1122                 if not user.matching_groups(eschema.get_groups(action)):
       
  1123                     if eschema.has_local_role(action):
       
  1124                         # have to ckeck local roles
       
  1125                         need_local_check.append(eschema)
       
  1126                         continue
       
  1127                     else:
       
  1128                         # even a local role won't be enough
       
  1129                         return 0
       
  1130                 score += 1
       
  1131             if need_local_check:
       
  1132                 # check local role for entities of necessary types
       
  1133                 for i, row in enumerate(rset):
       
  1134                     if not rset.description[i][col] in need_local_check:
       
  1135                         continue
       
  1136                     # micro-optimisation instead of calling self.score(req,
       
  1137                     # rset, i, col): rset may be large
       
  1138                     if not rset.get_entity(i, col).cw_has_perm(action):
       
  1139                         return 0
       
  1140                 score += 1
       
  1141             return score
       
  1142         return self.score(req, rset, row, col)
       
  1143 
       
  1144     def score_entity(self, entity):
       
  1145         if entity.cw_has_perm(self.action):
       
  1146             return 1
       
  1147         return 0
       
  1148 
       
  1149 
       
  1150 class has_add_permission(EClassSelector):
       
  1151     """Return 1 if request's user has the add permission on entity type
       
  1152     specified in the `etype` initializer argument, or according to entity found
       
  1153     in the input content if not specified.
       
  1154 
       
  1155     It also check that then entity type is not a strict subobject (e.g. may only
       
  1156     be used as a composed of another entity).
       
  1157 
       
  1158     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
  1159     class lookup / score rules according to the input context when `etype` is
       
  1160     not specified.
       
  1161     """
       
  1162     def __init__(self, etype=None, **kwargs):
       
  1163         super(has_add_permission, self).__init__(**kwargs)
       
  1164         self.etype = etype
       
  1165 
       
  1166     @lltrace
       
  1167     def __call__(self, cls, req, **kwargs):
       
  1168         if self.etype is None:
       
  1169             return super(has_add_permission, self).__call__(cls, req, **kwargs)
       
  1170         return self.score(cls, req, self.etype)
       
  1171 
       
  1172     def score_class(self, eclass, req):
       
  1173         eschema = eclass.e_schema
       
  1174         if eschema.final or eschema.is_subobject(strict=True) \
       
  1175                or not eschema.has_perm(req, 'add'):
       
  1176             return 0
       
  1177         return 1
       
  1178 
       
  1179 
       
  1180 class rql_condition(EntitySelector):
       
  1181     """Return non-zero score if arbitrary rql specified in `expression`
       
  1182     initializer argument return some results for entity found in the input
       
  1183     context. Returned score is the number of items returned by the rql
       
  1184     condition.
       
  1185 
       
  1186     `expression` is expected to be a string containing an rql expression, which
       
  1187     must use 'X' variable to represent the context entity and may use 'U' to
       
  1188     represent the request's user.
       
  1189 
       
  1190     .. warning::
       
  1191         If simply testing value of some attribute/relation of context entity (X),
       
  1192         you should rather use the :class:`score_entity` selector which will
       
  1193         benefit from the ORM's request entities cache.
       
  1194 
       
  1195     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
  1196     lookup / score rules according to the input context.
       
  1197     """
       
  1198     def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
       
  1199         super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
  1200         self.user_condition = user_condition
       
  1201         if user_condition:
       
  1202             rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
       
  1203         elif 'U' in frozenset(split_expression(expression)):
       
  1204             rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
       
  1205         else:
       
  1206             rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
       
  1207         self.rql = rql
       
  1208 
       
  1209     def __str__(self):
       
  1210         return '%s(%r)' % (self.__class__.__name__, self.rql)
       
  1211 
       
  1212     @lltrace
       
  1213     def __call__(self, cls, req, **kwargs):
       
  1214         if self.user_condition:
       
  1215             try:
       
  1216                 return req.execute(self.rql, {'u': req.user.eid})[0][0]
       
  1217             except Unauthorized:
       
  1218                 return 0
       
  1219         else:
       
  1220             return super(rql_condition, self).__call__(cls, req, **kwargs)
       
  1221 
       
  1222     def _score(self, req, eid):
       
  1223         try:
       
  1224             return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
       
  1225         except Unauthorized:
       
  1226             return 0
       
  1227 
       
  1228     def score(self, req, rset, row, col):
       
  1229         return self._score(req, rset[row][col])
       
  1230 
       
  1231     def score_entity(self, entity):
       
  1232         return self._score(entity._cw, entity.eid)
       
  1233 
       
  1234 
       
  1235 # workflow selectors ###########################################################
       
  1236 
       
  1237 class is_in_state(score_entity):
       
  1238     """Return 1 if entity is in one of the states given as argument list
       
  1239 
       
  1240     You should use this instead of your own :class:`score_entity` selector to
       
  1241     avoid some gotchas:
       
  1242 
       
  1243     * possible views gives a fake entity with no state
       
  1244     * you must use the latest tr info thru the workflow adapter for repository
       
  1245       side checking of the current state
       
  1246 
       
  1247     In debug mode, this selector can raise :exc:`ValueError` for unknown states names
       
  1248     (only checked on entities without a custom workflow)
       
  1249 
       
  1250     :rtype: int
       
  1251     """
       
  1252     def __init__(self, *expected):
       
  1253         assert expected, self
       
  1254         self.expected = frozenset(expected)
       
  1255         def score(entity, expected=self.expected):
       
  1256             adapted = entity.cw_adapt_to('IWorkflowable')
       
  1257             # in debug mode only (time consuming)
       
  1258             if entity._cw.vreg.config.debugmode:
       
  1259                 # validation can only be done for generic etype workflow because
       
  1260                 # expected transition list could have been changed for a custom
       
  1261                 # workflow (for the current entity)
       
  1262                 if not entity.custom_workflow:
       
  1263                     self._validate(adapted)
       
  1264             return self._score(adapted)
       
  1265         super(is_in_state, self).__init__(score)
       
  1266 
       
  1267     def _score(self, adapted):
       
  1268         trinfo = adapted.latest_trinfo()
       
  1269         if trinfo is None: # entity is probably in it's initial state
       
  1270             statename = adapted.state
       
  1271         else:
       
  1272             statename = trinfo.new_state.name
       
  1273         return statename in self.expected
       
  1274 
       
  1275     def _validate(self, adapted):
       
  1276         wf = adapted.current_workflow
       
  1277         valid = [n.name for n in wf.reverse_state_of]
       
  1278         unknown = sorted(self.expected.difference(valid))
       
  1279         if unknown:
       
  1280             raise ValueError("%s: unknown state(s): %s"
       
  1281                              % (wf.name, ",".join(unknown)))
       
  1282 
       
  1283     def __str__(self):
       
  1284         return '%s(%s)' % (self.__class__.__name__,
       
  1285                            ','.join(str(s) for s in self.expected))
       
  1286 
       
  1287 
       
  1288 def on_fire_transition(etype, tr_name, from_state_name=None):
       
  1289     """Return 1 when entity of the type `etype` is going through transition of
       
  1290     the name `tr_name`.
       
  1291 
       
  1292     If `from_state_name` is specified, this selector will also check the
       
  1293     incoming state.
       
  1294 
       
  1295     You should use this selector on 'after_add_entity' hook, since it's actually
       
  1296     looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
       
  1297     will reference the matching `TrInfo` entity, allowing to get all the
       
  1298     transition details (including the entity to which is applied the transition
       
  1299     but also its original state, transition, destination state, user...).
       
  1300 
       
  1301     See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
       
  1302     """
       
  1303     def match_etype_and_transition(trinfo):
       
  1304         # take care trinfo.transition is None when calling change_state
       
  1305         return (trinfo.transition and trinfo.transition.name == tr_name
       
  1306                 # is_instance() first two arguments are 'cls' (unused, so giving
       
  1307                 # None is fine) and the request/session
       
  1308                 and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
       
  1309 
       
  1310     return is_instance('TrInfo') & score_entity(match_etype_and_transition)
       
  1311 
       
  1312 
       
  1313 class match_transition(ExpectedValueSelector):
       
  1314     """Return 1 if `transition` argument is found in the input context which has
       
  1315     a `.name` attribute matching one of the expected names given to the
       
  1316     initializer.
       
  1317 
       
  1318     This selector is expected to be used to customise the status change form in
       
  1319     the web ui.
       
  1320     """
       
  1321     @lltrace
       
  1322     def __call__(self, cls, req, transition=None, **kwargs):
       
  1323         # XXX check this is a transition that apply to the object?
       
  1324         if transition is None:
       
  1325             treid = req.form.get('treid', None)
       
  1326             if treid:
       
  1327                 transition = req.entity_from_eid(treid)
       
  1328         if transition is not None and getattr(transition, 'name', None) in self.expected:
       
  1329             return 1
       
  1330         return 0
       
  1331 
       
  1332 
       
  1333 # logged user selectors ########################################################
       
  1334 
       
  1335 @objectify_selector
       
  1336 @lltrace
       
  1337 def no_cnx(cls, req, **kwargs):
       
  1338     """Return 1 if the web session has no connection set. This occurs when
       
  1339     anonymous access is not allowed and user isn't authenticated.
       
  1340 
       
  1341     May only be used on the web side, not on the data repository side.
       
  1342     """
       
  1343     if not req.cnx:
       
  1344         return 1
       
  1345     return 0
       
  1346 
       
  1347 @objectify_selector
       
  1348 @lltrace
       
  1349 def authenticated_user(cls, req, **kwargs):
       
  1350     """Return 1 if the user is authenticated (e.g. not the anonymous user).
       
  1351 
       
  1352     May only be used on the web side, not on the data repository side.
       
  1353     """
       
  1354     if req.session.anonymous_session:
       
  1355         return 0
       
  1356     return 1
       
  1357 
       
  1358 
       
  1359 # XXX == ~ authenticated_user()
       
  1360 def anonymous_user():
       
  1361     """Return 1 if the user is not authenticated (e.g. is the anonymous user).
       
  1362 
       
  1363     May only be used on the web side, not on the data repository side.
       
  1364     """
       
  1365     return ~ authenticated_user()
       
  1366 
       
  1367 class match_user_groups(ExpectedValueSelector):
       
  1368     """Return a non-zero score if request's user is in at least one of the
       
  1369     groups given as initializer argument. Returned score is the number of groups
       
  1370     in which the user is.
       
  1371 
       
  1372     If the special 'owners' group is given and `rset` is specified in the input
       
  1373     context:
       
  1374 
       
  1375     * if `row` is specified check the entity at the given `row`/`col` (default
       
  1376       to 0) is owned by the user
       
  1377 
       
  1378     * else check all entities in `col` (default to 0) are owned by the user
       
  1379     """
       
  1380 
       
  1381     @lltrace
       
  1382     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
  1383         if not getattr(req, 'cnx', True): # default to True for repo session instances
       
  1384             return 0
       
  1385         user = req.user
       
  1386         if user is None:
       
  1387             return int('guests' in self.expected)
       
  1388         score = user.matching_groups(self.expected)
       
  1389         if not score and 'owners' in self.expected and rset:
       
  1390             if row is not None:
       
  1391                 if not user.owns(rset[row][col]):
       
  1392                     return 0
       
  1393                 score = 1
       
  1394             else:
       
  1395                 score = all(user.owns(r[col]) for r in rset)
       
  1396         return score
       
  1397 
       
  1398 # Web request selectors ########################################################
       
  1399 
       
  1400 # XXX deprecate
       
  1401 @objectify_selector
       
  1402 @lltrace
       
  1403 def primary_view(cls, req, view=None, **kwargs):
       
  1404     """Return 1 if:
       
  1405 
       
  1406     * *no view is specified* in the input context
       
  1407 
       
  1408     * a view is specified and its `.is_primary()` method return True
       
  1409 
       
  1410     This selector is usually used by contextual components that only want to
       
  1411     appears for the primary view of an entity.
       
  1412     """
       
  1413     if view is not None and not view.is_primary():
       
  1414         return 0
       
  1415     return 1
       
  1416 
       
  1417 
       
  1418 @objectify_selector
       
  1419 @lltrace
       
  1420 def contextual(cls, req, view=None, **kwargs):
       
  1421     """Return 1 if view's contextual property is true"""
       
  1422     if view is not None and view.contextual:
       
  1423         return 1
       
  1424     return 0
       
  1425 
       
  1426 
       
  1427 class match_view(ExpectedValueSelector):
       
  1428     """Return 1 if a view is specified an as its registry id is in one of the
       
  1429     expected view id given to the initializer.
       
  1430     """
       
  1431     @lltrace
       
  1432     def __call__(self, cls, req, view=None, **kwargs):
       
  1433         if view is None or not view.__regid__ in self.expected:
       
  1434             return 0
       
  1435         return 1
       
  1436 
       
  1437 
       
  1438 class match_context(ExpectedValueSelector):
       
  1439 
       
  1440     @lltrace
       
  1441     def __call__(self, cls, req, context=None, **kwargs):
       
  1442         if not context in self.expected:
       
  1443             return 0
       
  1444         return 1
       
  1445 
       
  1446 
       
  1447 # XXX deprecate
       
  1448 @objectify_selector
       
  1449 @lltrace
       
  1450 def match_context_prop(cls, req, context=None, **kwargs):
       
  1451     """Return 1 if:
       
  1452 
       
  1453     * no `context` is specified in input context (take care to confusion, here
       
  1454       `context` refers to a string given as an argument to the input context...)
       
  1455 
       
  1456     * specified `context` is matching the context property value for the
       
  1457       appobject using this selector
       
  1458 
       
  1459     * the appobject's context property value is None
       
  1460 
       
  1461     This selector is usually used by contextual components that want to appears
       
  1462     in a configurable place.
       
  1463     """
       
  1464     if context is None:
       
  1465         return 1
       
  1466     propval = req.property_value('%s.%s.context' % (cls.__registry__,
       
  1467                                                     cls.__regid__))
       
  1468     if propval and context != propval:
       
  1469         return 0
       
  1470     return 1
       
  1471 
       
  1472 
       
  1473 class match_search_state(ExpectedValueSelector):
       
  1474     """Return 1 if the current request search state is in one of the expected
       
  1475     states given to the initializer.
       
  1476 
       
  1477     Known search states are either 'normal' or 'linksearch' (eg searching for an
       
  1478     object to create a relation with another).
       
  1479 
       
  1480     This selector is usually used by action that want to appears or not according
       
  1481     to the ui search state.
       
  1482     """
       
  1483 
       
  1484     @lltrace
       
  1485     def __call__(self, cls, req, **kwargs):
       
  1486         try:
       
  1487             if not req.search_state[0] in self.expected:
       
  1488                 return 0
       
  1489         except AttributeError:
       
  1490             return 1 # class doesn't care about search state, accept it
       
  1491         return 1
       
  1492 
       
  1493 
       
  1494 class match_form_params(ExpectedValueSelector):
       
  1495     """Return non-zero score if parameter names specified as initializer
       
  1496     arguments are specified in request's form parameters.
       
  1497 
       
  1498     Return a score corresponding to the number of expected parameters.
       
  1499 
       
  1500     When multiple parameters are expected, all of them should be found in
       
  1501     the input context unless `mode` keyword argument is given to 'any',
       
  1502     in which case a single matching parameter is enough.
       
  1503     """
       
  1504 
       
  1505     def _values_set(self, cls, req, **kwargs):
       
  1506         return frozenset(req.form)
       
  1507 
       
  1508 
       
  1509 class match_edited_type(ExpectedValueSelector):
       
  1510     """return non-zero if main edited entity type is the one specified as
       
  1511     initializer argument, or is among initializer arguments if `mode` == 'any'.
       
  1512     """
       
  1513 
       
  1514     def _values_set(self, cls, req, **kwargs):
       
  1515         try:
       
  1516             return frozenset((req.form['__type:%s' % req.form['__maineid']],))
       
  1517         except KeyError:
       
  1518             return frozenset()
       
  1519 
       
  1520 
       
  1521 class match_form_id(ExpectedValueSelector):
       
  1522     """return non-zero if request form identifier is the one specified as
       
  1523     initializer argument, or is among initializer arguments if `mode` == 'any'.
       
  1524     """
       
  1525 
       
  1526     def _values_set(self, cls, req, **kwargs):
       
  1527         try:
       
  1528             return frozenset((req.form['__form_id'],))
       
  1529         except KeyError:
       
  1530             return frozenset()
       
  1531 
       
  1532 
       
  1533 class specified_etype_implements(is_instance):
       
  1534     """Return non-zero score if the entity type specified by an 'etype' key
       
  1535     searched in (by priority) input context kwargs and request form parameters
       
  1536     match a known entity type (case insensitivly), and it's associated entity
       
  1537     class is of one of the type(s) given to the initializer. If multiple
       
  1538     arguments are given, matching one of them is enough.
       
  1539 
       
  1540     .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
       
  1541               should be given as string and the score will reflect class
       
  1542               proximity so the most specific object will be selected.
       
  1543 
       
  1544     This selector is usually used by views holding entity creation forms (since
       
  1545     we've no result set to work on).
       
  1546     """
       
  1547 
       
  1548     @lltrace
       
  1549     def __call__(self, cls, req, **kwargs):
       
  1550         try:
       
  1551             etype = kwargs['etype']
       
  1552         except KeyError:
       
  1553             try:
       
  1554                 etype = req.form['etype']
       
  1555             except KeyError:
       
  1556                 return 0
       
  1557             else:
       
  1558                 # only check this is a known type if etype comes from req.form,
       
  1559                 # else we want the error to propagate
       
  1560                 try:
       
  1561                     etype = req.vreg.case_insensitive_etypes[etype.lower()]
       
  1562                     req.form['etype'] = etype
       
  1563                 except KeyError:
       
  1564                     return 0
       
  1565         score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
       
  1566         if score:
       
  1567             eschema = req.vreg.schema.eschema(etype)
       
  1568             if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
       
  1569                 return score
       
  1570         return 0
       
  1571 
       
  1572 
       
  1573 class attribute_edited(EntitySelector):
       
  1574     """Scores if the specified attribute has been edited This is useful for
       
  1575     selection of forms by the edit controller.
       
  1576 
       
  1577     The initial use case is on a form, in conjunction with match_transition,
       
  1578     which will not score at edit time::
       
  1579 
       
  1580      is_instance('Version') & (match_transition('ready') |
       
  1581                                attribute_edited('publication_date'))
       
  1582     """
       
  1583     def __init__(self, attribute, once_is_enough=None, mode='all'):
       
  1584         super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
  1585         self._attribute = attribute
       
  1586 
       
  1587     def score_entity(self, entity):
       
  1588         return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
       
  1589 
       
  1590 
       
  1591 # Other selectors ##############################################################
       
  1592 
       
  1593 class match_exception(ExpectedValueSelector):
       
  1594     """Return 1 if exception given as `exc` in the input context is an instance
       
  1595     of one of the class given on instanciation of this predicate.
       
  1596     """
       
  1597     def __init__(self, *expected):
       
  1598         assert expected, self
       
  1599         # we want a tuple, not a set as done in the parent class
       
  1600         self.expected = expected
       
  1601 
       
  1602     @lltrace
       
  1603     def __call__(self, cls, req, exc=None, **kwargs):
       
  1604         if exc is not None and isinstance(exc, self.expected):
       
  1605             return 1
       
  1606         return 0
       
  1607 
       
  1608 
       
  1609 @objectify_selector
       
  1610 def debug_mode(cls, req, rset=None, **kwargs):
       
  1611     """Return 1 if running in debug mode."""
       
  1612     return req.vreg.config.debugmode and 1 or 0
       
  1613 
       
  1614 
       
  1615 ## deprecated stuff ############################################################
       
  1616 
    39 
  1617 
    40 
  1618 class on_transition(is_in_state):
    41 class on_transition(is_in_state):
  1619     """Return 1 if entity is in one of the transitions given as argument list
    42     """Return 1 if entity is in one of the transitions given as argument list
  1620 
    43 
  1621     Especially useful to match passed transition to enable notifications when
    44     Especially useful to match passed transition to enable notifications when
  1622     your workflow allows several transition to the same states.
    45     your workflow allows several transition to the same states.
  1623 
    46 
  1624     Note that if workflow `change_state` adapter method is used, this selector
    47     Note that if workflow `change_state` adapter method is used, this predicate
  1625     will not be triggered.
    48     will not be triggered.
  1626 
    49 
  1627     You should use this instead of your own :class:`score_entity` selector to
    50     You should use this instead of your own :class:`score_entity` predicate to
  1628     avoid some gotchas:
    51     avoid some gotchas:
  1629 
    52 
  1630     * possible views gives a fake entity with no state
    53     * possible views gives a fake entity with no state
  1631     * you must use the latest tr info thru the workflow adapter for repository
    54     * you must use the latest tr info thru the workflow adapter for repository
  1632       side checking of the current state
    55       side checking of the current state
  1633 
    56 
  1634     In debug mode, this selector can raise:
    57     In debug mode, this predicate can raise:
  1635     :raises: :exc:`ValueError` for unknown transition names
    58     :raises: :exc:`ValueError` for unknown transition names
  1636         (etype workflow only not checked in custom workflow)
    59         (etype workflow only not checked in custom workflow)
  1637 
    60 
  1638     :rtype: int
    61     :rtype: int
  1639     """
    62     """
  1653         unknown = sorted(self.expected.difference(valid))
    76         unknown = sorted(self.expected.difference(valid))
  1654         if unknown:
    77         if unknown:
  1655             raise ValueError("%s: unknown transition(s): %s"
    78             raise ValueError("%s: unknown transition(s): %s"
  1656                              % (wf.name, ",".join(unknown)))
    79                              % (wf.name, ",".join(unknown)))
  1657 
    80 
       
    81 
  1658 entity_implements = class_renamed('entity_implements', is_instance)
    82 entity_implements = class_renamed('entity_implements', is_instance)
  1659 
    83 
  1660 class _but_etype(EntitySelector):
    84 class _but_etype(EntityPredicate):
  1661     """accept if the given entity types are not found in the result set.
    85     """accept if the given entity types are not found in the result set.
  1662 
    86 
  1663     See `EntitySelector` documentation for behaviour when row is not specified.
    87     See `EntityPredicate` documentation for behaviour when row is not specified.
  1664 
    88 
  1665     :param *etypes: entity types (`basestring`) which should be refused
    89     :param *etypes: entity types (`basestring`) which should be refused
  1666     """
    90     """
  1667     def __init__(self, *etypes):
    91     def __init__(self, *etypes):
  1668         super(_but_etype, self).__init__()
    92         super(_but_etype, self).__init__()
  1673             return 0
    97             return 0
  1674         return 1
    98         return 1
  1675 
    99 
  1676 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
   100 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
  1677 
   101 
  1678 
   102 # XXX deprecated the one_* variants of predicates below w/ multi_xxx(nb=1)?
  1679 # XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
       
  1680 #     take care at the implementation though (looking for the 'row' argument's
   103 #     take care at the implementation though (looking for the 'row' argument's
  1681 #     value)
   104 #     value)
  1682 two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
   105 two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
  1683 two_cols_rset = class_renamed('two_cols_rset', multi_columns_rset)
   106 two_cols_rset = class_renamed('two_cols_rset', multi_columns_rset)
  1684 two_etypes_rset = class_renamed('two_etypes_rset', multi_etypes_rset)
   107 two_etypes_rset = class_renamed('two_etypes_rset', multi_etypes_rset)