reorganize, cleanup and properly document base selectors. Kill the may_add_relation selector.
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 05 Feb 2010 08:53:33 +0100
changeset 4462 c57c8176b8c2
parent 4461 a35c76ffed92
child 4463 b071d5c6b48f
reorganize, cleanup and properly document base selectors. Kill the may_add_relation selector.
appobject.py
selectors.py
web/action.py
web/views/actions.py
--- a/appobject.py	Fri Feb 05 08:11:38 2010 +0100
+++ b/appobject.py	Fri Feb 05 08:53:33 2010 +0100
@@ -27,7 +27,7 @@
     would be overkill::
 
         @objectify_selector
-        def yes(cls, *args, **kwargs):
+        def one(cls, *args, **kwargs):
             return 1
 
     """
@@ -185,9 +185,13 @@
 
 
 class yes(Selector):
-    """return arbitrary score
+    """Return the score given as parameter, with a default score of 0.5 so any
+    other selector take precedence.
 
-    default score of 0.5 so any other selector take precedence
+    Usually used for appobjects which can be selected whatever the context, or
+    also sometimes to add arbitrary points to a score.
+
+    Take care, `yes(0)` could be named 'no'...
     """
     def __init__(self, score=0.5):
         self.score = score
--- a/selectors.py	Fri Feb 05 08:11:38 2010 +0100
+++ b/selectors.py	Fri Feb 05 08:53:33 2010 +0100
@@ -136,7 +136,7 @@
     return 0
 
 
-# abstract selectors ##########################################################
+# abstract selectors / mixin helpers ###########################################
 
 class PartialSelectorMixIn(object):
     """convenience mix-in for selectors that will look into the containing
@@ -176,19 +176,28 @@
 
 
 class EClassSelector(Selector):
-    """abstract class for selectors working on the entity classes of the result
-    set. Its __call__ method has the following behaviour:
+    """abstract class for selectors 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
 
-    * if 'entity' find in kwargs, return the score returned by the score_class
-      method for this entity's class
-    * elif row is specified, return the score returned by the score_class method
-      called with the entity class found in the specified cell
-    * else return the sum of score returned by the score_class method for each
-      entity type found in the specified column, unless:
+    * elif `row` is specified, return score for the class of the entity
+      found in the specified cell, using column specified by `col` or 0
+
+    * else return the sum of scores for each entity class found in the column
+      specified specified by the `col` argument or in column 0 if not specified,
+      unless:
+
+      - `once_is_enough` is False (the default) and some entity class is scored
+        to 0, in which case 0 is returned
+
       - `once_is_enough` is True, in which case the first non-zero score is
         returned
-      - `once_is_enough` is False, in which case if score_class return 0, 0 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=False, accept_none=True):
         self.once_is_enough = once_is_enough
@@ -230,22 +239,35 @@
 
 
 class EntitySelector(EClassSelector):
-    """abstract class for selectors working on the entity instances of the
-    result set. Its __call__ method has the following behaviour:
+    """abstract class for selectors 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
 
-    * if 'entity' find in kwargs, return the score returned by the score_entity
-      method for this entity
-    * if row is specified, return the score returned by the score_entity method
-      called with the entity instance found in the specified cell
-    * else return the sum of score returned by the score_entity method for each
-      entity found in the specified column, unless:
+    * 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:
+
+      - `once_is_enough` is False (the default) and some entity is scored
+        to 0, in which case 0 is returned
+
       - `once_is_enough` is True, in which case the first non-zero score is
         returned
-      - `once_is_enough` is False, in which case if score_class return 0, 0 is
-        returned
+
+      - `accept_none` is False and some cell in the column has a None value
+        (this may occurs with outer join)
 
-    note: None values (resulting from some outer join in the query) are not
-          considered.
+    .. note::
+       using EntitySelector or EClassSelector as base selector 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.
     """
 
     @lltrace
@@ -259,6 +281,8 @@
             col = col or 0
             for row, rowvalue in enumerate(rset.rows):
                 if rowvalue[col] is None: # outer join
+                    if not self.accept_none:
+                        return 0
                     continue
                 escore = self.score(req, rset, row, col)
                 if not escore and not self.once_is_enough:
@@ -283,178 +307,9 @@
         raise NotImplementedError()
 
 
-# very basic selectors ########################################################
-
-@objectify_selector
-@lltrace
-def none_rset(cls, req, rset=None, **kwargs):
-    """accept no result set (e.g. given rset is None)"""
-    if rset is None:
-        return 1
-    return 0
-
-@objectify_selector
-@lltrace
-def any_rset(cls, req, rset=None, **kwargs):
-    """accept result set, whatever the number of result it contains"""
-    if rset is not None:
-        return 1
-    return 0
-
-@objectify_selector
-@lltrace
-def nonempty_rset(cls, req, rset=None, **kwargs):
-    """accept any non empty result set"""
-    if rset is not None and rset.rowcount:
-        return 1
-    return 0
-
-@objectify_selector
-@lltrace
-def empty_rset(cls, req, rset=None, **kwargs):
-    """accept empty result set"""
-    if rset is not None and rset.rowcount == 0:
-        return 1
-    return 0
-
-@objectify_selector
-@lltrace
-def one_line_rset(cls, req, rset=None, row=None, **kwargs):
-    """if row is specified, accept result set with a single line of result,
-    else accepts anyway
-    """
-    if rset is not None and (row is not None or rset.rowcount == 1):
-        return 1
-    return 0
-
-
-class multi_lines_rset(Selector):
-    def __init__(self, nb=None):
-        self.expected = nb
-
-    def match_expected(self, num):
-        if self.expected is None:
-            return num > 1
-        return num == self.expected
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        return rset is not None and self.match_expected(rset.rowcount)
-
-
-class multi_columns_rset(multi_lines_rset):
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        return rset and self.match_expected(len(rset.rows[0])) or 0 # *must not* return None
-
-
-@objectify_selector
-@lltrace
-def paginated_rset(cls, req, rset=None, **kwargs):
-    """accept result set with more lines than the page size.
-
-    Page size is searched in (respecting order):
-    * a page_size argument
-    * a page_size form parameters
-    * the navigation.page-size property
-    """
-    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 is None or rset.rowcount <= page_size:
-        return 0
-    return 1
-
-@objectify_selector
-@lltrace
-def sorted_rset(cls, req, rset=None, **kwargs):
-    """accept sorted result set"""
-    rqlst = rset.syntax_tree()
-    if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
-        return 0
-    return 2
-
-@objectify_selector
-@lltrace
-def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
-    """accept result set where entities in the specified column (or 0) are all
-    of the same type
-    """
-    if rset is None:
-        return 0
-    if len(rset.column_types(col)) != 1:
-        return 0
-    return 1
-
-
-class multi_etypes_rset(multi_lines_rset):
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, col=0, **kwargs):
-        return rset and self.match_expected(len(rset.column_types(col)))
-
-
-class non_final_entity(EClassSelector):
-    """accept if entity type found in the result set is non final.
-
-    See `EClassSelector` documentation for behaviour when row is not specified.
-    """
-    def score(self, cls, req, etype):
-        if etype in BASE_TYPES:
-            return 0
-        return 1
-
-
-@objectify_selector
-@lltrace
-def authenticated_user(cls, req, *args, **kwargs):
-    """accept if user is authenticated"""
-    if req.cnx.anonymous_connection:
-        return 0
-    return 1
-
-def anonymous_user():
-    return ~ authenticated_user()
-
-@objectify_selector
-@lltrace
-def primary_view(cls, req, rset=None, row=None, col=0, view=None, **kwargs):
-    """accept if view given as named argument is a primary view, or if no view
-    is given
-    """
-    if view is not None and not view.is_primary():
-        return 0
-    return 1
-
-@objectify_selector
-@lltrace
-def match_context_prop(cls, req, rset=None, row=None, col=0, context=None,
-                       **kwargs):
-    """accept if:
-    * no context given
-    * context (`basestring`) is matching the context property value for the
-      given cls
-    """
-    propval = req.property_value('%s.%s.context' % (cls.__registry__,
-                                                    cls.__regid__))
-    if not propval:
-        propval = cls.context
-    if context is not None and propval and context != propval:
-        return 0
-    return 1
-
-
-class match_search_state(Selector):
-    """accept if the current request search state is in one of the expected
-    states given to the initializer
-
-    :param expected: either 'normal' or 'linksearch' (eg searching for an
-                     object to create a relation with another)
+class ExpectedValueSelector(Selector):
+    """Take a list of expected values as initializer argument, check
+    _get_value method return one of these expected values.
     """
     def __init__(self, *expected):
         assert expected, self
@@ -465,212 +320,326 @@
                            ','.join(sorted(str(s) for s in self.expected)))
 
     @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **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
+    def __call__(self, cls, req, **kwargs):
+        if self._get_value(cls, req, **kwargs) in self.expected:
+            return 1
+        return 0
+
+    def _get_value(self, cls, req, **kwargs):
+        raise NotImplementedError()
 
 
-class match_form_params(match_search_state):
-    """accept if parameters specified as initializer arguments are specified
-    in request's form parameters
+# bare selectors ##############################################################
 
-    :param *expected: parameters (eg `basestring`) which are expected to be
-                      found in request's form parameters
+class match_kwargs(ExpectedValueSelector):
+    """Return non-zero score if parameter names specified as initializer
+    arguments are specified in the input context. When multiple parameters are
+    specified, all of them should be specified in the input context. Return a
+    score corresponding to the number of expected parameters.
     """
 
     @lltrace
-    def __call__(self, cls, req, *args, **kwargs):
-        score = 0
-        for param in self.expected:
-            if not param in req.form:
-                return 0
-            score += 1
-        return len(self.expected)
-
-
-class match_kwargs(match_search_state):
-    """accept if parameters specified as initializer arguments are specified
-    in named arguments given to the selector
-
-    :param *expected: parameters (eg `basestring`) which are expected to be
-                      found in named arguments (kwargs)
-    """
-
-    @lltrace
-    def __call__(self, cls, req, *args, **kwargs):
+    def __call__(self, cls, req, **kwargs):
         for arg in self.expected:
             if not arg in kwargs:
                 return 0
         return len(self.expected)
 
 
-class match_user_groups(match_search_state):
-    """accept if logged users is in at least one of the given groups. Returned
-    score is the number of groups in which the user is.
-
-    If the special 'owners' group is given:
-    * if row is specified check the entity at the given row/col is owned by the
-      logged user
-    * if row is not specified check all entities in col are owned by the logged
-      user
-
-    :param *required_groups: name of groups (`basestring`) in which the logged
-                             user should be
-    """
-
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        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
-
+class appobject_selectable(Selector):
+    """return 1 if another appobject is selectable using the same input context.
 
-class match_transition(match_search_state):
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
-        try:
-            # XXX check this is a transition that apply to the object?
-            if not kwargs['transition'].name in self.expected:
-                return 0
-        except KeyError:
-            return 0
-        return 1
-
-
-class match_view(match_search_state):
-    """accept if the current view is in one of the expected vid given to the
-    initializer
+    Initializer arguments:
+    * `registry`, a registry name
+    * `regid`, an object identifier in this registry
     """
-    @lltrace
-    def __call__(self, cls, req, rset=None, row=None, col=0, view=None, **kwargs):
-        if view is None or not view.__regid__ in self.expected:
-            return 0
-        return 1
-
-
-class appobject_selectable(Selector):
-    """accept with another appobject is selectable using selector's input
-    context.
-
-    :param registry: a registry name (`basestring`)
-    :param oid: an object identifier (`basestring`)
-    """
-    def __init__(self, registry, oid):
+    def __init__(self, registry, regid):
         self.registry = registry
-        self.oid = oid
+        self.regid = regid
 
     def __call__(self, cls, req, **kwargs):
         try:
-            req.vreg[self.registry].select(self.oid, req, **kwargs)
+            req.vreg[self.registry].select(self.regid, req, **kwargs)
             return 1
         except NoSelectableObject:
             return 0
 
 
-# not so basic selectors ######################################################
+# rset selectors ##############################################################
+
+@objectify_selector
+@lltrace
+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_selector
+@lltrace
+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_selector
+@lltrace
+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_selector
+@lltrace
+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_selector
+@lltrace
+def one_line_rset(cls, req, rset=None, row=None, **kwargs):
+    """Return 1 if the result set is of size 1 or if a specific row in the
+    result set is specified ('row' argument).
+    """
+    if rset is not None and (row is not None or rset.rowcount == 1):
+        return 1
+    return 0
+
+
+class multi_lines_rset(Selector):
+    """If `nb`is specified, return 1 if the result set has exactly `nb` row of
+    result. Else (`nb` is None), return 1 if the result set contains *at least*
+    two rows.
+    """
+    def __init__(self, nb=None):
+        self.expected = nb
+
+    def match_expected(self, num):
+        if self.expected is None:
+            return num > 1
+        return num == self.expected
+
+    @lltrace
+    def __call__(self, cls, req, rset=None, **kwargs):
+        return 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.
+    """
+
+    @lltrace
+    def __call__(self, cls, req, rset=None, **kwargs):
+        # 'or 0' since we *must not* return None
+        return rset and self.match_expected(len(rset.rows[0])) or 0
+
+
+@objectify_selector
+@lltrace
+def paginated_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for result set with more rows than a page size.
+
+    Page size is searched in (respecting order):
+    * a `page_size` argument
+    * a `page_size` form parameters
+    * the :ref:`navigation.page-size` property
+    """
+    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:
+        return 0
+    return 1
+
+
+@objectify_selector
+@lltrace
+def sorted_rset(cls, req, rset=None, **kwargs):
+    """Return 1 for sorted result set (e.g. from an RQL query containing an
+    :ref:ORDERBY clause.
+    """
+    if rset is None:
+        return 0
+    rqlst = rset.syntax_tree()
+    if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
+        return 0
+    return 2
+
+
+# XXX == multi_etypes_rset(1)
+@objectify_selector
+@lltrace
+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.
+    """
+
+    @lltrace
+    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
+
+
+# entity selectors #############################################################
+
+class non_final_entity(EClassSelector):
+    """Return 1 for entity of a non final entity type(s). Remember, "final"
+    entity types are String, Int, etc... This is equivalent to
+    `implements('Any')` but more optimized.
+
+    See :class:`~cubicweb.selectors.EClassSelector` 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
+
 
 class implements(ImplementsMixIn, EClassSelector):
-    """accept if entity classes found in the result set implements at least one
-    of the interfaces given as argument. Returned score is the number of
-    implemented interfaces.
-
-    See `EClassSelector` documentation for behaviour when row is not specified.
+    """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.
 
-    :param *expected_ifaces: expected interfaces. An interface may be a class
-                             or an entity type (e.g. `basestring`) in which case
-                             the associated class will be searched in the
-                             registry (at selection time)
+    Entity types should be given as string, the corresponding class will be
+    fetched from the entity types registry at selection time.
 
-    note: when interface is an entity class, the score will reflect class
-          proximity so the most specific object'll be selected
+    See :class:`~cubicweb.selectors.EClassSelector` 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.
     """
     def score_class(self, eclass, req):
         return self.score_interfaces(req, eclass, eclass)
 
 
-class specified_etype_implements(implements):
-    """accept if entity class specified using an 'etype' parameters in name
-    argument or request form implements at least one of the interfaces given as
-    argument. Returned score is the number of implemented interfaces.
+class score_entity(EntitySelector):
+    """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 selector that will usually interest you since it
+    allows a lot of things without having to write a specific selector.
+
+    See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
+    lookup / score rules according to the input context.
+    """
+    def __init__(self, scorefunc, once_is_enough=False):
+        super(score_entity, self).__init__(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 relation_possible(EntitySelector):
+    """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:
 
-    :param *expected_ifaces: expected interfaces. An interface may be a class
-                             or an entity type (e.g. `basestring`) in which case
-                             the associated class will be searched in the
-                             registry (at selection time)
+    * `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
 
-    note: when interface is an entity class, the score will reflect class
-          proximity so the most specific object'll be selected
+    * `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.selectors.EntitySelector` behaviour
+    while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
+    one. See those classes documentation for entity lookup / score rules
+    according to the input context.
     """
 
-    @lltrace
-    def __call__(self, cls, req, *args, **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 relation_possible(EClassSelector):
-    """accept if entity class found in the result set support the relation.
-
-    See `EClassSelector` documentation for behaviour when row is not specified.
-
-    :param rtype: a relation type (`basestring`)
-    :param role: the role of the result set entity in the relation. 'subject' or
-                 'object', default to 'subject'.
-    :param target_type: if specified, check the relation's end may be of this
-                        target type (`basestring`)
-    :param action: a relation schema action (one of 'read', 'add', 'delete')
-                   which must be granted to the logged user, else a 0 score will
-                   be returned
-    """
     def __init__(self, rtype, role='subject', target_etype=None,
-                 action='read', once_is_enough=False):
-        super(relation_possible, self).__init__(once_is_enough)
+                 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
 
-    def score_class(self, eclass, req):
+    # hack hack hack
+    def __call__(self, cls, req, **kwargs):
+        if self.strict:
+            return EntitySelector.__call__(self, cls, req, **kwargs)
+        return EClassSelector.__call__(self, cls, req, **kwargs)
+
+    def score(self, *args):
+        if self.strict:
+            return EntitySelector.score(self, *args)
+        return EClassSelector.score(self, *args)
+
+    def _get_rschema(self, eclass):
         eschema = eclass.e_schema
         try:
             if self.role == 'object':
-                rschema = eschema.objrels[self.rtype]
+                return eschema.objrels[self.rtype]
             else:
-                rschema = eschema.subjrels[self.rtype]
+                return eschema.subjrels[self.rtype]
         except KeyError:
-            return 0
+            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)
@@ -682,55 +651,10 @@
             return rschema.may_have_permission(self.action, req, eschema, self.role)
         return 1
 
-
-class partial_relation_possible(PartialSelectorMixIn, relation_possible):
-    """partial version of the relation_possible selector
-
-    The selector will look for class attributes to find its missing
-    information. The list of attributes required on the class
-    for this selector are:
-
-    - `rtype`: same as `rtype` parameter of the `relation_possible` selector
-
-    - `role`: this attribute will be passed to the `cubicweb.role` function
-      to determine the role of class in the relation
-
-    - `etype` (optional): the entity type on the other side of the relation
-
-    :param action: a relation schema action (one of 'read', 'add', 'delete')
-                   which must be granted to the logged user, else a 0 score will
-                   be returned
-    """
-    def __init__(self, action='read', once_is_enough=False):
-        super(partial_relation_possible, self).__init__(None, None, None,
-                                                        action, once_is_enough)
-
-    def complete(self, cls):
-        self.rtype = cls.rtype
-        self.role = role(cls)
-        self.target_etype = getattr(cls, 'etype', None)
-
-
-class may_add_relation(EntitySelector):
-    """accept if the relation can be added to an entity found in the result set
-    by the logged user.
-
-    See `EntitySelector` documentation for behaviour when row is not specified.
-
-    :param rtype: a relation type (`basestring`)
-    :param role: the role of the result set entity in the relation. 'subject' or
-                 'object', default to 'subject'.
-    """
-
-    def __init__(self, rtype, role='subject', target_etype=None,
-                 once_is_enough=False):
-        super(may_add_relation, self).__init__(once_is_enough)
-        self.rtype = rtype
-        self.role = role
-        self.target_etype = target_etype
-
     def score_entity(self, entity):
-        rschema = entity._cw.vreg.schema.rschema(self.rtype)
+        rschema = self._get_rschema(entity)
+        if rschema is None:
+            return 0 # relation not supported
         if self.target_etype is not None:
             rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
         if self.role == 'subject':
@@ -741,47 +665,50 @@
         return 1
 
 
-class partial_may_add_relation(PartialSelectorMixIn, may_add_relation):
-    """partial version of the may_add_relation selector
-
-    The selector will look for class attributes to find its missing
-    information. The list of attributes required on the class
-    for this selector are:
-
-    - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+class partial_relation_possible(PartialSelectorMixIn, relation_possible):
+    """Same as :class:~`cubicweb.selectors.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.
 
-    - `role`: this attribute will be passed to the `cubicweb.role` function
-      to determine the role of class in the relation.
-
-    :param action: a relation schema action (one of 'read', 'add', 'delete')
-                   which must be granted to the logged user, else a 0 score will
-                   be returned
+    This is useful to predefine selector of an abstract class designed to be
+    customized.
     """
-    def __init__(self, once_is_enough=False):
-        super(partial_may_add_relation, self).__init__(None, once_is_enough=once_is_enough)
+    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, 'etype', None)
+        if self.target_etype is not None:
+            warn('[3.6] please rename etype to target_etype on %s' % cls,
+                 DeprecationWarning)
+        else:
+            self.target_etype = getattr(cls, 'target_etype', None)
 
 
 class has_related_entities(EntitySelector):
-    """accept if entity found in the result set has some linked entities using
-    the specified relation (optionaly filtered according to the specified target
-    type). Checks first if the relation is possible.
+    """Return 1 if entity support the specified relation and has some linked
+    entities by this relation , optionaly filtered according to the specified
+    target type.
 
-    See `EntitySelector` documentation for behaviour when row is not specified.
+    The relation is specified by the following initializer arguments:
+
+    * `rtype`, the name of the relation
 
-    :param rtype: a relation type (`basestring`)
-    :param role: the role of the result set entity in the relation. 'subject' or
-                 'object', default to 'subject'.
-    :param target_type: if specified, check the relation's end may be of this
-                        target type (`basestring`)
+    * `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.selectors.EntitySelector` documentation for entity
+    lookup / score rules according to the input context.
     """
-    def __init__(self, rtype, role='subject', target_etype=None,
-                 once_is_enough=False):
-        super(has_related_entities, self).__init__(once_is_enough)
+    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
@@ -797,52 +724,52 @@
 
 
 class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
-    """partial version of the has_related_entities selector
-
-    The selector will look for class attributes to find its missing
-    information. The list of attributes required on the class
-    for this selector are:
-
-    - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+    """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
+    for attributes of the selected class to get information which is otherwise
+    expected by the initializer.
 
-    - `role`: this attribute will be passed to the `cubicweb.role` function
-      to determine the role of class in the relation.
-
-    - `etype` (optional): the entity type on the other side of the relation
+    This is useful to predefine selector of an abstract class designed to be
+    customized.
+    """
+    def __init__(self, **kwargs):
+        super(partial_has_related_entities, self).__init__(None, None, None,
+                                                           **kwargs)
 
-    :param action: a relation schema action (one of 'read', 'add', 'delete')
-                   which must be granted to the logged user, else a 0 score will
-                   be returned
-    """
-    def __init__(self, once_is_enough=False):
-        super(partial_has_related_entities, self).__init__(None, None,
-                                                           None, once_is_enough)
     def complete(self, cls):
         self.rtype = cls.rtype
         self.role = role(cls)
         self.target_etype = getattr(cls, 'etype', None)
+        if self.target_etype is not None:
+            warn('[3.6] please rename etype to target_etype on %s' % cls,
+                 DeprecationWarning)
+        else:
+            self.target_etype = getattr(cls, 'target_etype', None)
 
 
 class has_permission(EntitySelector):
-    """accept if user has the permission to do the requested action on a result
-    set entity.
+    """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').
 
-    * if row is specified, return 1 if user has the permission on the entity
-      instance found in the specified cell
-    * else return a positive score if user has the permission for every entity
-      in the found in the specified column
+    Here are entity lookup / scoring rules:
+
+    * if `entity` is specified, check permission is granted for this entity
 
-    note: None values (resulting from some outer join in the query) are not
-          considered.
+    * elif `row` is specified, check permission is granted for the entity found
+      in the specified cell
 
-    :param action: an entity schema action (eg 'read'/'add'/'delete'/'update')
+    * 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, once_is_enough=False):
-        super(has_permission, self).__init__(once_is_enough)
+    def __init__(self, action):
         self.action = action
 
+    # don't use EntitySelector.__call__ but this optimized implementation to
+    # avoid considering each entity when it's not necessary
     @lltrace
     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
         user = req.user
@@ -882,32 +809,47 @@
 
 
 class has_add_permission(EClassSelector):
-    """accept if logged user has the add permission on entity class found in the
-    result set, and class is not a strict subobject.
+    """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.
 
-    See `EClassSelector` documentation for behaviour when row is 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.selectors.EClassSelector` documentation for entity
+    class lookup / score rules according to the input context when `etype` is
+    not specified.
     """
-    def score(self, cls, req, etype):
-        eschema = req.vreg.schema.eschema(etype)
-        if not (eschema.final or eschema.is_subobject(strict=True)) \
-               and eschema.has_perm(req, 'add'):
-            return 1
-        return 0
+    def __init__(self, etype=None, **kwargs):
+        super(has_add_permission, self).__init__(**kwargs)
+        self.etype = etype
+
+    @lltrace
+    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(EntitySelector):
-    """accept if an arbitrary rql return some results for an eid found in the
-    result set. Returned score is the number of items returned by the rql
+    """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.
 
-    See `EntitySelector` documentation for behaviour when row is not specified.
+    `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.
 
-    :param expression: basestring containing an rql expression, which should use
-                       X variable to represent the context entity and may use U
-                       to represent the logged user
-
-    return the sum of the number of items returned by the rql condition as score
-    or 0 at the first entity scoring to zero.
+    See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
+    lookup / score rules according to the input context.
     """
     def __init__(self, expression, once_is_enough=False):
         super(rql_condition, self).__init__(once_is_enough)
@@ -917,6 +859,9 @@
             rql = 'Any X WHERE X eid %%(x)s, %s' % expression
         self.rql = rql
 
+    def __repr__(self):
+        return u'<rql_condition "%s" at %x>' % (self.rql, id(self))
+
     def score(self, req, rset, row, col):
         try:
             return len(req.execute(self.rql, {'x': rset[row][col],
@@ -924,29 +869,210 @@
         except Unauthorized:
             return 0
 
-    def __repr__(self):
-        return u'<rql_condition "%s" at %x>' % (self.rql, id(self))
+# logged user selectors ########################################################
+
+@objectify_selector
+@lltrace
+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.cnx.anonymous_connection:
+        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(ExpectedValueSelector):
+    """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
+    """
+
+    @lltrace
+    def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+        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 selectors ########################################################
+
+@objectify_selector
+@lltrace
+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 selector 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
+
+
+class match_view(ExpectedValueSelector):
+    """Return 1 if a view is specified an as its registry id is in one of the
+    expected view id given to the initializer.
+    """
+    @lltrace
+    def __call__(self, cls, req, view=None, **kwargs):
+        if view is None or not view.__regid__ in self.expected:
+            return 0
+        return 1
 
 
-class score_entity(EntitySelector):
-    """accept if some arbitrary function return a positive score for an entity
-    found in the result set.
+@objectify_selector
+@lltrace
+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 selector
+
+    * the appobject's context property value is None
+
+    This selector is usually used by contextual components that want to appears
+    in a configurable place.
+    """
+    if context is None:
+        return 0
+    propval = req.property_value('%s.%s.context' % (cls.__registry__,
+                                                    cls.__regid__))
+    if not propval:
+        propval = cls.context
+    if propval and context != propval:
+        return 0
+    return 1
+
 
-    See `EntitySelector` documentation for behaviour when row is not specified.
+class match_search_state(ExpectedValueSelector):
+    """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 selector is usually used by action that want to appears or not according
+    to the ui search state.
+    """
 
-    :param scorefunc: callable expected to take an entity as argument and to
-                      return a score >= 0
+    @lltrace
+    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(ExpectedValueSelector):
+    """Return non-zero score if parameter names specified as initializer
+    arguments are specified in request's form parameters. When multiple
+    parameters are specified, all of them should be found in req.form. Return a
+    score corresponding to the number of expected parameters.
     """
-    def __init__(self, scorefunc, once_is_enough=False):
-        super(score_entity, self).__init__(once_is_enough)
-        def intscore(*args, **kwargs):
-            score = scorefunc(*args, **kwargs)
-            if not score:
+
+    @lltrace
+    def __call__(self, cls, req, **kwargs):
+        for param in self.expected:
+            if not param in req.form:
                 return 0
-            if isinstance(score, (int, long)):
+        return len(self.expected)
+
+
+class specified_etype_implements(implements):
+    """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 or implements at
+    least one of the given interfaces. 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.
+
+    .. note:: when interface is an entity class, the score will reflect class
+              proximity so the most specific object will be selected.
+
+    This selector is usually used by views holding entity creation forms (since
+    we've no result set to work on).
+    """
+
+    @lltrace
+    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
+
+
+# Other selectors ##############################################################
+
+
+class match_transition(ExpectedValueSelector):
+    """Return 1 if a `transition` argument is found in the input context which
+    has a `.name` attribute matching one of the expected names given to the
+    initializer.
+    """
+    @lltrace
+    def __call__(self, cls, req, transition=None, **kwargs):
+        # XXX check this is a transition that apply to the object?
+        if transition is not None and getattr(transition, 'name', None) in self.expected:
             return 1
-        self.score_entity = intscore
+        return 0
+
 
 ## deprecated stuff ############################################################
 
--- a/web/action.py	Fri Feb 05 08:11:38 2010 +0100
+++ b/web/action.py	Fri Feb 05 08:53:33 2010 +0100
@@ -10,7 +10,7 @@
 
 from cubicweb import target
 from cubicweb.selectors import (partial_relation_possible, match_search_state,
-                                one_line_rset, partial_may_add_relation, yes)
+                                one_line_rset, yes)
 from cubicweb.appobject import AppObject
 
 
@@ -76,22 +76,28 @@
 
 
 class LinkToEntityAction(Action):
-    """base class for actions consisting to create a new object
-    with an initial relation set to an entity.
-    Additionaly to EntityAction behaviour, this class is parametrized
-    using .etype, .rtype and .target attributes to check if the
-    action apply and if the logged user has access to it
+    """base class for actions consisting to create a new object with an initial
+    relation set to an entity.
+
+    Additionaly to EntityAction behaviour, this class is parametrized using
+    .rtype, .role and .target_etype attributes to check if the action apply and
+    if the logged user has access to it (see
+    :class:`~cubicweb.selectors.partial_relation_possible` selector
+    documentation for more information).
     """
     __select__ = (match_search_state('normal') & one_line_rset()
-                  & partial_relation_possible(action='add')
-                  & partial_may_add_relation())
+                  & partial_relation_possible(action='add', strict=True))
 
     submenu = 'addrelated'
 
     def url(self):
-        current_entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
-        linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self))
-        return self._cw.build_url('add/%s' % self.etype, __linkto=linkto,
-                                  __redirectpath=current_entity.rest_path(), # should not be url quoted!
+        try:
+            ttype = self.etype # deprecated in 3.6, already warned by the selector
+        except AttributeError:
+            ttype = self.target_etype
+        entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
+        linkto = '%s:%s:%s' % (self.rtype, entity.eid, target(self))
+        return self._cw.build_url('add/%s' % ttype, __linkto=linkto,
+                                  __redirectpath=entity.rest_path(),
                                   __redirectvid=self._cw.form.get('__redirectvid', ''))
 
--- a/web/views/actions.py	Fri Feb 05 08:11:38 2010 +0100
+++ b/web/views/actions.py	Fri Feb 05 08:53:33 2010 +0100
@@ -211,7 +211,7 @@
     __regid__ = 'addentity'
     __select__ = (action.Action.__select__ &
                   (addable_etype_empty_rset()
-                   | (multi_lines_rset() & one_etype_rset & has_add_permission()))
+                   | (multi_lines_rset() & one_etype_rset() & has_add_permission()))
                   )
 
     category = 'moreactions'