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