--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/predicates.py Tue Jul 10 15:07:52 2012 +0200
@@ -0,0 +1,1570 @@
+# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
+""".. _Selectors:
+
+Predicates and selectors
+------------------------
+
+A predicate is a class testing a particular aspect of a context. A selector is
+built by combining existant predicates or even selectors.
+
+Using and combining existant predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine predicates using the `&`, `|` and `~` operators.
+
+When two predicates are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two predicates are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a predicate by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
+
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+ class RSSIconBox(box.Box):
+ ''' just display the RSS icon on uniform result set '''
+ __select__ = box.Box.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+ hierarchy to know the details)
+
+* :class:`~cubicweb.predicates.non_final_entity`, which filters on result sets
+ containing non final entities (a 'final entity' being synonym for entity
+ attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+ class EntityRSSIconBox(RSSIconBox):
+ '''just display the RSS icon on uniform result set for a single entity'''
+ __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.predicates.one_line_rset` predicate, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+ on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
+
+
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''if the user is the anonymous user, build a link to login else a link
+ to the connected user object with a logout link
+ '''
+ __regid__ = 'loggeduserlink'
+
+ def call(self):
+ if self._cw.session.anonymous_session:
+ # display login link
+ ...
+ else:
+ # display a link to the connected user object with a loggout link
+ ...
+
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+ class UserLink(component.Component):
+ '''display a link to the connected user object with a loggout link'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & authenticated_user()
+
+ def call(self):
+ # display useractions and siteactions
+ ...
+
+ class AnonUserLink(component.Component):
+ '''build a link to login'''
+ __regid__ = 'loggeduserlink'
+ __select__ = component.Component.__select__ & anonymous_user()
+
+ def call(self):
+ # display login link
+ ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
+
+
+.. _CustomPredicates:
+
+Defining your own predicates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autodocstring:: cubicweb.appobject::objectify_predicate
+
+In other cases, you can take a look at the following abstract base classes:
+
+.. autoclass:: cubicweb.predicates.ExpectedValuePredicate
+.. autoclass:: cubicweb.predicates.EClassPredicate
+.. autoclass:: cubicweb.predicates.EntityPredicate
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any application object)
+is, or is not selected appropriately. Looking at which predicates fired (or did
+not) is the way. The :class:`logilab.common.registry.traced_selection` context
+manager to help with that, *if you're running your instance in debug mode*.
+
+.. autoclass:: logilab.common.registry.traced_selection
+
+"""
+
+__docformat__ = "restructuredtext en"
+
+import logging
+from warnings import warn
+from operator import eq
+
+from logilab.common.compat import all, any
+from logilab.common.interface import implements as implements_iface
+from logilab.common.registry import Predicate, objectify_predicate
+
+from yams.schema import BASE_TYPES, role_name
+from rql.nodes import Function
+
+from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
+ CW_EVENT_MANAGER, role)
+# even if not used, let yes here so it's importable through this module
+from cubicweb.uilib import eid_param
+from cubicweb.schema import split_expression
+
+def score_interface(etypesreg, eclass, iface):
+ """Return XXX if the give object (maybe an instance or class) implements
+ the interface.
+ """
+ if getattr(iface, '__registry__', None) == 'etypes':
+ # adjust score if the interface is an entity class
+ parents, any = etypesreg.parent_classes(eclass.__regid__)
+ if iface is eclass:
+ return len(parents) + 4
+ if iface is any: # Any
+ return 1
+ for index, basecls in enumerate(reversed(parents)):
+ if iface is basecls:
+ return index + 3
+ return 0
+ # XXX iface in implements deprecated in 3.9
+ if implements_iface(eclass, iface):
+ # implementing an interface takes precedence other special Any interface
+ return 2
+ return 0
+
+
+# abstract predicates / mixin helpers ###########################################
+
+class PartialPredicateMixIn(object):
+ """convenience mix-in for predicates that will look into the containing
+ class to find missing information.
+
+ cf. `cubicweb.web.action.LinkToEntityAction` for instance
+ """
+ def __call__(self, cls, *args, **kwargs):
+ self.complete(cls)
+ return super(PartialPredicateMixIn, self).__call__(cls, *args, **kwargs)
+
+
+class EClassPredicate(Predicate):
+ """abstract class for predicates working on *entity class(es)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity's class
+
+ * elif `rset`, `select` and `filtered_variable` are specified, return score
+ for the possible classes for variable in the given rql :class:`Select`
+ node
+
+ * elif `rset` and `row` are specified, return score for the class of the
+ entity found in the specified cell, using column specified by `col` or 0
+
+ * elif `rset` is specified return score for each entity class found in the
+ column specified specified by the `col` argument or in column 0 if not
+ specified
+
+ When there are several classes to be evaluated, return the sum of scores for
+ each entity class unless:
+
+ - `mode` == 'all' (the default) and some entity class is scored
+ to 0, in which case 0 is returned
+
+ - `mode` == 'any', in which case the first non-zero score is
+ returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
+ """
+ def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
+ if once_is_enough is not None:
+ warn("[3.14] once_is_enough is deprecated, use mode='any'",
+ DeprecationWarning, stacklevel=2)
+ if once_is_enough:
+ mode = 'any'
+ assert mode in ('any', 'all'), 'bad mode %s' % mode
+ self.once_is_enough = mode == 'any'
+ self.accept_none = accept_none
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, entity=None,
+ select=None, filtered_variable=None,
+ accept_none=None,
+ **kwargs):
+ if entity is not None:
+ return self.score_class(entity.__class__, req)
+ if not rset:
+ return 0
+ if select is not None and filtered_variable is not None:
+ etypes = set(sol[filtered_variable.name] for sol in select.solutions)
+ elif row is None:
+ if accept_none is None:
+ accept_none = self.accept_none
+ if not accept_none and \
+ any(rset[i][col] is None for i in xrange(len(rset))):
+ return 0
+ etypes = rset.column_types(col)
+ else:
+ etype = rset.description[row][col]
+ # may have None in rset.description on outer join
+ if etype is None or rset.rows[row][col] is None:
+ return 0
+ etypes = (etype,)
+ score = 0
+ for etype in etypes:
+ escore = self.score(cls, req, etype)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ return score
+
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return self.score_class(req.vreg['etypes'].etype_class(etype), req)
+
+ def score_class(self, eclass, req):
+ raise NotImplementedError()
+
+
+class EntityPredicate(EClassPredicate):
+ """abstract class for predicates working on *entity instance(s)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity
+
+ * elif `row` is specified, return score for the entity found in the
+ specified cell, using column specified by `col` or 0
+
+ * else return the sum of scores for each entity found in the column
+ specified specified by the `col` argument or in column 0 if not specified,
+ unless:
+
+ - `mode` == 'all' (the default) and some entity class is scored
+ to 0, in which case 0 is returned
+
+ - `mode` == 'any', in which case the first non-zero score is
+ returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
+
+ .. Note::
+ using :class:`EntityPredicate` or :class:`EClassPredicate` as base predicate
+ class impacts performance, since when no entity or row is specified the
+ later works on every different *entity class* found in the result set,
+ while the former works on each *entity* (eg each row of the result set),
+ which may be much more costly.
+ """
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, accept_none=None,
+ **kwargs):
+ if not rset and not kwargs.get('entity'):
+ return 0
+ score = 0
+ if kwargs.get('entity'):
+ score = self.score_entity(kwargs['entity'])
+ elif row is None:
+ col = col or 0
+ if accept_none is None:
+ accept_none = self.accept_none
+ for row, rowvalue in enumerate(rset.rows):
+ if rowvalue[col] is None: # outer join
+ if not accept_none:
+ return 0
+ continue
+ escore = self.score(req, rset, row, col)
+ if not escore and not self.once_is_enough:
+ return 0
+ elif self.once_is_enough:
+ return escore
+ score += escore
+ else:
+ col = col or 0
+ etype = rset.description[row][col]
+ if etype is not None: # outer join
+ score = self.score(req, rset, row, col)
+ return score
+
+ def score(self, req, rset, row, col):
+ try:
+ return self.score_entity(rset.get_entity(row, col))
+ except NotAnEntity:
+ return 0
+
+ def score_entity(self, entity):
+ raise NotImplementedError()
+
+
+class ExpectedValuePredicate(Predicate):
+ """Take a list of expected values as initializer argument and store them
+ into the :attr:`expected` set attribute. You may also give a set as single
+ argument, which will then be referenced as set of expected values,
+ allowing modifications to the given set to be considered.
+
+ You should implement one of :meth:`_values_set(cls, req, **kwargs)` or
+ :meth:`_get_value(cls, req, **kwargs)` method which should respectively
+ return the set of values or the unique possible value for the given context.
+
+ You may also specify a `mode` behaviour as argument, as explained below.
+
+ Returned score is:
+
+ - 0 if `mode` == 'all' (the default) and at least one expected
+ values isn't found
+
+ - 0 if `mode` == 'any' and no expected values isn't found at all
+
+ - else the number of matching values
+
+ Notice `mode` = 'any' with a single expected value has no effect at all.
+ """
+ def __init__(self, *expected, **kwargs):
+ assert expected, self
+ if len(expected) == 1 and isinstance(expected[0], set):
+ self.expected = expected[0]
+ else:
+ self.expected = frozenset(expected)
+ mode = kwargs.pop('mode', 'all')
+ assert mode in ('any', 'all'), 'bad mode %s' % mode
+ self.once_is_enough = mode == 'any'
+ assert not kwargs, 'unexpected arguments %s' % kwargs
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(sorted(str(s) for s in self.expected)))
+
+ def __call__(self, cls, req, **kwargs):
+ values = self._values_set(cls, req, **kwargs)
+ matching = len(values & self.expected)
+ if self.once_is_enough:
+ return matching
+ if matching == len(self.expected):
+ return matching
+ return 0
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset( (self._get_value(cls, req, **kwargs),) )
+
+ def _get_value(self, cls, req, **kwargs):
+ raise NotImplementedError()
+
+
+# bare predicates ##############################################################
+
+class match_kwargs(ExpectedValuePredicate):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in the input context.
+
+
+ Return a score corresponding to the number of expected parameters.
+
+ When multiple parameters are expected, all of them should be found in
+ the input context unless `mode` keyword argument is given to 'any',
+ in which case a single matching parameter is enough.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset(kwargs)
+
+
+class appobject_selectable(Predicate):
+ """Return 1 if another appobject is selectable using the same input context.
+
+ Initializer arguments:
+
+ * `registry`, a registry name
+
+ * `regids`, object identifiers in this registry, one of them should be
+ selectable.
+ """
+ selectable_score = 1
+ def __init__(self, registry, *regids):
+ self.registry = registry
+ self.regids = regids
+
+ def __call__(self, cls, req, **kwargs):
+ for regid in self.regids:
+ try:
+ req.vreg[self.registry].select(regid, req, **kwargs)
+ return self.selectable_score
+ except NoSelectableObject:
+ continue
+ return 0
+
+
+class adaptable(appobject_selectable):
+ """Return 1 if another appobject is selectable using the same input context.
+
+ Initializer arguments:
+
+ * `regids`, adapter identifiers (e.g. interface names) to which the context
+ (usually entities) should be adaptable. One of them should be selectable
+ when multiple identifiers are given.
+ """
+ def __init__(self, *regids):
+ super(adaptable, self).__init__('adapters', *regids)
+
+ def __call__(self, cls, req, **kwargs):
+ kwargs.setdefault('accept_none', False)
+ # being adaptable to an interface should takes precedence other is_instance('Any'),
+ # but not other explicit is_instance('SomeEntityType'), and:
+ # * is_instance('Any') score is 1
+ # * is_instance('SomeEntityType') score is at least 2
+ score = super(adaptable, self).__call__(cls, req, **kwargs)
+ if score >= 2:
+ return score - 0.5
+ if score == 1:
+ return score + 0.5
+ return score
+
+
+class configuration_values(Predicate):
+ """Return 1 if the instance has an option set to a given value(s) in its
+ configuration file.
+ """
+ # XXX this predicate could be evaluated on startup
+ def __init__(self, key, values):
+ self._key = key
+ if not isinstance(values, (tuple, list)):
+ values = (values,)
+ self._values = frozenset(values)
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ return self._score
+ except AttributeError:
+ if req is None:
+ config = kwargs['repo'].config
+ else:
+ config = req.vreg.config
+ self._score = config[self._key] in self._values
+ return self._score
+
+
+# rset predicates ##############################################################
+
+@objectify_predicate
+def none_rset(cls, req, rset=None, **kwargs):
+ """Return 1 if the result set is None (eg usually not specified)."""
+ if rset is None:
+ return 1
+ return 0
+
+
+# XXX == ~ none_rset
+@objectify_predicate
+def any_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for any result set, whatever the number of rows in it, even 0."""
+ if rset is not None:
+ return 1
+ return 0
+
+
+@objectify_predicate
+def nonempty_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for result set containing one ore more rows."""
+ if rset is not None and rset.rowcount:
+ return 1
+ return 0
+
+
+# XXX == ~ nonempty_rset
+@objectify_predicate
+def empty_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for result set which doesn't contain any row."""
+ if rset is not None and rset.rowcount == 0:
+ return 1
+ return 0
+
+
+# XXX == multi_lines_rset(1)
+@objectify_predicate
+def one_line_rset(cls, req, rset=None, row=None, **kwargs):
+ """Return 1 if the result set is of size 1, or greater but a specific row in
+ the result set is specified ('row' argument).
+ """
+ if rset is None and 'entity' in kwargs:
+ return 1
+ if rset is not None and (row is not None or rset.rowcount == 1):
+ return 1
+ return 0
+
+
+class multi_lines_rset(Predicate):
+ """Return 1 if the operator expression matches between `num` elements
+ in the result set and the `expected` value if defined.
+
+ By default, multi_lines_rset(expected) matches equality expression:
+ `nb` row(s) in result set equals to expected value
+ But, you can perform richer comparisons by overriding default operator:
+ multi_lines_rset(expected, operator.gt)
+
+ If `expected` is None, return 1 if the result set contains *at least*
+ two rows.
+ If rset is None, return 0.
+ """
+ def __init__(self, expected=None, operator=eq):
+ self.expected = expected
+ self.operator = operator
+
+ def match_expected(self, num):
+ if self.expected is None:
+ return num > 1
+ return self.operator(num, self.expected)
+
+ def __call__(self, cls, req, rset=None, **kwargs):
+ return int(rset is not None and self.match_expected(rset.rowcount))
+
+
+class multi_columns_rset(multi_lines_rset):
+ """If `nb` is specified, return 1 if the result set has exactly `nb` column
+ per row. Else (`nb` is None), return 1 if the result set contains *at least*
+ two columns per row. Return 0 for empty result set.
+ """
+
+ def __call__(self, cls, req, rset=None, **kwargs):
+ # 'or 0' since we *must not* return None. Also don't use rset.rows so
+ # this selector will work if rset is a simple list of list.
+ return rset and self.match_expected(len(rset[0])) or 0
+
+
+class paginated_rset(Predicate):
+ """Return 1 or more for result set with more rows than one or more page
+ size. You can specify expected number of pages to the initializer (default
+ to one), and you'll get that number of pages as score if the result set is
+ big enough.
+
+ Page size is searched in (respecting order):
+ * a `page_size` argument
+ * a `page_size` form parameters
+ * the `navigation.page-size` property (see :ref:`PersistentProperties`)
+ """
+ def __init__(self, nbpages=1):
+ assert nbpages > 0
+ self.nbpages = nbpages
+
+ def __call__(self, cls, req, rset=None, **kwargs):
+ if rset is None:
+ return 0
+ page_size = kwargs.get('page_size')
+ if page_size is None:
+ page_size = req.form.get('page_size')
+ if page_size is None:
+ page_size = req.property_value('navigation.page-size')
+ else:
+ page_size = int(page_size)
+ if rset.rowcount <= (page_size*self.nbpages):
+ return 0
+ return self.nbpages
+
+
+@objectify_predicate
+def sorted_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for sorted result set (e.g. from an RQL query containing an
+ ORDERBY clause), with exception that it will return 0 if the rset is
+ 'ORDERBY FTIRANK(VAR)' (eg sorted by rank value of the has_text index).
+ """
+ if rset is None:
+ return 0
+ selects = rset.syntax_tree().children
+ if (len(selects) > 1 or
+ not selects[0].orderby or
+ (isinstance(selects[0].orderby[0].term, Function) and
+ selects[0].orderby[0].term.name == 'FTIRANK')
+ ):
+ return 0
+ return 2
+
+
+# XXX == multi_etypes_rset(1)
+@objectify_predicate
+def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
+ """Return 1 if the result set contains entities which are all of the same
+ type in the column specified by the `col` argument of the input context, or
+ in column 0.
+ """
+ if rset is None:
+ return 0
+ if len(rset.column_types(col)) != 1:
+ return 0
+ return 1
+
+
+class multi_etypes_rset(multi_lines_rset):
+ """If `nb` is specified, return 1 if the result set contains `nb` different
+ types of entities in the column specified by the `col` argument of the input
+ context, or in column 0. If `nb` is None, return 1 if the result set contains
+ *at least* two different types of entities.
+ """
+
+ def __call__(self, cls, req, rset=None, col=0, **kwargs):
+ # 'or 0' since we *must not* return None
+ return rset and self.match_expected(len(rset.column_types(col))) or 0
+
+
+@objectify_predicate
+def logged_user_in_rset(cls, req, rset=None, row=None, col=0, **kwargs):
+ """Return positive score if the result set at the specified row / col
+ contains the eid of the logged user.
+ """
+ if rset is None:
+ return 0
+ return req.user.eid == rset[row or 0][col]
+
+
+# entity predicates #############################################################
+
+class non_final_entity(EClassPredicate):
+ """Return 1 for entity of a non final entity type(s). Remember, "final"
+ entity types are String, Int, etc... This is equivalent to
+ `is_instance('Any')` but more optimized.
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context.
+ """
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return 1
+
+ def score_class(self, eclass, req):
+ return 1 # necessarily true if we're there
+
+
+class implements(EClassPredicate):
+ """Return non-zero score for entity that are of the given type(s) or
+ implements at least one of the given interface(s). If multiple arguments are
+ given, matching one of them is enough.
+
+ Entity types should be given as string, the corresponding class will be
+ fetched from the entity types registry at selection time.
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context.
+
+ .. note:: when interface is an entity class, the score will reflect class
+ proximity so the most specific object will be selected.
+
+ .. note:: deprecated in cubicweb >= 3.9, use either
+ :class:`~cubicweb.predicates.is_instance` or
+ :class:`~cubicweb.predicates.adaptable`.
+ """
+
+ def __init__(self, *expected_ifaces, **kwargs):
+ emit_warn = kwargs.pop('warn', True)
+ super(implements, self).__init__(**kwargs)
+ self.expected_ifaces = expected_ifaces
+ if emit_warn:
+ warn('[3.9] implements predicate is deprecated, use either '
+ 'is_instance or adaptable', DeprecationWarning, stacklevel=2)
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected_ifaces))
+
+ def score_class(self, eclass, req):
+ score = 0
+ etypesreg = req.vreg['etypes']
+ for iface in self.expected_ifaces:
+ if isinstance(iface, basestring):
+ # entity type
+ try:
+ iface = etypesreg.etype_class(iface)
+ except KeyError:
+ continue # entity type not in the schema
+ score += score_interface(etypesreg, eclass, iface)
+ return score
+
+def _reset_is_instance_cache(vreg):
+ vreg._is_instance_predicate_cache = {}
+
+CW_EVENT_MANAGER.bind('before-registry-reset', _reset_is_instance_cache)
+
+class is_instance(EClassPredicate):
+ """Return non-zero score for entity that is an instance of the one of given
+ type(s). If multiple arguments are given, matching one of them is enough.
+
+ Entity types should be given as string, the corresponding class will be
+ fetched from the registry at selection time.
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context.
+
+ .. note:: the score will reflect class proximity so the most specific object
+ will be selected.
+ """
+
+ def __init__(self, *expected_etypes, **kwargs):
+ super(is_instance, self).__init__(**kwargs)
+ self.expected_etypes = expected_etypes
+ for etype in self.expected_etypes:
+ assert isinstance(etype, basestring), etype
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected_etypes))
+
+ def score_class(self, eclass, req):
+ # cache on vreg to avoid reloading issues
+ try:
+ cache = req.vreg._is_instance_predicate_cache
+ except AttributeError:
+ # XXX 'before-registry-reset' not called for db-api connections
+ cache = req.vreg._is_instance_predicate_cache = {}
+ try:
+ expected_eclasses = cache[self]
+ except KeyError:
+ # turn list of entity types as string into a list of
+ # (entity class, parent classes)
+ etypesreg = req.vreg['etypes']
+ expected_eclasses = cache[self] = []
+ for etype in self.expected_etypes:
+ try:
+ expected_eclasses.append(etypesreg.etype_class(etype))
+ except KeyError:
+ continue # entity type not in the schema
+ parents, any = req.vreg['etypes'].parent_classes(eclass.__regid__)
+ score = 0
+ for expectedcls in expected_eclasses:
+ # adjust score according to class proximity
+ if expectedcls is eclass:
+ score += len(parents) + 4
+ elif expectedcls is any: # Any
+ score += 1
+ else:
+ for index, basecls in enumerate(reversed(parents)):
+ if expectedcls is basecls:
+ score += index + 3
+ break
+ return score
+
+
+class score_entity(EntityPredicate):
+ """Return score according to an arbitrary function given as argument which
+ will be called with input content entity as argument.
+
+ This is a very useful predicate that will usually interest you since it
+ allows a lot of things without having to write a specific predicate.
+
+ The function can return arbitrary value which will be casted to an integer
+ value at the end.
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, scorefunc, once_is_enough=None, mode='all'):
+ super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ def intscore(*args, **kwargs):
+ score = scorefunc(*args, **kwargs)
+ if not score:
+ return 0
+ if isinstance(score, (int, long)):
+ return score
+ return 1
+ self.score_entity = intscore
+
+
+class has_mimetype(EntityPredicate):
+ """Return 1 if the entity adapt to IDownloadable and has the given MIME type.
+
+ You can give 'image/' to match any image for instance, or 'image/png' to match
+ only PNG images.
+ """
+ def __init__(self, mimetype, once_is_enough=None, mode='all'):
+ super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self.mimetype = mimetype
+
+ def score_entity(self, entity):
+ idownloadable = entity.cw_adapt_to('IDownloadable')
+ if idownloadable is None:
+ return 0
+ mt = idownloadable.download_content_type()
+ if not (mt and mt.startswith(self.mimetype)):
+ return 0
+ return 1
+
+
+class relation_possible(EntityPredicate):
+ """Return 1 for entity that supports the relation, provided that the
+ request's user may do some `action` on it (see below).
+
+ The relation is specified by the following initializer arguments:
+
+ * `rtype`, the name of the relation
+
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'
+
+ * `target_etype`, optional name of an entity type that should be supported
+ at the other end of the relation
+
+ * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
+ default to 'read') which must be granted to the user, else a 0 score will
+ be returned. Give None if you don't want any permission checking.
+
+ * `strict`, boolean (default to False) telling what to do when the user has
+ not globally the permission for the action (eg the action is not granted
+ to one of the user's groups)
+
+ - when strict is False, if there are some local role defined for this
+ action (e.g. using rql expressions), then the permission will be
+ considered as granted
+
+ - when strict is True, then the permission will be actually checked for
+ each entity
+
+ Setting `strict` to True impacts performance for large result set since
+ you'll then get the :class:`~cubicweb.predicates.EntityPredicate` behaviour
+ while otherwise you get the :class:`~cubicweb.predicates.EClassPredicate`'s
+ one. See those classes documentation for entity lookup / score rules
+ according to the input context.
+ """
+
+ def __init__(self, rtype, role='subject', target_etype=None,
+ action='read', strict=False, **kwargs):
+ super(relation_possible, self).__init__(**kwargs)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+ self.action = action
+ self.strict = strict
+
+ # hack hack hack
+ def __call__(self, cls, req, **kwargs):
+ # hack hack hack
+ if self.strict:
+ return EntityPredicate.__call__(self, cls, req, **kwargs)
+ return EClassPredicate.__call__(self, cls, req, **kwargs)
+
+ def score(self, *args):
+ if self.strict:
+ return EntityPredicate.score(self, *args)
+ return EClassPredicate.score(self, *args)
+
+ def _get_rschema(self, eclass):
+ eschema = eclass.e_schema
+ try:
+ if self.role == 'object':
+ return eschema.objrels[self.rtype]
+ else:
+ return eschema.subjrels[self.rtype]
+ except KeyError:
+ return None
+
+ def score_class(self, eclass, req):
+ rschema = self._get_rschema(eclass)
+ if rschema is None:
+ return 0 # relation not supported
+ eschema = eclass.e_schema
+ if self.target_etype is not None:
+ try:
+ rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
+ except KeyError:
+ return 0
+ if self.action and not rdef.may_have_permission(self.action, req):
+ return 0
+ teschema = req.vreg.schema.eschema(self.target_etype)
+ if not teschema.may_have_permission('read', req):
+ return 0
+ elif self.action:
+ return rschema.may_have_permission(self.action, req, eschema, self.role)
+ return 1
+
+ def score_entity(self, entity):
+ rschema = self._get_rschema(entity)
+ if rschema is None:
+ return 0 # relation not supported
+ if self.action:
+ if self.target_etype is not None:
+ rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
+ if self.role == 'subject':
+ if not rschema.has_perm(entity._cw, self.action, fromeid=entity.eid):
+ return 0
+ elif not rschema.has_perm(entity._cw, self.action, toeid=entity.eid):
+ return 0
+ if self.target_etype is not None:
+ req = entity._cw
+ teschema = req.vreg.schema.eschema(self.target_etype)
+ if not teschema.may_have_permission('read', req):
+ return 0
+ return 1
+
+
+class partial_relation_possible(PartialPredicateMixIn, relation_possible):
+ """Same as :class:~`cubicweb.predicates.relation_possible`, but will look for
+ attributes of the selected class to get information which is otherwise
+ expected by the initializer, except for `action` and `strict` which are kept
+ as initializer arguments.
+
+ This is useful to predefine predicate of an abstract class designed to be
+ customized.
+ """
+ def __init__(self, action='read', **kwargs):
+ super(partial_relation_possible, self).__init__(None, None, None,
+ action, **kwargs)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_related_entities(EntityPredicate):
+ """Return 1 if entity support the specified relation and has some linked
+ entities by this relation , optionaly filtered according to the specified
+ target type.
+
+ The relation is specified by the following initializer arguments:
+
+ * `rtype`, the name of the relation
+
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'.
+
+ * `target_etype`, optional name of an entity type that should be found
+ at the other end of the relation
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
+ super(has_related_entities, self).__init__(**kwargs)
+ self.rtype = rtype
+ self.role = role
+ self.target_etype = target_etype
+
+ def score_entity(self, entity):
+ relpossel = relation_possible(self.rtype, self.role, self.target_etype)
+ if not relpossel.score_class(entity.__class__, entity._cw):
+ return 0
+ rset = entity.related(self.rtype, self.role)
+ if self.target_etype:
+ return any(r for r in rset.description if r[0] == self.target_etype)
+ return rset and 1 or 0
+
+
+class partial_has_related_entities(PartialPredicateMixIn, has_related_entities):
+ """Same as :class:~`cubicweb.predicates.has_related_entity`, but will look
+ for attributes of the selected class to get information which is otherwise
+ expected by the initializer.
+
+ This is useful to predefine predicate of an abstract class designed to be
+ customized.
+ """
+ def __init__(self, **kwargs):
+ super(partial_has_related_entities, self).__init__(None, None, None,
+ **kwargs)
+
+ def complete(self, cls):
+ self.rtype = cls.rtype
+ self.role = role(cls)
+ self.target_etype = getattr(cls, 'target_etype', None)
+
+
+class has_permission(EntityPredicate):
+ """Return non-zero score if request's user has the permission to do the
+ requested action on the entity. `action` is an entity schema action (eg one
+ of 'read', 'add', 'delete', 'update').
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, check permission is granted for this entity
+
+ * elif `row` is specified, check permission is granted for the entity found
+ in the specified cell
+
+ * else check permission is granted for each entity found in the column
+ specified specified by the `col` argument or in column 0
+ """
+ def __init__(self, action):
+ self.action = action
+
+ # don't use EntityPredicate.__call__ but this optimized implementation to
+ # avoid considering each entity when it's not necessary
+ def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ if kwargs.get('entity'):
+ return self.score_entity(kwargs['entity'])
+ if rset is None:
+ return 0
+ if row is None:
+ score = 0
+ need_local_check = []
+ geteschema = req.vreg.schema.eschema
+ user = req.user
+ action = self.action
+ for etype in rset.column_types(0):
+ if etype in BASE_TYPES:
+ return 0
+ eschema = geteschema(etype)
+ if not user.matching_groups(eschema.get_groups(action)):
+ if eschema.has_local_role(action):
+ # have to ckeck local roles
+ need_local_check.append(eschema)
+ continue
+ else:
+ # even a local role won't be enough
+ return 0
+ score += 1
+ if need_local_check:
+ # check local role for entities of necessary types
+ for i, row in enumerate(rset):
+ if not rset.description[i][col] in need_local_check:
+ continue
+ # micro-optimisation instead of calling self.score(req,
+ # rset, i, col): rset may be large
+ if not rset.get_entity(i, col).cw_has_perm(action):
+ return 0
+ score += 1
+ return score
+ return self.score(req, rset, row, col)
+
+ def score_entity(self, entity):
+ if entity.cw_has_perm(self.action):
+ return 1
+ return 0
+
+
+class has_add_permission(EClassPredicate):
+ """Return 1 if request's user has the add permission on entity type
+ specified in the `etype` initializer argument, or according to entity found
+ in the input content if not specified.
+
+ It also check that then entity type is not a strict subobject (e.g. may only
+ be used as a composed of another entity).
+
+ See :class:`~cubicweb.predicates.EClassPredicate` documentation for entity
+ class lookup / score rules according to the input context when `etype` is
+ not specified.
+ """
+ def __init__(self, etype=None, **kwargs):
+ super(has_add_permission, self).__init__(**kwargs)
+ self.etype = etype
+
+ def __call__(self, cls, req, **kwargs):
+ if self.etype is None:
+ return super(has_add_permission, self).__call__(cls, req, **kwargs)
+ return self.score(cls, req, self.etype)
+
+ def score_class(self, eclass, req):
+ eschema = eclass.e_schema
+ if eschema.final or eschema.is_subobject(strict=True) \
+ or not eschema.has_perm(req, 'add'):
+ return 0
+ return 1
+
+
+class rql_condition(EntityPredicate):
+ """Return non-zero score if arbitrary rql specified in `expression`
+ initializer argument return some results for entity found in the input
+ context. Returned score is the number of items returned by the rql
+ condition.
+
+ `expression` is expected to be a string containing an rql expression, which
+ must use 'X' variable to represent the context entity and may use 'U' to
+ represent the request's user.
+
+ .. warning::
+ If simply testing value of some attribute/relation of context entity (X),
+ you should rather use the :class:`score_entity` predicate which will
+ benefit from the ORM's request entities cache.
+
+ See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
+ super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self.user_condition = user_condition
+ if user_condition:
+ rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
+ elif 'U' in frozenset(split_expression(expression)):
+ rql = 'Any COUNT(X) WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
+ else:
+ rql = 'Any COUNT(X) WHERE X eid %%(x)s, %s' % expression
+ self.rql = rql
+
+ def __str__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.rql)
+
+ def __call__(self, cls, req, **kwargs):
+ if self.user_condition:
+ try:
+ return req.execute(self.rql, {'u': req.user.eid})[0][0]
+ except Unauthorized:
+ return 0
+ else:
+ return super(rql_condition, self).__call__(cls, req, **kwargs)
+
+ def _score(self, req, eid):
+ try:
+ return req.execute(self.rql, {'x': eid, 'u': req.user.eid})[0][0]
+ except Unauthorized:
+ return 0
+
+ def score(self, req, rset, row, col):
+ return self._score(req, rset[row][col])
+
+ def score_entity(self, entity):
+ return self._score(entity._cw, entity.eid)
+
+
+# workflow predicates ###########################################################
+
+class is_in_state(score_entity):
+ """Return 1 if entity is in one of the states given as argument list
+
+ You should use this instead of your own :class:`score_entity` predicate to
+ avoid some gotchas:
+
+ * possible views gives a fake entity with no state
+ * you must use the latest tr info thru the workflow adapter for repository
+ side checking of the current state
+
+ In debug mode, this predicate can raise :exc:`ValueError` for unknown states names
+ (only checked on entities without a custom workflow)
+
+ :rtype: int
+ """
+ def __init__(self, *expected):
+ assert expected, self
+ self.expected = frozenset(expected)
+ def score(entity, expected=self.expected):
+ adapted = entity.cw_adapt_to('IWorkflowable')
+ # in debug mode only (time consuming)
+ if entity._cw.vreg.config.debugmode:
+ # validation can only be done for generic etype workflow because
+ # expected transition list could have been changed for a custom
+ # workflow (for the current entity)
+ if not entity.custom_workflow:
+ self._validate(adapted)
+ return self._score(adapted)
+ super(is_in_state, self).__init__(score)
+
+ def _score(self, adapted):
+ trinfo = adapted.latest_trinfo()
+ if trinfo is None: # entity is probably in it's initial state
+ statename = adapted.state
+ else:
+ statename = trinfo.new_state.name
+ return statename in self.expected
+
+ def _validate(self, adapted):
+ wf = adapted.current_workflow
+ valid = [n.name for n in wf.reverse_state_of]
+ unknown = sorted(self.expected.difference(valid))
+ if unknown:
+ raise ValueError("%s: unknown state(s): %s"
+ % (wf.name, ",".join(unknown)))
+
+ def __str__(self):
+ return '%s(%s)' % (self.__class__.__name__,
+ ','.join(str(s) for s in self.expected))
+
+
+def on_fire_transition(etype, tr_name, from_state_name=None):
+ """Return 1 when entity of the type `etype` is going through transition of
+ the name `tr_name`.
+
+ If `from_state_name` is specified, this predicate will also check the
+ incoming state.
+
+ You should use this predicate on 'after_add_entity' hook, since it's actually
+ looking for addition of `TrInfo` entities. Hence in the hook, `self.entity`
+ will reference the matching `TrInfo` entity, allowing to get all the
+ transition details (including the entity to which is applied the transition
+ but also its original state, transition, destination state, user...).
+
+ See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
+ """
+ def match_etype_and_transition(trinfo):
+ # take care trinfo.transition is None when calling change_state
+ return (trinfo.transition and trinfo.transition.name == tr_name
+ # is_instance() first two arguments are 'cls' (unused, so giving
+ # None is fine) and the request/session
+ and is_instance(etype)(None, trinfo._cw, entity=trinfo.for_entity))
+
+ return is_instance('TrInfo') & score_entity(match_etype_and_transition)
+
+
+class match_transition(ExpectedValuePredicate):
+ """Return 1 if `transition` argument is found in the input context which has
+ a `.name` attribute matching one of the expected names given to the
+ initializer.
+
+ This predicate is expected to be used to customise the status change form in
+ the web ui.
+ """
+ def __call__(self, cls, req, transition=None, **kwargs):
+ # XXX check this is a transition that apply to the object?
+ if transition is None:
+ treid = req.form.get('treid', None)
+ if treid:
+ transition = req.entity_from_eid(treid)
+ if transition is not None and getattr(transition, 'name', None) in self.expected:
+ return 1
+ return 0
+
+
+# logged user predicates ########################################################
+
+@objectify_predicate
+def no_cnx(cls, req, **kwargs):
+ """Return 1 if the web session has no connection set. This occurs when
+ anonymous access is not allowed and user isn't authenticated.
+
+ May only be used on the web side, not on the data repository side.
+ """
+ if not req.cnx:
+ return 1
+ return 0
+
+@objectify_predicate
+def authenticated_user(cls, req, **kwargs):
+ """Return 1 if the user is authenticated (e.g. not the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ if req.session.anonymous_session:
+ return 0
+ return 1
+
+
+# XXX == ~ authenticated_user()
+def anonymous_user():
+ """Return 1 if the user is not authenticated (e.g. is the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ return ~ authenticated_user()
+
+class match_user_groups(ExpectedValuePredicate):
+ """Return a non-zero score if request's user is in at least one of the
+ groups given as initializer argument. Returned score is the number of groups
+ in which the user is.
+
+ If the special 'owners' group is given and `rset` is specified in the input
+ context:
+
+ * if `row` is specified check the entity at the given `row`/`col` (default
+ to 0) is owned by the user
+
+ * else check all entities in `col` (default to 0) are owned by the user
+ """
+
+ def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ if not getattr(req, 'cnx', True): # default to True for repo session instances
+ return 0
+ user = req.user
+ if user is None:
+ return int('guests' in self.expected)
+ score = user.matching_groups(self.expected)
+ if not score and 'owners' in self.expected and rset:
+ if row is not None:
+ if not user.owns(rset[row][col]):
+ return 0
+ score = 1
+ else:
+ score = all(user.owns(r[col]) for r in rset)
+ return score
+
+# Web request predicates ########################################################
+
+# XXX deprecate
+@objectify_predicate
+def primary_view(cls, req, view=None, **kwargs):
+ """Return 1 if:
+
+ * *no view is specified* in the input context
+
+ * a view is specified and its `.is_primary()` method return True
+
+ This predicate is usually used by contextual components that only want to
+ appears for the primary view of an entity.
+ """
+ if view is not None and not view.is_primary():
+ return 0
+ return 1
+
+
+@objectify_predicate
+def contextual(cls, req, view=None, **kwargs):
+ """Return 1 if view's contextual property is true"""
+ if view is not None and view.contextual:
+ return 1
+ return 0
+
+
+class match_view(ExpectedValuePredicate):
+ """Return 1 if a view is specified an as its registry id is in one of the
+ expected view id given to the initializer.
+ """
+ def __call__(self, cls, req, view=None, **kwargs):
+ if view is None or not view.__regid__ in self.expected:
+ return 0
+ return 1
+
+
+class match_context(ExpectedValuePredicate):
+
+ def __call__(self, cls, req, context=None, **kwargs):
+ if not context in self.expected:
+ return 0
+ return 1
+
+
+# XXX deprecate
+@objectify_predicate
+def match_context_prop(cls, req, context=None, **kwargs):
+ """Return 1 if:
+
+ * no `context` is specified in input context (take care to confusion, here
+ `context` refers to a string given as an argument to the input context...)
+
+ * specified `context` is matching the context property value for the
+ appobject using this predicate
+
+ * the appobject's context property value is None
+
+ This predicate is usually used by contextual components that want to appears
+ in a configurable place.
+ """
+ if context is None:
+ return 1
+ propval = req.property_value('%s.%s.context' % (cls.__registry__,
+ cls.__regid__))
+ if propval and context != propval:
+ return 0
+ return 1
+
+
+class match_search_state(ExpectedValuePredicate):
+ """Return 1 if the current request search state is in one of the expected
+ states given to the initializer.
+
+ Known search states are either 'normal' or 'linksearch' (eg searching for an
+ object to create a relation with another).
+
+ This predicate is usually used by action that want to appears or not according
+ to the ui search state.
+ """
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ if not req.search_state[0] in self.expected:
+ return 0
+ except AttributeError:
+ return 1 # class doesn't care about search state, accept it
+ return 1
+
+
+class match_form_params(ExpectedValuePredicate):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in request's form parameters.
+
+ Return a score corresponding to the number of expected parameters.
+
+ When multiple parameters are expected, all of them should be found in
+ the input context unless `mode` keyword argument is given to 'any',
+ in which case a single matching parameter is enough.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ return frozenset(req.form)
+
+
+class match_edited_type(ExpectedValuePredicate):
+ """return non-zero if main edited entity type is the one specified as
+ initializer argument, or is among initializer arguments if `mode` == 'any'.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ try:
+ return frozenset((req.form['__type:%s' % req.form['__maineid']],))
+ except KeyError:
+ return frozenset()
+
+
+class match_form_id(ExpectedValuePredicate):
+ """return non-zero if request form identifier is the one specified as
+ initializer argument, or is among initializer arguments if `mode` == 'any'.
+ """
+
+ def _values_set(self, cls, req, **kwargs):
+ try:
+ return frozenset((req.form['__form_id'],))
+ except KeyError:
+ return frozenset()
+
+
+class specified_etype_implements(is_instance):
+ """Return non-zero score if the entity type specified by an 'etype' key
+ searched in (by priority) input context kwargs and request form parameters
+ match a known entity type (case insensitivly), and it's associated entity
+ class is of one of the type(s) given to the initializer. If multiple
+ arguments are given, matching one of them is enough.
+
+ .. note:: as with :class:`~cubicweb.predicates.is_instance`, entity types
+ should be given as string and the score will reflect class
+ proximity so the most specific object will be selected.
+
+ This predicate is usually used by views holding entity creation forms (since
+ we've no result set to work on).
+ """
+
+ def __call__(self, cls, req, **kwargs):
+ try:
+ etype = kwargs['etype']
+ except KeyError:
+ try:
+ etype = req.form['etype']
+ except KeyError:
+ return 0
+ else:
+ # only check this is a known type if etype comes from req.form,
+ # else we want the error to propagate
+ try:
+ etype = req.vreg.case_insensitive_etypes[etype.lower()]
+ req.form['etype'] = etype
+ except KeyError:
+ return 0
+ score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
+ if score:
+ eschema = req.vreg.schema.eschema(etype)
+ if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
+ return score
+ return 0
+
+
+class attribute_edited(EntityPredicate):
+ """Scores if the specified attribute has been edited This is useful for
+ selection of forms by the edit controller.
+
+ The initial use case is on a form, in conjunction with match_transition,
+ which will not score at edit time::
+
+ is_instance('Version') & (match_transition('ready') |
+ attribute_edited('publication_date'))
+ """
+ def __init__(self, attribute, once_is_enough=None, mode='all'):
+ super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
+ self._attribute = attribute
+
+ def score_entity(self, entity):
+ return eid_param(role_name(self._attribute, 'subject'), entity.eid) in entity._cw.form
+
+
+# Other predicates ##############################################################
+
+class match_exception(ExpectedValuePredicate):
+ """Return 1 if exception given as `exc` in the input context is an instance
+ of one of the class given on instanciation of this predicate.
+ """
+ def __init__(self, *expected):
+ assert expected, self
+ # we want a tuple, not a set as done in the parent class
+ self.expected = expected
+
+ def __call__(self, cls, req, exc=None, **kwargs):
+ if exc is not None and isinstance(exc, self.expected):
+ return 1
+ return 0
+
+
+@objectify_predicate
+def debug_mode(cls, req, rset=None, **kwargs):
+ """Return 1 if running in debug mode."""
+ return req.vreg.config.debugmode and 1 or 0