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