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