selectors.py
changeset 8190 2a3c1b787688
parent 8034 b07d61090706
child 8220 e4ea285510e5
equal deleted inserted replaced
8189:2ee0ef069fa7 8190:2a3c1b787688
     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] moved to cubicweb.predicates', DeprecationWarning, stacklevel=2)
   102 ~~~~~~~~~~~~~~~~~~~~~~
       
   103 
    27 
   104 Selectors are to be used whenever arises the need of dispatching on the shape or
    28 # XXX pre 3.15 bw compat
   105 content of a result set or whatever else context (value in request form params,
    29 from cubicweb.appobject import (objectify_selector, traced_selection,
   106 authenticated user groups, etc...). That is, almost all the time.
    30                                 lltrace, yes)
   107 
    31 
   108 Here is a quick example:
    32 ExpectedValueSelector = class_renamed('ExpectedValueSelector', ExpectedValuePredicate)
       
    33 EClassSelector = class_renamed('EClassSelector', EClassPredicate)
       
    34 EntitySelector = class_renamed('EntitySelector', EntityPredicate)
   109 
    35 
   110 .. sourcecode:: python
    36 # 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
       
   645         return rset and self.match_expected(len(rset.rows[0])) or 0
       
   646 
       
   647 
       
   648 class paginated_rset(Selector):
       
   649     """Return 1 or more for result set with more rows than one or more page
       
   650     size.  You can specify expected number of pages to the initializer (default
       
   651     to one), and you'll get that number of pages as score if the result set is
       
   652     big enough.
       
   653 
       
   654     Page size is searched in (respecting order):
       
   655     * a `page_size` argument
       
   656     * a `page_size` form parameters
       
   657     * the `navigation.page-size` property (see :ref:`PersistentProperties`)
       
   658     """
       
   659     def __init__(self, nbpages=1):
       
   660         assert nbpages > 0
       
   661         self.nbpages = nbpages
       
   662 
       
   663     @lltrace
       
   664     def __call__(self, cls, req, rset=None, **kwargs):
       
   665         if rset is None:
       
   666             return 0
       
   667         page_size = kwargs.get('page_size')
       
   668         if page_size is None:
       
   669             page_size = req.form.get('page_size')
       
   670             if page_size is None:
       
   671                 page_size = req.property_value('navigation.page-size')
       
   672             else:
       
   673                 page_size = int(page_size)
       
   674         if rset.rowcount <= (page_size*self.nbpages):
       
   675             return 0
       
   676         return self.nbpages
       
   677 
       
   678 
       
   679 @objectify_selector
       
   680 @lltrace
       
   681 def sorted_rset(cls, req, rset=None, **kwargs):
       
   682     """Return 1 for sorted result set (e.g. from an RQL query containing an
       
   683     ORDERBY clause), with exception that it will return 0 if the rset is
       
   684     'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
       
   685     """
       
   686     if rset is None:
       
   687         return 0
       
   688     selects = rset.syntax_tree().children
       
   689     if (len(selects) > 1 or
       
   690         not selects[0].orderby or
       
   691         (isinstance(selects[0].orderby[0].term, Function) and
       
   692          selects[0].orderby[0].term.name == 'FTIRANK')
       
   693         ):
       
   694         return 0
       
   695     return 2
       
   696 
       
   697 
       
   698 # XXX == multi_etypes_rset(1)
       
   699 @objectify_selector
       
   700 @lltrace
       
   701 def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
       
   702     """Return 1 if the result set contains entities which are all of the same
       
   703     type in the column specified by the `col` argument of the input context, or
       
   704     in column 0.
       
   705     """
       
   706     if rset is None:
       
   707         return 0
       
   708     if len(rset.column_types(col)) != 1:
       
   709         return 0
       
   710     return 1
       
   711 
       
   712 
       
   713 class multi_etypes_rset(multi_lines_rset):
       
   714     """If `nb` is specified, return 1 if the result set contains `nb` different
       
   715     types of entities in the column specified by the `col` argument of the input
       
   716     context, or in column 0. If `nb` is None, return 1 if the result set contains
       
   717     *at least* two different types of entities.
       
   718     """
       
   719 
       
   720     @lltrace
       
   721     def __call__(self, cls, req, rset=None, col=0, **kwargs):
       
   722         # 'or 0' since we *must not* return None
       
   723         return rset and self.match_expected(len(rset.column_types(col))) or 0
       
   724 
       
   725 
       
   726 @objectify_selector
       
   727 def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
       
   728     """Return positive score if the result set at the specified row / col
       
   729     contains the eid of the logged user.
       
   730     """
       
   731     if rset is None:
       
   732         return 0
       
   733     return req.user.eid == rset[row or 0][col]
       
   734 
       
   735 
       
   736 # entity selectors #############################################################
       
   737 
       
   738 class non_final_entity(EClassSelector):
       
   739     """Return 1 for entity of a non final entity type(s). Remember, "final"
       
   740     entity types are String, Int, etc... This is equivalent to
       
   741     `is_instance('Any')` but more optimized.
       
   742 
       
   743     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   744     class lookup / score rules according to the input context.
       
   745     """
       
   746     def score(self, cls, req, etype):
       
   747         if etype in BASE_TYPES:
       
   748             return 0
       
   749         return 1
       
   750 
       
   751     def score_class(self, eclass, req):
       
   752         return 1 # necessarily true if we're there
       
   753 
       
   754 
       
   755 class implements(EClassSelector):
       
   756     """Return non-zero score for entity that are of the given type(s) or
       
   757     implements at least one of the given interface(s). If multiple arguments are
       
   758     given, matching one of them is enough.
       
   759 
       
   760     Entity types should be given as string, the corresponding class will be
       
   761     fetched from the entity types registry at selection time.
       
   762 
       
   763     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   764     class lookup / score rules according to the input context.
       
   765 
       
   766     .. note:: when interface is an entity class, the score will reflect class
       
   767               proximity so the most specific object will be selected.
       
   768 
       
   769     .. note:: deprecated in cubicweb >= 3.9, use either
       
   770               :class:`~cubicweb.selectors.is_instance` or
       
   771               :class:`~cubicweb.selectors.adaptable`.
       
   772     """
       
   773 
       
   774     def __init__(self, *expected_ifaces, **kwargs):
       
   775         emit_warn = kwargs.pop('warn', True)
       
   776         super(implements, self).__init__(**kwargs)
       
   777         self.expected_ifaces = expected_ifaces
       
   778         if emit_warn:
       
   779             warn('[3.9] implements selector is deprecated, use either '
       
   780                  'is_instance or adaptable', DeprecationWarning, stacklevel=2)
       
   781 
       
   782     def __str__(self):
       
   783         return '%s(%s)' % (self.__class__.__name__,
       
   784                            ','.join(str(s) for s in self.expected_ifaces))
       
   785 
       
   786     def score_class(self, eclass, req):
       
   787         score = 0
       
   788         etypesreg = req.vreg['etypes']
       
   789         for iface in self.expected_ifaces:
       
   790             if isinstance(iface, basestring):
       
   791                 # entity type
       
   792                 try:
       
   793                     iface = etypesreg.etype_class(iface)
       
   794                 except KeyError:
       
   795                     continue # entity type not in the schema
       
   796             score += score_interface(etypesreg, eclass, iface)
       
   797         return score
       
   798 
       
   799 def _reset_is_instance_cache(vreg):
       
   800     vreg._is_instance_selector_cache = {}
       
   801 
       
   802 CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
       
   803 
       
   804 class is_instance(EClassSelector):
       
   805     """Return non-zero score for entity that is an instance of the one of given
       
   806     type(s). If multiple arguments are given, matching one of them is enough.
       
   807 
       
   808     Entity types should be given as string, the corresponding class will be
       
   809     fetched from the registry at selection time.
       
   810 
       
   811     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
   812     class lookup / score rules according to the input context.
       
   813 
       
   814     .. note:: the score will reflect class proximity so the most specific object
       
   815               will be selected.
       
   816     """
       
   817 
       
   818     def __init__(self, *expected_etypes, **kwargs):
       
   819         super(is_instance, self).__init__(**kwargs)
       
   820         self.expected_etypes = expected_etypes
       
   821         for etype in self.expected_etypes:
       
   822             assert isinstance(etype, basestring), etype
       
   823 
       
   824     def __str__(self):
       
   825         return '%s(%s)' % (self.__class__.__name__,
       
   826                            ','.join(str(s) for s in self.expected_etypes))
       
   827 
       
   828     def score_class(self, eclass, req):
       
   829         # cache on vreg to avoid reloading issues
       
   830         try:
       
   831             cache = req.vreg._is_instance_selector_cache
       
   832         except AttributeError:
       
   833             # XXX 'before-registry-reset' not called for db-api connections
       
   834             cache = req.vreg._is_instance_selector_cache = {}
       
   835         try:
       
   836             expected_eclasses = cache[self]
       
   837         except KeyError:
       
   838             # turn list of entity types as string into a list of
       
   839             #  (entity class, parent classes)
       
   840             etypesreg = req.vreg['etypes']
       
   841             expected_eclasses = cache[self] = []
       
   842             for etype in self.expected_etypes:
       
   843                 try:
       
   844                     expected_eclasses.append(etypesreg.etype_class(etype))
       
   845                 except KeyError:
       
   846                     continue # entity type not in the schema
       
   847         parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
       
   848         score = 0
       
   849         for expectedcls in expected_eclasses:
       
   850             # adjust score according to class proximity
       
   851             if expectedcls is eclass:
       
   852                 score += len(parents) + 4
       
   853             elif expectedcls is any: # Any
       
   854                 score += 1
       
   855             else:
       
   856                 for index, basecls in enumerate(reversed(parents)):
       
   857                     if expectedcls is basecls:
       
   858                         score += index + 3
       
   859                         break
       
   860         return score
       
   861 
       
   862 
       
   863 class score_entity(EntitySelector):
       
   864     """Return score according to an arbitrary function given as argument which
       
   865     will be called with input content entity as argument.
       
   866 
       
   867     This is a very useful selector that will usually interest you since it
       
   868     allows a lot of things without having to write a specific selector.
       
   869 
       
   870     The function can return arbitrary value which will be casted to an integer
       
   871     value at the end.
       
   872 
       
   873     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
   874     lookup / score rules according to the input context.
       
   875     """
       
   876     def __init__(self, scorefunc, once_is_enough=None, mode='all'):
       
   877         super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
   878         def intscore(*args, **kwargs):
       
   879             score = scorefunc(*args, **kwargs)
       
   880             if not score:
       
   881                 return 0
       
   882             if isinstance(score, (int, long)):
       
   883                 return score
       
   884             return 1
       
   885         self.score_entity = intscore
       
   886 
       
   887 
       
   888 class has_mimetype(EntitySelector):
       
   889     """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
       
   890 
       
   891     You can give 'image/' to match any image for instance, or 'image/png' to match
       
   892     only PNG images.
       
   893     """
       
   894     def __init__(self, mimetype, once_is_enough=None, mode='all'):
       
   895         super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
   896         self.mimetype = mimetype
       
   897 
       
   898     def score_entity(self, entity):
       
   899         idownloadable = entity.cw_adapt_to('IDownloadable')
       
   900         if idownloadable is None:
       
   901             return 0
       
   902         mt = idownloadable.download_content_type()
       
   903         if not (mt and mt.startswith(self.mimetype)):
       
   904             return 0
       
   905         return 1
       
   906 
       
   907 
       
   908 class relation_possible(EntitySelector):
       
   909     """Return 1 for entity that supports the relation, provided that the
       
   910     request's user may do some `action` on it (see below).
       
   911 
       
   912     The relation is specified by the following initializer arguments:
       
   913 
       
   914     * `rtype`, the name of the relation
       
   915 
       
   916     * `role`, the role of the entity in the relation, either 'subject' or
       
   917       'object', default to 'subject'
       
   918 
       
   919     * `target_etype`, optional name of an entity type that should be supported
       
   920       at the other end of the relation
       
   921 
       
   922     * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
       
   923       default to 'read') which must be granted to the user, else a 0 score will
       
   924       be returned. Give None if you don't want any permission checking.
       
   925 
       
   926     * `strict`, boolean (default to False) telling what to do when the user has
       
   927       not globally the permission for the action (eg the action is not granted
       
   928       to one of the user's groups)
       
   929 
       
   930       - when strict is False, if there are some local role defined for this
       
   931         action (e.g. using rql expressions), then the permission will be
       
   932         considered as granted
       
   933 
       
   934       - when strict is True, then the permission will be actually checked for
       
   935         each entity
       
   936 
       
   937     Setting `strict` to True impacts performance for large result set since
       
   938     you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour
       
   939     while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
       
   940     one. See those classes documentation for entity lookup / score rules
       
   941     according to the input context.
       
   942     """
       
   943 
       
   944     def __init__(self, rtype, role='subject', target_etype=None,
       
   945                  action='read', strict=False, **kwargs):
       
   946         super(relation_possible, self).__init__(**kwargs)
       
   947         self.rtype = rtype
       
   948         self.role = role
       
   949         self.target_etype = target_etype
       
   950         self.action = action
       
   951         self.strict = strict
       
   952 
       
   953     # hack hack hack
       
   954     def __call__(self, cls, req, **kwargs):
       
   955         # hack hack hack
       
   956         if self.strict:
       
   957             return EntitySelector.__call__(self, cls, req, **kwargs)
       
   958         return EClassSelector.__call__(self, cls, req, **kwargs)
       
   959 
       
   960     def score(self, *args):
       
   961         if self.strict:
       
   962             return EntitySelector.score(self, *args)
       
   963         return EClassSelector.score(self, *args)
       
   964 
       
   965     def _get_rschema(self, eclass):
       
   966         eschema = eclass.e_schema
       
   967         try:
       
   968             if self.role == 'object':
       
   969                 return eschema.objrels[self.rtype]
       
   970             else:
       
   971                 return eschema.subjrels[self.rtype]
       
   972         except KeyError:
       
   973             return None
       
   974 
       
   975     def score_class(self, eclass, req):
       
   976         rschema = self._get_rschema(eclass)
       
   977         if rschema is None:
       
   978             return 0 # relation not supported
       
   979         eschema = eclass.e_schema
       
   980         if self.target_etype is not None:
       
   981             try:
       
   982                 rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
       
   983             except KeyError:
       
   984                 return 0
       
   985             if self.action and not rdef.may_have_permission(self.action, req):
       
   986                 return 0
       
   987             teschema = req.vreg.schema.eschema(self.target_etype)
       
   988             if not teschema.may_have_permission('read', req):
       
   989                 return 0
       
   990         elif self.action:
       
   991             return rschema.may_have_permission(self.action, req, eschema, self.role)
       
   992         return 1
       
   993 
       
   994     def score_entity(self, entity):
       
   995         rschema = self._get_rschema(entity)
       
   996         if rschema is None:
       
   997             return 0 # relation not supported
       
   998         if self.action:
       
   999             if self.target_etype is not None:
       
  1000                 rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
       
  1001             if self.role == 'subject':
       
  1002                 if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
       
  1003                     return 0
       
  1004             elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
       
  1005                 return 0
       
  1006         if self.target_etype is not None:
       
  1007             req = entity._cw
       
  1008             teschema = req.vreg.schema.eschema(self.target_etype)
       
  1009             if not teschema.may_have_permission('read', req):
       
  1010                 return 0
       
  1011         return 1
       
  1012 
       
  1013 
       
  1014 class partial_relation_possible(PartialSelectorMixIn, relation_possible):
       
  1015     """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for
       
  1016     attributes of the selected class to get information which is otherwise
       
  1017     expected by the initializer, except for `action` and `strict` which are kept
       
  1018     as initializer arguments.
       
  1019 
       
  1020     This is useful to predefine selector of an abstract class designed to be
       
  1021     customized.
       
  1022     """
       
  1023     def __init__(self, action='read', **kwargs):
       
  1024         super(partial_relation_possible, self).__init__(None, None, None,
       
  1025                                                         action, **kwargs)
       
  1026 
       
  1027     def complete(self, cls):
       
  1028         self.rtype = cls.rtype
       
  1029         self.role = role(cls)
       
  1030         self.target_etype = getattr(cls, 'target_etype', None)
       
  1031 
       
  1032 
       
  1033 class has_related_entities(EntitySelector):
       
  1034     """Return 1 if entity support the specified relation and has some linked
       
  1035     entities by this relation , optionaly filtered according to the specified
       
  1036     target type.
       
  1037 
       
  1038     The relation is specified by the following initializer arguments:
       
  1039 
       
  1040     * `rtype`, the name of the relation
       
  1041 
       
  1042     * `role`, the role of the entity in the relation, either 'subject' or
       
  1043       'object', default to 'subject'.
       
  1044 
       
  1045     * `target_etype`, optional name of an entity type that should be found
       
  1046       at the other end of the relation
       
  1047 
       
  1048     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
  1049     lookup / score rules according to the input context.
       
  1050     """
       
  1051     def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
       
  1052         super(has_related_entities, self).__init__(**kwargs)
       
  1053         self.rtype = rtype
       
  1054         self.role = role
       
  1055         self.target_etype = target_etype
       
  1056 
       
  1057     def score_entity(self, entity):
       
  1058         relpossel = relation_possible(self.rtype, self.role, self.target_etype)
       
  1059         if not relpossel.score_class(entity.__class__, entity._cw):
       
  1060             return 0
       
  1061         rset = entity.related(self.rtype, self.role)
       
  1062         if self.target_etype:
       
  1063             return any(r for r in rset.description if r[0] == self.target_etype)
       
  1064         return rset and 1 or 0
       
  1065 
       
  1066 
       
  1067 class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
       
  1068     """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
       
  1069     for attributes of the selected class to get information which is otherwise
       
  1070     expected by the initializer.
       
  1071 
       
  1072     This is useful to predefine selector of an abstract class designed to be
       
  1073     customized.
       
  1074     """
       
  1075     def __init__(self, **kwargs):
       
  1076         super(partial_has_related_entities, self).__init__(None, None, None,
       
  1077                                                            **kwargs)
       
  1078 
       
  1079     def complete(self, cls):
       
  1080         self.rtype = cls.rtype
       
  1081         self.role = role(cls)
       
  1082         self.target_etype = getattr(cls, 'target_etype', None)
       
  1083 
       
  1084 
       
  1085 class has_permission(EntitySelector):
       
  1086     """Return non-zero score if request's user has the permission to do the
       
  1087     requested action on the entity. `action` is an entity schema action (eg one
       
  1088     of 'read', 'add', 'delete', 'update').
       
  1089 
       
  1090     Here are entity lookup / scoring rules:
       
  1091 
       
  1092     * if `entity` is specified, check permission is granted for this entity
       
  1093 
       
  1094     * elif `row` is specified, check permission is granted for the entity found
       
  1095       in the specified cell
       
  1096 
       
  1097     * else check permission is granted for each entity found in the column
       
  1098       specified specified by the `col` argument or in column 0
       
  1099     """
       
  1100     def __init__(self, action):
       
  1101         self.action = action
       
  1102 
       
  1103     # don't use EntitySelector.__call__ but this optimized implementation to
       
  1104     # avoid considering each entity when it's not necessary
       
  1105     @lltrace
       
  1106     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
  1107         if kwargs.get('entity'):
       
  1108             return self.score_entity(kwargs['entity'])
       
  1109         if rset is None:
       
  1110             return 0
       
  1111         if row is None:
       
  1112             score = 0
       
  1113             need_local_check = []
       
  1114             geteschema = req.vreg.schema.eschema
       
  1115             user = req.user
       
  1116             action = self.action
       
  1117             for etype in rset.column_types(0):
       
  1118                 if etype in BASE_TYPES:
       
  1119                     return 0
       
  1120                 eschema = geteschema(etype)
       
  1121                 if not user.matching_groups(eschema.get_groups(action)):
       
  1122                     if eschema.has_local_role(action):
       
  1123                         # have to ckeck local roles
       
  1124                         need_local_check.append(eschema)
       
  1125                         continue
       
  1126                     else:
       
  1127                         # even a local role won't be enough
       
  1128                         return 0
       
  1129                 score += 1
       
  1130             if need_local_check:
       
  1131                 # check local role for entities of necessary types
       
  1132                 for i, row in enumerate(rset):
       
  1133                     if not rset.description[i][col] in need_local_check:
       
  1134                         continue
       
  1135                     # micro-optimisation instead of calling self.score(req,
       
  1136                     # rset, i, col): rset may be large
       
  1137                     if not rset.get_entity(i, col).cw_has_perm(action):
       
  1138                         return 0
       
  1139                 score += 1
       
  1140             return score
       
  1141         return self.score(req, rset, row, col)
       
  1142 
       
  1143     def score_entity(self, entity):
       
  1144         if entity.cw_has_perm(self.action):
       
  1145             return 1
       
  1146         return 0
       
  1147 
       
  1148 
       
  1149 class has_add_permission(EClassSelector):
       
  1150     """Return 1 if request's user has the add permission on entity type
       
  1151     specified in the `etype` initializer argument, or according to entity found
       
  1152     in the input content if not specified.
       
  1153 
       
  1154     It also check that then entity type is not a strict subobject (e.g. may only
       
  1155     be used as a composed of another entity).
       
  1156 
       
  1157     See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
       
  1158     class lookup / score rules according to the input context when `etype` is
       
  1159     not specified.
       
  1160     """
       
  1161     def __init__(self, etype=None, **kwargs):
       
  1162         super(has_add_permission, self).__init__(**kwargs)
       
  1163         self.etype = etype
       
  1164 
       
  1165     @lltrace
       
  1166     def __call__(self, cls, req, **kwargs):
       
  1167         if self.etype is None:
       
  1168             return super(has_add_permission, self).__call__(cls, req, **kwargs)
       
  1169         return self.score(cls, req, self.etype)
       
  1170 
       
  1171     def score_class(self, eclass, req):
       
  1172         eschema = eclass.e_schema
       
  1173         if eschema.final or eschema.is_subobject(strict=True) \
       
  1174                or not eschema.has_perm(req, 'add'):
       
  1175             return 0
       
  1176         return 1
       
  1177 
       
  1178 
       
  1179 class rql_condition(EntitySelector):
       
  1180     """Return non-zero score if arbitrary rql specified in `expression`
       
  1181     initializer argument return some results for entity found in the input
       
  1182     context. Returned score is the number of items returned by the rql
       
  1183     condition.
       
  1184 
       
  1185     `expression` is expected to be a string containing an rql expression, which
       
  1186     must use 'X' variable to represent the context entity and may use 'U' to
       
  1187     represent the request's user.
       
  1188 
       
  1189     .. warning::
       
  1190         If simply testing value of some attribute/relation of context entity (X),
       
  1191         you should rather use the :class:`score_entity` selector which will
       
  1192         benefit from the ORM's request entities cache.
       
  1193 
       
  1194     See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
       
  1195     lookup / score rules according to the input context.
       
  1196     """
       
  1197     def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
       
  1198         super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
  1199         self.user_condition = user_condition
       
  1200         if user_condition:
       
  1201             rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
       
  1202         elif 'U' in frozenset(split_expression(expression)):
       
  1203             rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
       
  1204         else:
       
  1205             rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
       
  1206         self.rql = rql
       
  1207 
       
  1208     def __str__(self):
       
  1209         return '%s(%r)' % (self.__class__.__name__, self.rql)
       
  1210 
       
  1211     @lltrace
       
  1212     def __call__(self, cls, req, **kwargs):
       
  1213         if self.user_condition:
       
  1214             try:
       
  1215                 return req.execute(self.rql, {'u': req.user.eid})[0][0]
       
  1216             except Unauthorized:
       
  1217                 return 0
       
  1218         else:
       
  1219             return super(rql_condition, self).__call__(cls, req, **kwargs)
       
  1220 
       
  1221     def _score(self, req, eid):
       
  1222         try:
       
  1223             return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
       
  1224         except Unauthorized:
       
  1225             return 0
       
  1226 
       
  1227     def score(self, req, rset, row, col):
       
  1228         return self._score(req, rset[row][col])
       
  1229 
       
  1230     def score_entity(self, entity):
       
  1231         return self._score(entity._cw, entity.eid)
       
  1232 
       
  1233 
       
  1234 # workflow selectors ###########################################################
       
  1235 
       
  1236 class is_in_state(score_entity):
       
  1237     """Return 1 if entity is in one of the states given as argument list
       
  1238 
       
  1239     You should use this instead of your own :class:`score_entity` selector to
       
  1240     avoid some gotchas:
       
  1241 
       
  1242     * possible views gives a fake entity with no state
       
  1243     * you must use the latest tr info thru the workflow adapter for repository
       
  1244       side checking of the current state
       
  1245 
       
  1246     In debug mode, this selector can raise :exc:`ValueError` for unknown states names
       
  1247     (only checked on entities without a custom workflow)
       
  1248 
       
  1249     :rtype: int
       
  1250     """
       
  1251     def __init__(self, *expected):
       
  1252         assert expected, self
       
  1253         self.expected = frozenset(expected)
       
  1254         def score(entity, expected=self.expected):
       
  1255             adapted = entity.cw_adapt_to('IWorkflowable')
       
  1256             # in debug mode only (time consuming)
       
  1257             if entity._cw.vreg.config.debugmode:
       
  1258                 # validation can only be done for generic etype workflow because
       
  1259                 # expected transition list could have been changed for a custom
       
  1260                 # workflow (for the current entity)
       
  1261                 if not entity.custom_workflow:
       
  1262                     self._validate(adapted)
       
  1263             return self._score(adapted)
       
  1264         super(is_in_state, self).__init__(score)
       
  1265 
       
  1266     def _score(self, adapted):
       
  1267         trinfo = adapted.latest_trinfo()
       
  1268         if trinfo is None: # entity is probably in it's initial state
       
  1269             statename = adapted.state
       
  1270         else:
       
  1271             statename = trinfo.new_state.name
       
  1272         return statename in self.expected
       
  1273 
       
  1274     def _validate(self, adapted):
       
  1275         wf = adapted.current_workflow
       
  1276         valid = [n.name for n in wf.reverse_state_of]
       
  1277         unknown = sorted(self.expected.difference(valid))
       
  1278         if unknown:
       
  1279             raise ValueError("%s: unknown state(s): %s"
       
  1280                              % (wf.name, ",".join(unknown)))
       
  1281 
       
  1282     def __str__(self):
       
  1283         return '%s(%s)' % (self.__class__.__name__,
       
  1284                            ','.join(str(s) for s in self.expected))
       
  1285 
       
  1286 
       
  1287 def on_fire_transition(etype, tr_name, from_state_name=None):
       
  1288     """Return 1 when entity of the type `etype` is going through transition of
       
  1289     the name `tr_name`.
       
  1290 
       
  1291     If `from_state_name` is specified, this selector will also check the
       
  1292     incoming state.
       
  1293 
       
  1294     You should use this selector on 'after_add_entity' hook, since it's actually
       
  1295     looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
       
  1296     will reference the matching `TrInfo` entity, allowing to get all the
       
  1297     transition details (including the entity to which is applied the transition
       
  1298     but also its original state, transition, destination state, user...).
       
  1299 
       
  1300     See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
       
  1301     """
       
  1302     def match_etype_and_transition(trinfo):
       
  1303         # take care trinfo.transition is None when calling change_state
       
  1304         return (trinfo.transition and trinfo.transition.name == tr_name
       
  1305                 # is_instance() first two arguments are 'cls' (unused, so giving
       
  1306                 # None is fine) and the request/session
       
  1307                 and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
       
  1308 
       
  1309     return is_instance('TrInfo') & score_entity(match_etype_and_transition)
       
  1310 
       
  1311 
       
  1312 class match_transition(ExpectedValueSelector):
       
  1313     """Return 1 if `transition` argument is found in the input context which has
       
  1314     a `.name` attribute matching one of the expected names given to the
       
  1315     initializer.
       
  1316 
       
  1317     This selector is expected to be used to customise the status change form in
       
  1318     the web ui.
       
  1319     """
       
  1320     @lltrace
       
  1321     def __call__(self, cls, req, transition=None, **kwargs):
       
  1322         # XXX check this is a transition that apply to the object?
       
  1323         if transition is None:
       
  1324             treid = req.form.get('treid', None)
       
  1325             if treid:
       
  1326                 transition = req.entity_from_eid(treid)
       
  1327         if transition is not None and getattr(transition, 'name', None) in self.expected:
       
  1328             return 1
       
  1329         return 0
       
  1330 
       
  1331 
       
  1332 # logged user selectors ########################################################
       
  1333 
       
  1334 @objectify_selector
       
  1335 @lltrace
       
  1336 def no_cnx(cls, req, **kwargs):
       
  1337     """Return 1 if the web session has no connection set. This occurs when
       
  1338     anonymous access is not allowed and user isn't authenticated.
       
  1339 
       
  1340     May only be used on the web side, not on the data repository side.
       
  1341     """
       
  1342     if not req.cnx:
       
  1343         return 1
       
  1344     return 0
       
  1345 
       
  1346 @objectify_selector
       
  1347 @lltrace
       
  1348 def authenticated_user(cls, req, **kwargs):
       
  1349     """Return 1 if the user is authenticated (e.g. not the anonymous user).
       
  1350 
       
  1351     May only be used on the web side, not on the data repository side.
       
  1352     """
       
  1353     if req.session.anonymous_session:
       
  1354         return 0
       
  1355     return 1
       
  1356 
       
  1357 
       
  1358 # XXX == ~ authenticated_user()
       
  1359 def anonymous_user():
       
  1360     """Return 1 if the user is not authenticated (e.g. is the anonymous user).
       
  1361 
       
  1362     May only be used on the web side, not on the data repository side.
       
  1363     """
       
  1364     return ~ authenticated_user()
       
  1365 
       
  1366 class match_user_groups(ExpectedValueSelector):
       
  1367     """Return a non-zero score if request's user is in at least one of the
       
  1368     groups given as initializer argument. Returned score is the number of groups
       
  1369     in which the user is.
       
  1370 
       
  1371     If the special 'owners' group is given and `rset` is specified in the input
       
  1372     context:
       
  1373 
       
  1374     * if `row` is specified check the entity at the given `row`/`col` (default
       
  1375       to 0) is owned by the user
       
  1376 
       
  1377     * else check all entities in `col` (default to 0) are owned by the user
       
  1378     """
       
  1379 
       
  1380     @lltrace
       
  1381     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
  1382         if not getattr(req, 'cnx', True): # default to True for repo session instances
       
  1383             return 0
       
  1384         user = req.user
       
  1385         if user is None:
       
  1386             return int('guests' in self.expected)
       
  1387         score = user.matching_groups(self.expected)
       
  1388         if not score and 'owners' in self.expected and rset:
       
  1389             if row is not None:
       
  1390                 if not user.owns(rset[row][col]):
       
  1391                     return 0
       
  1392                 score = 1
       
  1393             else:
       
  1394                 score = all(user.owns(r[col]) for r in rset)
       
  1395         return score
       
  1396 
       
  1397 # Web request selectors ########################################################
       
  1398 
       
  1399 # XXX deprecate
       
  1400 @objectify_selector
       
  1401 @lltrace
       
  1402 def primary_view(cls, req, view=None, **kwargs):
       
  1403     """Return 1 if:
       
  1404 
       
  1405     * *no view is specified* in the input context
       
  1406 
       
  1407     * a view is specified and its `.is_primary()` method return True
       
  1408 
       
  1409     This selector is usually used by contextual components that only want to
       
  1410     appears for the primary view of an entity.
       
  1411     """
       
  1412     if view is not None and not view.is_primary():
       
  1413         return 0
       
  1414     return 1
       
  1415 
       
  1416 
       
  1417 @objectify_selector
       
  1418 @lltrace
       
  1419 def contextual(cls, req, view=None, **kwargs):
       
  1420     """Return 1 if view's contextual property is true"""
       
  1421     if view is not None and view.contextual:
       
  1422         return 1
       
  1423     return 0
       
  1424 
       
  1425 
       
  1426 class match_view(ExpectedValueSelector):
       
  1427     """Return 1 if a view is specified an as its registry id is in one of the
       
  1428     expected view id given to the initializer.
       
  1429     """
       
  1430     @lltrace
       
  1431     def __call__(self, cls, req, view=None, **kwargs):
       
  1432         if view is None or not view.__regid__ in self.expected:
       
  1433             return 0
       
  1434         return 1
       
  1435 
       
  1436 
       
  1437 class match_context(ExpectedValueSelector):
       
  1438 
       
  1439     @lltrace
       
  1440     def __call__(self, cls, req, context=None, **kwargs):
       
  1441         if not context in self.expected:
       
  1442             return 0
       
  1443         return 1
       
  1444 
       
  1445 
       
  1446 # XXX deprecate
       
  1447 @objectify_selector
       
  1448 @lltrace
       
  1449 def match_context_prop(cls, req, context=None, **kwargs):
       
  1450     """Return 1 if:
       
  1451 
       
  1452     * no `context` is specified in input context (take care to confusion, here
       
  1453       `context` refers to a string given as an argument to the input context...)
       
  1454 
       
  1455     * specified `context` is matching the context property value for the
       
  1456       appobject using this selector
       
  1457 
       
  1458     * the appobject's context property value is None
       
  1459 
       
  1460     This selector is usually used by contextual components that want to appears
       
  1461     in a configurable place.
       
  1462     """
       
  1463     if context is None:
       
  1464         return 1
       
  1465     propval = req.property_value('%s.%s.context' % (cls.__registry__,
       
  1466                                                     cls.__regid__))
       
  1467     if propval and context != propval:
       
  1468         return 0
       
  1469     return 1
       
  1470 
       
  1471 
       
  1472 class match_search_state(ExpectedValueSelector):
       
  1473     """Return 1 if the current request search state is in one of the expected
       
  1474     states given to the initializer.
       
  1475 
       
  1476     Known search states are either 'normal' or 'linksearch' (eg searching for an
       
  1477     object to create a relation with another).
       
  1478 
       
  1479     This selector is usually used by action that want to appears or not according
       
  1480     to the ui search state.
       
  1481     """
       
  1482 
       
  1483     @lltrace
       
  1484     def __call__(self, cls, req, **kwargs):
       
  1485         try:
       
  1486             if not req.search_state[0] in self.expected:
       
  1487                 return 0
       
  1488         except AttributeError:
       
  1489             return 1 # class doesn't care about search state, accept it
       
  1490         return 1
       
  1491 
       
  1492 
       
  1493 class match_form_params(ExpectedValueSelector):
       
  1494     """Return non-zero score if parameter names specified as initializer
       
  1495     arguments are specified in request's form parameters.
       
  1496 
       
  1497     Return a score corresponding to the number of expected parameters.
       
  1498 
       
  1499     When multiple parameters are expected, all of them should be found in
       
  1500     the input context unless `mode` keyword argument is given to 'any',
       
  1501     in which case a single matching parameter is enough.
       
  1502     """
       
  1503 
       
  1504     def _values_set(self, cls, req, **kwargs):
       
  1505         return frozenset(req.form)
       
  1506 
       
  1507 
       
  1508 class match_edited_type(ExpectedValueSelector):
       
  1509     """return non-zero if main edited entity type is the one specified as
       
  1510     initializer argument, or is among initializer arguments if `mode` == 'any'.
       
  1511     """
       
  1512 
       
  1513     def _values_set(self, cls, req, **kwargs):
       
  1514         try:
       
  1515             return frozenset((req.form['__type:%s' % req.form['__maineid']],))
       
  1516         except KeyError:
       
  1517             return frozenset()
       
  1518 
       
  1519 
       
  1520 class match_form_id(ExpectedValueSelector):
       
  1521     """return non-zero if request form identifier is the one specified as
       
  1522     initializer argument, or is among initializer arguments if `mode` == 'any'.
       
  1523     """
       
  1524 
       
  1525     def _values_set(self, cls, req, **kwargs):
       
  1526         try:
       
  1527             return frozenset((req.form['__form_id'],))
       
  1528         except KeyError:
       
  1529             return frozenset()
       
  1530 
       
  1531 
       
  1532 class specified_etype_implements(is_instance):
       
  1533     """Return non-zero score if the entity type specified by an 'etype' key
       
  1534     searched in (by priority) input context kwargs and request form parameters
       
  1535     match a known entity type (case insensitivly), and it's associated entity
       
  1536     class is of one of the type(s) given to the initializer. If multiple
       
  1537     arguments are given, matching one of them is enough.
       
  1538 
       
  1539     .. note:: as with :class:`~cubicweb.selectors.is_instance`, entity types
       
  1540               should be given as string and the score will reflect class
       
  1541               proximity so the most specific object will be selected.
       
  1542 
       
  1543     This selector is usually used by views holding entity creation forms (since
       
  1544     we've no result set to work on).
       
  1545     """
       
  1546 
       
  1547     @lltrace
       
  1548     def __call__(self, cls, req, **kwargs):
       
  1549         try:
       
  1550             etype = kwargs['etype']
       
  1551         except KeyError:
       
  1552             try:
       
  1553                 etype = req.form['etype']
       
  1554             except KeyError:
       
  1555                 return 0
       
  1556             else:
       
  1557                 # only check this is a known type if etype comes from req.form,
       
  1558                 # else we want the error to propagate
       
  1559                 try:
       
  1560                     etype = req.vreg.case_insensitive_etypes[etype.lower()]
       
  1561                     req.form['etype'] = etype
       
  1562                 except KeyError:
       
  1563                     return 0
       
  1564         score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
       
  1565         if score:
       
  1566             eschema = req.vreg.schema.eschema(etype)
       
  1567             if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
       
  1568                 return score
       
  1569         return 0
       
  1570 
       
  1571 
       
  1572 class attribute_edited(EntitySelector):
       
  1573     """Scores if the specified attribute has been edited This is useful for
       
  1574     selection of forms by the edit controller.
       
  1575 
       
  1576     The initial use case is on a form, in conjunction with match_transition,
       
  1577     which will not score at edit time::
       
  1578 
       
  1579      is_instance('Version') & (match_transition('ready') |
       
  1580                                attribute_edited('publication_date'))
       
  1581     """
       
  1582     def __init__(self, attribute, once_is_enough=None, mode='all'):
       
  1583         super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
       
  1584         self._attribute = attribute
       
  1585 
       
  1586     def score_entity(self, entity):
       
  1587         return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
       
  1588 
       
  1589 
       
  1590 # Other selectors ##############################################################
       
  1591 
       
  1592 class match_exception(ExpectedValueSelector):
       
  1593     """Return 1 if exception given as `exc` in the input context is an instance
       
  1594     of one of the class given on instanciation of this predicate.
       
  1595     """
       
  1596     def __init__(self, *expected):
       
  1597         assert expected, self
       
  1598         # we want a tuple, not a set as done in the parent class
       
  1599         self.expected = expected
       
  1600 
       
  1601     @lltrace
       
  1602     def __call__(self, cls, req, exc=None, **kwargs):
       
  1603         if exc is not None and isinstance(exc, self.expected):
       
  1604             return 1
       
  1605         return 0
       
  1606 
       
  1607 
       
  1608 @objectify_selector
       
  1609 def debug_mode(cls, req, rset=None, **kwargs):
       
  1610     """Return 1 if running in debug mode."""
       
  1611     return req.vreg.config.debugmode and 1 or 0
       
  1612 
       
  1613 
       
  1614 ## deprecated stuff ############################################################
       
  1615 
    37 
  1616 
    38 
  1617 class on_transition(is_in_state):
    39 class on_transition(is_in_state):
  1618     """Return 1 if entity is in one of the transitions given as argument list
    40     """Return 1 if entity is in one of the transitions given as argument list
  1619 
    41 
  1620     Especially useful to match passed transition to enable notifications when
    42     Especially useful to match passed transition to enable notifications when
  1621     your workflow allows several transition to the same states.
    43     your workflow allows several transition to the same states.
  1622 
    44 
  1623     Note that if workflow `change_state` adapter method is used, this selector
    45     Note that if workflow `change_state` adapter method is used, this predicate
  1624     will not be triggered.
    46     will not be triggered.
  1625 
    47 
  1626     You should use this instead of your own :class:`score_entity` selector to
    48     You should use this instead of your own :class:`score_entity` predicate to
  1627     avoid some gotchas:
    49     avoid some gotchas:
  1628 
    50 
  1629     * possible views gives a fake entity with no state
    51     * possible views gives a fake entity with no state
  1630     * you must use the latest tr info thru the workflow adapter for repository
    52     * you must use the latest tr info thru the workflow adapter for repository
  1631       side checking of the current state
    53       side checking of the current state
  1632 
    54 
  1633     In debug mode, this selector can raise:
    55     In debug mode, this predicate can raise:
  1634     :raises: :exc:`ValueError` for unknown transition names
    56     :raises: :exc:`ValueError` for unknown transition names
  1635         (etype workflow only not checked in custom workflow)
    57         (etype workflow only not checked in custom workflow)
  1636 
    58 
  1637     :rtype: int
    59     :rtype: int
  1638     """
    60     """
  1652         unknown = sorted(self.expected.difference(valid))
    74         unknown = sorted(self.expected.difference(valid))
  1653         if unknown:
    75         if unknown:
  1654             raise ValueError("%s: unknown transition(s): %s"
    76             raise ValueError("%s: unknown transition(s): %s"
  1655                              % (wf.name, ",".join(unknown)))
    77                              % (wf.name, ",".join(unknown)))
  1656 
    78 
       
    79 
  1657 entity_implements = class_renamed('entity_implements', is_instance)
    80 entity_implements = class_renamed('entity_implements', is_instance)
  1658 
    81 
  1659 class _but_etype(EntitySelector):
    82 class _but_etype(EntityPredicate):
  1660     """accept if the given entity types are not found in the result set.
    83     """accept if the given entity types are not found in the result set.
  1661 
    84 
  1662     See `EntitySelector` documentation for behaviour when row is not specified.
    85     See `EntityPredicate` documentation for behaviour when row is not specified.
  1663 
    86 
  1664     :param *etypes: entity types (`basestring`) which should be refused
    87     :param *etypes: entity types (`basestring`) which should be refused
  1665     """
    88     """
  1666     def __init__(self, *etypes):
    89     def __init__(self, *etypes):
  1667         super(_but_etype, self).__init__()
    90         super(_but_etype, self).__init__()
  1672             return 0
    95             return 0
  1673         return 1
    96         return 1
  1674 
    97 
  1675 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
    98 but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
  1676 
    99 
  1677 
   100 # XXX deprecated the one_* variants of predicates below w/ multi_xxx(nb=1)?
  1678 # XXX deprecated the one_* variants of selectors below w/ multi_xxx(nb=1)?
       
  1679 #     take care at the implementation though (looking for the 'row' argument's
   101 #     take care at the implementation though (looking for the 'row' argument's
  1680 #     value)
   102 #     value)
  1681 two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
   103 two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
  1682 two_cols_rset = class_renamed('two_cols_rset', multi_columns_rset)
   104 two_cols_rset = class_renamed('two_cols_rset', multi_columns_rset)
  1683 two_etypes_rset = class_renamed('two_etypes_rset', multi_etypes_rset)
   105 two_etypes_rset = class_renamed('two_etypes_rset', multi_etypes_rset)