major selector refactoring (mostly to avoid looking for select parameters on the target class), start accept / interface unification) tls-sprint
authorsylvain.thenault@logilab.fr
Mon, 16 Feb 2009 18:26:13 +0100
branchtls-sprint
changeset 631 99f5852f8604
parent 630 66ff0b2f7d03
child 632 3a394a90b702
major selector refactoring (mostly to avoid looking for select parameters on the target class), start accept / interface unification)
common/appobject.py
common/entity.py
common/registerers.py
common/selectors.py
cwvreg.py
test/unittest_rset.py
test/unittest_vregistry.py
vregistry.py
web/action.py
web/views/__init__.py
web/views/actions.py
web/views/baseviews.py
web/views/bookmark.py
web/views/embedding.py
web/views/euser.py
web/views/idownloadable.py
web/views/management.py
--- a/common/appobject.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/common/appobject.py	Mon Feb 16 18:26:13 2009 +0100
@@ -304,91 +304,6 @@
         if first in ('insert', 'set', 'delete'):
             raise Unauthorized(self.req._('only select queries are authorized'))
 
-    # .accepts handling utilities #############################################
-    
-    accepts = ('Any',)
-
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        """apply the following rules:
-        * if row is None, return the sum of values returned by the method
-          for each entity's type in the result set. If any score is 0,
-          return 0.
-        * if row is specified, return the value returned by the method with
-          the entity's type of this row
-        """
-        if row is None:
-            score = 0
-            for etype in rset.column_types(0):
-                accepted = cls.accept(req.user, etype)
-                if not accepted:
-                    return 0
-                score += accepted
-            return score
-        return cls.accept(req.user, rset.description[row][col or 0])
-        
-    @classmethod
-    def accept(cls, user, etype):
-        """score etype, returning better score on exact match"""
-        if 'Any' in cls.accepts:
-            return 1
-        eschema = cls.schema.eschema(etype)
-        matching_types = [e.type for e in eschema.ancestors()]
-        matching_types.append(etype)
-        for index, basetype in enumerate(matching_types):
-            if basetype in cls.accepts:
-                return 2 + index
-        return 0
-    
-    # .rtype  handling utilities ##############################################
-    
-    @classmethod
-    def relation_possible(cls, etype):
-        """tell if a relation with etype entity is possible according to 
-        mixed class'.etype, .rtype and .target attributes
-
-        XXX should probably be moved out to a function
-        """
-        schema = cls.schema
-        rtype = cls.rtype
-        eschema = schema.eschema(etype)
-        if hasattr(cls, 'role'):
-            role = cls.role
-        elif cls.target == 'subject':
-            role = 'object'
-        else:
-            role = 'subject'
-        # check if this relation is possible according to the schema
-        try:
-            if role == 'object':
-                rschema = eschema.object_relation(rtype)
-            else:
-                rschema = eschema.subject_relation(rtype)
-        except KeyError:
-            return False            
-        if hasattr(cls, 'etype'):
-            letype = cls.etype
-            try:
-                if role == 'object':
-                    return etype in rschema.objects(letype)
-                else:
-                    return etype in rschema.subjects(letype)
-            except KeyError, ex:
-                return False
-        return True
-
-    
-    # XXX deprecated (since 2.43) ##########################
-    
-    @obsolete('use req.datadir_url')
-    def datadir_url(self):
-        """return url of the application's data directory"""
-        return self.req.datadir_url
-
-    @obsolete('use req.external_resource()')
-    def external_resource(self, rid, default=_MARKER):
-        return self.req.external_resource(rid, default)
-
         
 class AppObject(AppRsetObject):
     """base class for application objects which are not selected
--- a/common/entity.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/common/entity.py	Mon Feb 16 18:26:13 2009 +0100
@@ -10,6 +10,7 @@
 from logilab.common.compat import all
 from logilab.common.decorators import cached
 from logilab.mtconverter import TransformData, TransformError
+
 from rql.utils import rqlvar_maker
 
 from cubicweb import Unauthorized
--- a/common/registerers.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/common/registerers.py	Mon Feb 16 18:26:13 2009 +0100
@@ -5,7 +5,7 @@
 to the application's schema or to already registered object
 
 :organization: Logilab
-:copyright: 2006-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2006-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -84,13 +84,14 @@
         # remove it latter if no object is implementing accepted interfaces
         if _accepts_interfaces(self.vobject):
             return self.vobject
-        if not 'Any' in self.vobject.accepts:
-            for ertype in self.vobject.accepts:
-                if ertype in self.schema:
-                    break
-            else:
-                self.skip()
-                return None
+# XXX no more .accepts attribute    
+#         if not 'Any' in self.vobject.accepts:
+#             for ertype in self.vobject.accepts:
+#                 if ertype in self.schema:
+#                     break
+#             else:
+#                 self.skip()
+#                 return None
         for required in getattr(self.vobject, 'requires', ()):
             if required not in self.schema:
                 self.skip()
--- a/common/selectors.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/common/selectors.py	Mon Feb 16 18:26:13 2009 +0100
@@ -43,10 +43,13 @@
 
 from logilab.common.compat import all
 from logilab.common.deprecation import deprecated_function
+from logilab.common.interface import implements as implements_iface
+
+from yams import BASE_TYPES
 
 from cubicweb import Unauthorized, NoSelectableObject, role
+from cubicweb.vregistry import NoSelectableObject, Selector, chainall, chainfirst
 from cubicweb.cwvreg import DummyCursorError
-from cubicweb.vregistry import chainall, chainfirst, NoSelectableObject
 from cubicweb.cwconfig import CubicWebConfiguration
 from cubicweb.schema import split_expression
 
@@ -59,9 +62,15 @@
     if CubicWebConfiguration.mode == 'installed':
         return selector
     def traced(cls, *args, **kwargs):
+        if isinstance(cls, Selector):
+            selname = cls.__class__.__name__
+            oid = args[0].id
+        else:
+            selname = selector.__name__
+            oid = cls.id
         ret = selector(cls, *args, **kwargs)
-        if TRACED_OIDS == 'all' or cls.id in TRACED_OIDS:
-            SELECTOR_LOGGER.warning('selector %s returned %s for %s', selector.__name__, ret, cls)
+        if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
+            SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
         return ret
     traced.__name__ = selector.__name__
     return traced
@@ -174,7 +183,7 @@
 largerset_selector = deprecated_function(paginated_rset)
 
 @lltrace
-def sorted_rset(cls, req, rset, row=None, col=None, **kwargs):
+def sorted_rset(cls, req, rset, row=None, col=0, **kwargs):
     """accept sorted result set"""
     rqlst = rset.syntax_tree()
     if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
@@ -202,21 +211,24 @@
     return 0
 multitype_selector = deprecated_function(two_etypes_rset)
 
-@lltrace
-def match_search_state(cls, req, rset, row=None, col=None, **kwargs):
-    """checks if the current search state is in a .search_states attribute of
-    the wrapped class
+
+class match_search_state(Selector):
+    def __init__(self, *expected_states):
+        self.expected_states = expected_states
+        
+    def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+        """checks if the current request search state is in one of the expected states
+        the wrapped class
 
-    search state should be either 'normal' or 'linksearch' (eg searching for an
-    object to create a relation with another)
-    """
-    try:
-        if not req.search_state[0] in cls.search_states:
-            return 0
-    except AttributeError:
-        return 1 # class doesn't care about search state, accept it
-    return 1
-searchstate_selector = deprecated_function(match_search_state)
+        search state should be either 'normal' or 'linksearch' (eg searching for an
+        object to create a relation with another)
+        """
+        try:
+            if not req.search_state[0] in cls.search_states:
+                return 0
+        except AttributeError:
+            return 1 # class doesn't care about search state, accept it
+        return 1
 
 @lltrace
 def anonymous_user(cls, req, *args, **kwargs):
@@ -258,46 +270,331 @@
     return 1
 kwargs_selector = deprecated_function(match_kwargs)
 
+# abstract selectors ##########################################################
+
+class EClassSelector(Selector):
+    """abstract class for selectors working on the entity classes of the result
+    set
+    """
+    once_is_enough = False
+    
+    @lltrace
+    def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+        if not rset:
+            return 0
+        score = 0
+        if row is None:
+            for etype in rset.column_types(col):
+                if etype is None: # outer join
+                    continue
+                if etype in BASE_TYPES:
+                    return 0
+                escore = self.score_class(cls.vreg.etype_class(etype), req)
+                if not escore:
+                    return 0
+                elif self.once_is_enough:
+                    return escore
+                score += escore
+        else:
+            etype = rset.description[row][col]
+            if etype is not None and not etype in BASE_TYPES:
+                score = self.score_class(cls.vreg.etype_class(etype), req)
+        return score and (score + 1)
+
+    def score_class(self, eclass, req):
+        raise NotImplementedError()
+
+
+class EntitySelector(Selector):
+    """abstract class for selectors working on the entity instances of the
+    result set
+    """
+    @lltrace
+    def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+        if not rset:
+            return 0
+        score = 0
+        if row is None:
+            for row, rowvalue in enumerate(rset.rows):
+                if rowvalue[col] is None: # outer join
+                    continue
+                try:
+                    escore = self.score_entity(rset.get_entity(row, col))
+                except NotAnEntity:
+                    return 0
+                if not escore:
+                    return 0
+                score += escore
+        else:
+            etype = rset.description[row][col]
+            if etype is not None: # outer join
+                try:
+                    score = self.score_entity(rset.get_entity(row, col))
+                except NotAnEntity:
+                    return 0
+        return score and (score + 1)
+
+    def score_entity(self, entity):
+        raise NotImplementedError()
 
 # not so basic selectors ######################################################
 
+class implements(EClassSelector):
+    """initializer takes a list of interfaces or entity types as argument
+    
+    * if row is None, return the number of implemented interfaces for each
+      entity's class in the result set at the specified column (or column 0).
+      If any class has no matching interface, return 0.
+    * if row is specified, return number of implemented interfaces by the
+      entity's class at this row (and column)
+
+    if some interface is an entity class, the score will reflect class
+    proximity so the most specific object'll be selected
+    """
+
+    def __init__(self, *expected_ifaces):
+        self.expected_ifaces = expected_ifaces
+
+    def score_class(self, eclass, req):
+        score = 0
+        for iface in self.expected_ifaces:
+            if isinstance(iface, basestring):
+                # entity type
+                iface = eclass.vreg.etype_class(iface)
+            if implements_iface(eclass, iface):
+                score += 1
+                if getattr(iface, '__registry__', None) == 'etypes':
+                    # adjust score if the interface is an entity class
+                    if iface is eclass:
+                        score += len(eclass.e_schema.ancestors()) + 1
+                    else:
+                        parents = [e.type for e in eclass.e_schema.ancestors()]
+                        for index, etype in enumerate(reversed(parents)):
+                            basecls = eclass.vreg.etype_class(etype)
+                            if iface is basecls:
+                                score += index + 1
+                                break
+        return score
+
+
+class relation_possible(EClassSelector):
+    """initializer takes relation name as argument and an optional role (default
+      as subject) and target type (default to unspecified)
+      
+    * if row is None, return 1 if every entity's class in the result set at the
+      specified column (or column 0) may have this relation (as role). If target
+      type is specified, check the relation's end may be of this target type.
+      
+    * if row is specified, check relation is supported by the entity's class at
+      this row (and column)
+    """
+    def __init__(self, rtype, role='subject', target_etype=None,
+                 permission='read', once_is_enough=False):
+        self.rtype = rtype
+        self.role = role
+        self.target_etype = target_etype
+        self.permission = permission
+        self.once_is_enough = once_is_enough
+
+    @lltrace
+    def __call__(self, cls, *args, **kwargs):
+        rschema = cls.schema.rschema(self.rtype)
+        if not (rschema.has_perm(req, self.permission)
+                or rschema.has_local_role(self.permission)):
+            return 0
+        return super(relation_possible, self)(cls, *args, **kwargs)
+        
+    def score_class(self, eclass, req):
+        eschema = eclass.e_schema
+        try:
+            if self.role == 'object':
+                rschema = eschema.object_relation(self.rtype)
+            else:
+                rschema = eschema.subject_relation(self.rtype)
+        except KeyError:
+            return 0
+        if self.target_etype is not None:
+            try:
+                if self.role == 'object':
+                    return self.target_etype in rschema.objects(eschema)
+                else:
+                    return self.target_etype in rschema.subjects(eschema)
+            except KeyError, ex:
+                return 0
+        return 1
+
+
+class non_final_entity(EClassSelector):
+    """initializer takes no argument
+
+    * if row is None, return 1 if there are only non final entity's class in the
+      result set at the specified column (or column 0)
+    * if row is specified, return 1 if entity's class at this row (and column)
+      isn't final
+    """
+    def score_class(self, eclass, req):
+        return int(not eclass.e_schema.is_final())
+
+
+class match_user_groups(Selector):
+    """initializer takes users group as argument
+
+    * check logged user is in one of the given groups. If special 'owners' group
+      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
+    """
+    
+    def __init__(self, *required_groups):
+        self.required_groups = required_groups
+    
+    @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.require_groups)
+        score = user.matching_groups(self.require_groups)
+        if not score and 'owners' in self.require_groups and rset:
+            nbowned = 0
+            if row is not None:
+                if not user.owns(rset[row][col]):
+                    return 0
+                score = 1
+            else:
+                score = all(user.owns(r[col or 0]) for r in rset)
+        return 0
+
+
+class has_editable_relation(EntitySelector):
+    """initializer takes no argument
+
+    * if row is specified check the entity at the given row/col has some
+      relation editable by the logged user
+    * if row is not specified check all entities in col are owned have some
+      relation editable by the logged userlogged user
+    """
+        
+    def score_entity(self, entity):
+        # if user has no update right but it can modify some relation,
+        # display action anyway
+        for dummy in entity.srelations_by_category(('generic', 'metadata'),
+                                                   'add'):
+            return 1
+        for rschema, targetschemas, role in entity.relations_by_category(
+            ('primary', 'secondary'), 'add'):
+            if not rschema.is_final():
+                return 1
+        return 0
+
+
+class may_add_relation(EntitySelector):
+    """initializer a relation type and optional role (default to 'subject') as
+    argument
+
+    if row is specified check the relation may be added to the entity at the
+    given row/col (if row specified) or to every entities in the given col (if
+    row is not specified)
+    """
+    
+    def __init__(self, rtype, role='subject'):
+        self.rtype = rtype
+        self.role = role
+        
+    def score_entity(self, entity):
+        rschema = entity.schema.rschema(self.rtype)
+        if self.role == 'subject':
+            if not rschema.has_perm(req, 'add', fromeid=entity.eid):
+                return False
+        elif not rschema.has_perm(req, 'add', toeid=entity.eid):
+            return False
+        return True
+
+        
+class has_permission(EntitySelector):
+    """initializer takes a schema action (eg 'read'/'add'/'delete'/'update') as
+    argument
+
+    * if row is specified check user has permission to do the requested action
+      on the entity at the given row/col
+    * if row is specified check user has permission to do the requested action
+      on all entities in the given col
+    """
+    def __init__(self, schema_action):
+        self.schema_action = schema_action
+        
+    @lltrace
+    def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
+        user = req.user
+        action = self.schema_action
+        if row is None:
+            score = 0
+            need_local_check = [] 
+            geteschema = cls.schema.eschema
+            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 += accepted
+            if need_local_check:
+                # check local role for entities of necessary types
+                for i, row in enumerate(rset):
+                    if not rset.description[i][0] in need_local_check:
+                        continue
+                    if not self.score_entity(rset.get_entity(i, col)):
+                        return 0
+                    score += 1
+            return score
+        if rset.description[row][col] in BASE_TYPES:
+            return 0
+        return self.score_entity(rset.get_entity(row, col))
+    
+    def score_entity(self, entity):
+        if entity.has_perm(self.schema_action):
+            return 1
+        return 0
+
+
+class has_add_permission(EClassSelector):
+    
+    def score_class(self, eclass, req):
+        eschema = eclass.e_schema
+        if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
+               and eschema.has_perm(req, 'add'):
+            return 1
+        return 0
+
+        
+class score_entity(EntitySelector):
+    def __init__(self, scorefunc):
+        self.score_entity = scorefunc
+
+# XXX not so basic selectors ######################################################
+
 @lltrace
 def accept_etype(cls, req, *args, **kwargs):
     """check etype presence in request form *and* accepts conformance"""
-    if 'etype' not in req.form and 'etype' not in kwargs:
-        return 0
     try:
         etype = req.form['etype']
     except KeyError:
-        etype = kwargs['etype']
-    # value is a list or a tuple if web request form received several
-    # values for etype parameter
-    assert isinstance(etype, basestring), "got multiple etype parameters in req.form"
-    if 'Any' in cls.accepts:
-        return 1
-    # no Any found, we *need* exact match
-    if etype not in cls.accepts:
-        return 0
-    # exact match must return a greater value than 'Any'-match
-    return 2
+        try:
+            etype = kwargs['etype']
+        except KeyError:
+            return 0
+    return implements(*cls.accepts).score_class(cls.vreg.etype_class(etype), req)
 etype_form_selector = deprecated_function(accept_etype)
 
 @lltrace
-def _non_final_entity(cls, req, rset, row=None, col=None, **kwargs):
-    """accept non final entities
-    if row is not specified, use the first one
-    if col is not specified, use the first one
-    """
-    etype = rset.description[row or 0][col or 0]
-    if etype is None: # outer join
-        return 0
-    if cls.schema.eschema(etype).is_final():
-        return 0
-    return 1
-_nfentity_selector = deprecated_function(_non_final_entity)
-
-@lltrace
-def _rql_condition(cls, req, rset, row=None, col=None, **kwargs):
+def _rql_condition(cls, req, rset, row=None, col=0, **kwargs):
     """accept single entity result set if the entity match an rql condition
     """
     if cls.condition:
@@ -313,89 +610,9 @@
         
     return 1
 _rqlcondition_selector = deprecated_function(_rql_condition)
-
+        
 @lltrace
-def _implement_interface(cls, req, rset, row=None, col=None, **kwargs):
-    """accept uniform result sets, and apply the following rules:
-
-    * wrapped class must have a accepts_interfaces attribute listing the
-      accepted ORed interfaces
-    * if row is None, return the sum of values returned by the method
-      for each entity's class in the result set. If any score is 0,
-      return 0.
-    * if row is specified, return the value returned by the method with
-      the entity's class of this row
-    """
-    # XXX this selector can be refactored : extract the code testing
-    #     for entity schema / interface compliance
-    score = 0
-    # check 'accepts' to give priority to more specific classes
-    if row is None:
-        for etype in rset.column_types(col or 0):
-            eclass = cls.vreg.etype_class(etype)
-            escore = 0
-            for iface in cls.accepts_interfaces:
-                escore += iface.is_implemented_by(eclass)
-            if not escore:
-                return 0
-            score += escore
-            accepts = set(getattr(cls, 'accepts', ()))
-            # if accepts is defined on the vobject, eclass must match
-            if accepts:
-                eschema = eclass.e_schema
-                etypes = set([eschema] + eschema.ancestors())
-                if accepts & etypes:
-                    score += 2
-                elif 'Any' not in accepts:
-                    return 0
-        return score + 1
-    etype = rset.description[row][col or 0]
-    if etype is None: # outer join
-        return 0
-    eclass = cls.vreg.etype_class(etype)
-    for iface in cls.accepts_interfaces:
-        score += iface.is_implemented_by(eclass)
-    if score:
-        accepts = set(getattr(cls, 'accepts', ()))
-        # if accepts is defined on the vobject, eclass must match
-        if accepts:
-            eschema = eclass.e_schema
-            etypes = set([eschema] + eschema.ancestors())
-            if accepts & etypes:
-                score += 1
-            elif 'Any' not in accepts:
-                return 0
-        score += 1
-    return score
-_interface_selector = deprecated_function(_implement_interface)
-
-@lltrace
-def score_entity_selector(cls, req, rset, row=None, col=None, **kwargs):
-    if row is None:
-        rows = xrange(rset.rowcount)
-    else:
-        rows = (row,)
-    for row in rows:
-        try:
-            score = cls.score_entity(rset.get_entity(row, col or 0))
-        except DummyCursorError:
-            # get a dummy cursor error, that means we are currently
-            # using a dummy rset to list possible views for an entity
-            # type, not for an actual result set. In that case, we
-            # don't care of the value, consider the object as selectable
-            return 1
-        if not score:
-            return 0
-    return 1
-
-@lltrace
-def accept_rset(cls, req, rset, row=None, col=None, **kwargs):
-    """simply delegate to cls.accept_rset method"""
-    return cls.accept_rset(req, rset, row=row, col=col)
-accept_rset_selector = deprecated_function(accept_rset)
-
-@lltrace
-def but_etype(cls, req, rset, row=None, col=None, **kwargs):
+def but_etype(cls, req, rset, row=None, col=0, **kwargs):
     """restrict the searchstate_accept_one_selector to exclude entity's type
     refered by the .etype attribute
     """
@@ -405,7 +622,7 @@
 but_etype_selector = deprecated_function(but_etype)
 
 @lltrace
-def etype_rtype_selector(cls, req, rset, row=None, col=None, **kwargs):
+def etype_rtype_selector(cls, req, rset, row=None, col=0, **kwargs):
     """only check if the user has read access on the entity's type refered
     by the .etype attribute and on the relations's type refered by the
     .rtype attribute if set.
@@ -423,75 +640,11 @@
     return 1
 
 @lltrace
-def has_relation(cls, req, rset, row=None, col=None, **kwargs):
-    """check if the user has read access on the relations's type refered by the
-    .rtype attribute of the class, and if all entities types in the
-    result set has this relation.
-    """
-    if hasattr(cls, 'rtype'):
-        rschema = cls.schema.rschema(cls.rtype)
-        perm = getattr(cls, 'require_permission', 'read')
-        if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
-            return 0
-        if row is None:
-            for etype in rset.column_types(col or 0):
-                if not cls.relation_possible(etype):
-                    return 0
-        elif not cls.relation_possible(rset.description[row][col or 0]):
-            return 0
-    return 1
-accept_rtype_selector = deprecated_function(has_relation)
+def has_related_entities(cls, req, rset, row=None, col=0, **kwargs):
+    return bool(rset.get_entity(row or 0, col or 0).related(cls.rtype, role(cls)))
 
 @lltrace
-def one_has_relation(cls, req, rset, row=None, col=None, **kwargs):
-    """check if the user has read access on the relations's type refered by the
-    .rtype attribute of the class, and if at least one entity type in the
-    result set has this relation.
-    """
-    rschema = cls.schema.rschema(cls.rtype)
-    perm = getattr(cls, 'require_permission', 'read')
-    if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
-        return 0
-    if row is None:
-        for etype in rset.column_types(col or 0):
-            if cls.relation_possible(etype):
-                return 1
-    elif cls.relation_possible(rset.description[row][col or 0]):
-        return 1
-    return 0
-one_has_relation_selector = deprecated_function(one_has_relation)
-
-@lltrace
-def has_related_entities(cls, req, rset, row=None, col=None, **kwargs):
-    return bool(rset.get_entity(row or 0, col or 0).related(cls.rtype, role(cls)))
-
-
-@lltrace
-def match_user_group(cls, req, rset=None, row=None, col=None, **kwargs):
-    """select according to user's groups"""
-    if not cls.require_groups:
-        return 1
-    user = req.user
-    if user is None:
-        return int('guests' in cls.require_groups)
-    score = 0
-    if 'owners' in cls.require_groups and rset:
-        if row is not None:
-            eid = rset[row][col or 0]
-            if user.owns(eid):
-                score = 1
-        else:
-            score = all(user.owns(r[col or 0]) for r in rset)
-    score += user.matching_groups(cls.require_groups)
-    if score:
-        # add 1 so that an object with one matching group take priority
-        # on an object without require_groups
-        return score + 1 
-    return 0
-in_group_selector = deprecated_function(match_user_group)
-
-@lltrace
-def user_can_add_etype(cls, req, rset, row=None, col=None, **kwargs):
+def user_can_add_etype(cls, req, rset, row=None, col=0, **kwargs):
     """only check if the user has add access on the entity's type refered
     by the .etype attribute.
     """
@@ -501,7 +654,7 @@
 add_etype_selector = deprecated_function(user_can_add_etype)
 
 @lltrace
-def match_context_prop(cls, req, rset, row=None, col=None, context=None,
+def match_context_prop(cls, req, rset, row=None, col=0, context=None,
                        **kwargs):
     propval = req.property_value('%s.%s.context' % (cls.__registry__, cls.id))
     if not propval:
@@ -512,7 +665,7 @@
 contextprop_selector = deprecated_function(match_context_prop)
 
 @lltrace
-def primary_view(cls, req, rset, row=None, col=None, view=None,
+def primary_view(cls, req, rset, row=None, col=0, view=None,
                           **kwargs):
     if view is not None and not view.is_primary():
         return 0
@@ -533,39 +686,70 @@
     return selector
 
 
+
+# XXX DEPRECATED ##############################################################
+
+def nfentity_selector(cls, req, rset, row=None, col=0, **kwargs):
+    return non_final_entity()(cls, req, rset, row, col)
+nfentity_selector = deprecated_function(nfentity_selector)
+
+def implement_interface(cls, req, rset, row=None, col=0, **kwargs):
+    return implements(*cls.accepts_interfaces)(cls, req, rset, row, col)
+_interface_selector = deprecated_function(implement_interface)
+interface_selector = deprecated_function(implement_interface)
+implement_interface = deprecated_function(implement_interface)
+
+def searchstate_selector(cls, req, rset, row=None, col=0, **kwargs):
+    return match_search_state(cls.search_states)(cls, req, rset, row, col)
+searchstate_selector = deprecated_function(searchstate_selector)
+
+def match_user_group(cls, req, rset=None, row=None, col=0, **kwargs):
+    return match_user_groups(cls.require_groups)(cls, req, rset, row, col, **kwargs)
+in_group_selector = deprecated_function(match_user_group)
+match_user_group = deprecated_function(match_user_group)
+
+def has_relation(cls, req, rset, row=None, col=0, **kwargs):
+    return relation_possible(cls.rtype, role(cls), cls.etype,
+                             getattr(cls, 'require_permission', 'read'))(cls, req, rset, row, col, **kwargs)
+has_relation = deprecated_function(has_relation)
+
+def one_has_relation(cls, req, rset, row=None, col=0, **kwargs):
+    return relation_possible(cls.rtype, role(cls), cls.etype,
+                             getattr(cls, 'require_permission', 'read',
+                                     once_is_enough=True))(cls, req, rset, row, col, **kwargs)
+one_has_relation = deprecated_function(one_has_relation, 'use relation_possible selector')
+
+def accept_rset(cls, req, rset, row=None, col=0, **kwargs):
+    """simply delegate to cls.accept_rset method"""
+    return implements(*cls.accepts)(cls, req, rset, row=row, col=col)
+accept_rset_selector = deprecated_function(accept_rset)
+accept_rset = deprecated_function(accept_rset, 'use implements selector')
+
+accept = chainall(non_final_entity(), accept_rset, name='accept')
+accept_selector = deprecated_function(accept)
+accept = deprecated_function(accept, 'use implements selector')
+
 # compound selectors ##########################################################
 
-non_final_entity = chainall(nonempty_rset, _non_final_entity)
-non_final_entity.__name__ = 'non_final_entity'
-nfentity_selector = deprecated_function(non_final_entity)
-
-implement_interface = chainall(non_final_entity, _implement_interface)
-implement_interface.__name__ = 'implement_interface'
-interface_selector = deprecated_function(implement_interface)
-
-accept = chainall(non_final_entity, accept_rset)
-accept.__name__ = 'accept'
-accept_selector = deprecated_function(accept)
-
-accept_one = chainall(one_line_rset, accept)
-accept_one.__name__ = 'accept_one'
+accept_one = deprecated_function(chainall(one_line_rset, accept,
+                                          name='accept_one'))
 accept_one_selector = deprecated_function(accept_one)
 
-rql_condition = chainall(non_final_entity, one_line_rset, _rql_condition)
-rql_condition.__name__ = 'rql_condition'
+rql_condition = chainall(non_final_entity(), one_line_rset, _rql_condition,
+                         name='rql_condition')
 rqlcondition_selector = deprecated_function(rql_condition)
 
 
-searchstate_accept = chainall(nonempty_rset, match_search_state, accept)
-searchstate_accept.__name__ = 'searchstate_accept'
+searchstate_accept = chainall(nonempty_rset, match_search_state, accept,
+                              name='searchstate_accept')
 searchstate_accept_selector = deprecated_function(searchstate_accept)
 
 searchstate_accept_one = chainall(one_line_rset, match_search_state,
-                                  accept, _rql_condition)
-searchstate_accept_one.__name__ = 'searchstate_accept_one'
+                                  accept, _rql_condition,
+                                  name='searchstate_accept_one')
 searchstate_accept_one_selector = deprecated_function(searchstate_accept_one)
 
-searchstate_accept_one_but_etype = chainall(searchstate_accept_one, but_etype)
-searchstate_accept_one_but_etype.__name__ = 'searchstate_accept_one_but_etype'
+searchstate_accept_one_but_etype = chainall(searchstate_accept_one, but_etype,
+                                            name='searchstate_accept_one_but_etype')
 searchstate_accept_one_but_etype_selector = deprecated_function(
     searchstate_accept_one_but_etype)
--- a/cwvreg.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/cwvreg.py	Mon Feb 16 18:26:13 2009 +0100
@@ -1,7 +1,7 @@
 """extend the generic VRegistry with some cubicweb specific stuff
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -9,6 +9,7 @@
 from warnings import warn
 
 from logilab.common.decorators import cached, clear_cache
+from logilab.common.interface import extend
 
 from rql import RQLHelper
 
@@ -129,6 +130,8 @@
         default to a dump of the class registered for 'Any'
         """
         etype = str(etype)
+        if etype == 'Any':
+            return self.select(self.registry_objects('etypes', 'Any'), 'Any')
         eschema = self.schema.eschema(etype)
         baseschemas = [eschema] + eschema.ancestors()
         # browse ancestors from most specific to most generic and
@@ -136,12 +139,21 @@
         for baseschema in baseschemas:
             btype = str(baseschema)
             try:
-                return self.select(self.registry_objects('etypes', btype), etype)
+                cls = self.select(self.registry_objects('etypes', btype), etype)
+                break
             except ObjectNotFound:
                 pass
-        # no entity class for any of the ancestors, fallback to the default one
-        return self.select(self.registry_objects('etypes', 'Any'), etype)
-
+        else:
+            # no entity class for any of the ancestors, fallback to the default
+            # one
+            cls = self.select(self.registry_objects('etypes', 'Any'), etype)
+        # add class itself to the list of implemented interfaces, as well as the
+        # Any entity class so we can select according to class using the
+        # `implements` selector
+        extend(cls, cls)
+        extend(cls, self.etype_class('Any'))
+        return cls
+    
     def render(self, registry, oid, req, **context):
         """select an object in a given registry and render it
 
--- a/test/unittest_rset.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/test/unittest_rset.py	Mon Feb 16 18:26:13 2009 +0100
@@ -1,8 +1,11 @@
 # coding: utf-8
 """unit tests for module cubicweb.common.utils"""
+from __future__ import with_statement
 
 from logilab.common.testlib import TestCase, unittest_main
+
 from cubicweb.devtools.apptest import EnvBasedTC
+from cubicweb.common.selectors import traced_selection
 
 from urlparse import urlsplit
 from rql import parse
--- a/test/unittest_vregistry.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/test/unittest_vregistry.py	Mon Feb 16 18:26:13 2009 +0100
@@ -21,7 +21,8 @@
     def test_load(self):
         self.vreg.load_file(join(BASE, 'web', 'views'), 'euser.py')
         self.vreg.load_file(join(BASE, 'web', 'views'), 'baseviews.py')
-        fpvc = [v for v in self.vreg.registry_objects('views', 'primary') if v.accepts[0] == 'EUser'][0]
+        fpvc = [v for v in self.vreg.registry_objects('views', 'primary')
+               i f v.__module__ == 'cubicweb.web.views.euser'][0]
         fpv = fpvc(None, None)
         # don't want a TypeError due to super call
         self.assertRaises(AttributeError, fpv.render_entity_attributes, None, None)
--- a/vregistry.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/vregistry.py	Mon Feb 16 18:26:13 2009 +0100
@@ -513,7 +513,7 @@
 
 # advanced selector building functions ########################################
 
-def chainall(*selectors):
+def chainall(*selectors, **kwargs):
     """return a selector chaining given selectors. If one of
     the selectors fail, selection will fail, else the returned score
     will be the sum of each selector'score
@@ -527,9 +527,11 @@
                 return 0
             score += partscore
         return score
+    if 'name' in kwargs:
+        selector.__name__ = kwargs['name']
     return selector
 
-def chainfirst(*selectors):
+def chainfirst(*selectors, **kwargs):
     """return a selector chaining given selectors. If all
     the selectors fail, selection will fail, else the returned score
     will be the first non-zero selector score
@@ -541,10 +543,13 @@
             if partscore:
                 return partscore
         return 0
+    if 'name' in kwargs:
+        selector.__name__ = kwargs['name']
     return selector
 
 
 # selector base classes and operations ########################################
+
 class Selector(object):
     """base class for selector classes providing implementation
     for operators ``&`` and ``|``
--- a/web/action.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/action.py	Mon Feb 16 18:26:13 2009 +0100
@@ -1,14 +1,15 @@
 """abstract action classes for CubicWeb web client
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
 
+from cubicweb import target
 from cubicweb.common.appobject import AppRsetObject
 from cubicweb.common.registerers import action_registerer
-from cubicweb.common.selectors import add_etype_selector, \
+from cubicweb.common.selectors import user_can_add_etype, \
      match_search_state, searchstate_accept_one, \
      searchstate_accept_one_but_etype
     
@@ -21,9 +22,7 @@
     """
     __registry__ = 'actions'
     __registerer__ = action_registerer
-    __selectors__ = (match_search_state,)
-    # by default actions don't appear in link search mode
-    search_states = ('normal',) 
+
     property_defs = {
         'visible':  dict(type='Boolean', default=True,
                          help=_('display the action or not')),
@@ -37,53 +36,6 @@
     site_wide = True # don't want user to configuration actions eproperties
     category = 'moreactions'
     
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        user = req.user
-        action = cls.schema_action
-        if row is None:
-            score = 0
-            need_local_check = [] 
-            geteschema = cls.schema.eschema
-            for etype in rset.column_types(0):
-                accepted = cls.accept(user, etype)
-                if not accepted:
-                    return 0
-                if action:
-                    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 += accepted
-            if need_local_check:
-                # check local role for entities of necessary types
-                for i, row in enumerate(rset):
-                    if not rset.description[i][0] in need_local_check:
-                        continue
-                    if not cls.has_permission(rset.get_entity(i, 0), action):
-                        return 0
-                    score += 1
-            return score
-        col = col or 0
-        etype = rset.description[row][col]
-        score = cls.accept(user, etype)
-        if score and action:
-            if not cls.has_permission(rset.get_entity(row, col), action):
-                return 0
-        return score
-    
-    @classmethod
-    def has_permission(cls, entity, action):
-        """defined in a separated method to ease overriding (see ModifyAction
-        for instance)
-        """
-        return entity.has_perm(action)
-    
     def url(self):
         """return the url associated with this action"""
         raise NotImplementedError
@@ -94,6 +46,7 @@
         if self.category:
             return 'box' + self.category.capitalize()
 
+
 class UnregisteredAction(Action):
     """non registered action used to build boxes. Unless you set them
     explicitly, .vreg and .schema attributes at least are None.
@@ -115,7 +68,7 @@
     """link to the entity creation form. Concrete class must set .etype and
     may override .vid
     """
-    __selectors__ = (add_etype_selector, match_search_state)
+    __selectors__ = (user_can_add_etype,)
     vid = 'creation'
     etype = None
     
@@ -127,21 +80,9 @@
     """an action for an entity. By default entity actions are only
     displayable on single entity result if accept match.
     """
-    __selectors__ = (searchstate_accept_one,)
-    schema_action = None
-    condition = None
+    # XXX deprecate
     
-    @classmethod
-    def accept(cls, user, etype):
-        score = super(EntityAction, cls).accept(user, etype)
-        if not score:
-            return 0
-        # check if this type of entity has the necessary relation
-        if hasattr(cls, 'rtype') and not cls.relation_possible(etype):
-            return 0
-        return score
 
-    
 class LinkToEntityAction(EntityAction):
     """base class for actions consisting to create a new object
     with an initial relation set to an entity.
@@ -149,64 +90,19 @@
     using .etype, .rtype and .target attributes to check if the
     action apply and if the logged user has access to it
     """
-    etype = None
-    rtype = None
-    target = None
+    def my_selector(cls, req, rset, row=None, col=0, **kwargs):
+        return chainall(match_search_state('normal'),
+                        one_line_rset, accept,
+                        relation_possible(cls.rtype, role(cls), cls.etype,
+                                          permission='add'),
+                        may_add_relation(cls.rtype, role(cls)))
+    __selectors__ = my_selector,
+    
     category = 'addrelated'
-
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        entity = rset.get_entity(row or 0, col or 0)
-        # check if this type of entity has the necessary relation
-        if hasattr(cls, 'rtype') and not cls.relation_possible(entity.e_schema):
-            return 0
-        score = cls.accept(req.user, entity.e_schema)
-        if not score:
-            return 0
-        if not cls.check_perms(req, entity):
-            return 0
-        return score
-
-    @classmethod
-    def check_perms(cls, req, entity):
-        if not cls.check_rtype_perm(req, entity):
-            return False
-        # XXX document this:
-        # if user can create the relation, suppose it can create the entity
-        # this is because we usually can't check "add" permission before the
-        # entity has actually been created, and schema security should be
-        # defined considering this
-        #if not cls.check_etype_perm(req, entity):
-        #    return False
-        return True
-        
-    @classmethod
-    def check_etype_perm(cls, req, entity):
-        eschema = cls.schema.eschema(cls.etype)
-        if not eschema.has_perm(req, 'add'):
-            #print req.user.login, 'has no add perm on etype', cls.etype
-            return False
-        #print 'etype perm ok', cls
-        return True
-
-    @classmethod
-    def check_rtype_perm(cls, req, entity):
-        rschema = cls.schema.rschema(cls.rtype)
-        # cls.target is telling us if we want to add the subject or object of
-        # the relation
-        if cls.target == 'subject':
-            if not rschema.has_perm(req, 'add', toeid=entity.eid):
-                #print req.user.login, 'has no add perm on subject rel', cls.rtype, 'with', entity
-                return False
-        elif not rschema.has_perm(req, 'add', fromeid=entity.eid):
-            #print req.user.login, 'has no add perm on object rel', cls.rtype, 'with', entity
-            return False
-        #print 'rtype perm ok', cls
-        return True
-            
+                
     def url(self):
         current_entity = self.rset.get_entity(self.row or 0, self.col or 0)
-        linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, self.target)
+        linkto = '%s:%s:%s' % (self.rtype, current_entity.eid, target(self))
         return self.build_url(vid='creation', etype=self.etype,
                               __linkto=linkto,
                               __redirectpath=current_entity.rest_path(), # should not be url quoted!
@@ -217,5 +113,10 @@
     """LinkToEntity action where the action is not usable on the same
     entity's type as the one refered by the .etype attribute
     """
-    __selectors__ = (searchstate_accept_one_but_etype,)
+    def my_selector(cls, req, rset, row=None, col=0, **kwargs):
+        return chainall(match_search_state('normal'),
+                        but_etype, one_line_rset, accept,
+                        relation_possible(cls.rtype, role(cls), cls.etype),
+                        may_add_relation(cls.rtype, role(cls)))
+    __selectors__ = my_selector,
     
--- a/web/views/__init__.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/__init__.py	Mon Feb 16 18:26:13 2009 +0100
@@ -1,7 +1,7 @@
 """Views/forms and actions for the CubicWeb web client
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -67,30 +67,16 @@
             return 'outofcontext-search'
         return 'list'
     return 'table'
-
-def linksearch_match(req, rset):
-    """when searching an entity to create a relation, return True if entities in
-    the given rset may be used as relation end
-    """
-    try:
-        searchedtype = req.search_state[1][-1]
-    except IndexError:
-        return 0 # no searching for association
-    for etype in rset.column_types(0):
-        if etype != searchedtype:
-            return 0
-    return 1
     
 def linksearch_select_url(req, rset):
     """when searching an entity to create a relation, return an url to select
     entities in the given rset
     """
     req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') )
-    target, link_eid, r_type, searchedtype = req.search_state[1]
+    target, eid, r_type, searchedtype = req.search_state[1]
     if target == 'subject':
-        id_fmt = '%s:%s:%%s' % (link_eid, r_type)
+        id_fmt = '%s:%s:%%s' % (eid, r_type)
     else:
-        id_fmt = '%%s:%s:%s' % (r_type, link_eid)
+        id_fmt = '%%s:%s:%s' % (r_type, eid)
     triplets = '-'.join(id_fmt % row[0] for row in rset.rows)
-    return "javascript: selectForAssociation('%s', '%s');" % (triplets,
-                                                              link_eid)
+    return "javascript: selectForAssociation('%s', '%s');" % (triplets, eid)
--- a/web/views/actions.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/actions.py	Mon Feb 16 18:26:13 2009 +0100
@@ -6,70 +6,86 @@
 """
 __docformat__ = "restructuredtext en"
 
-from cubicweb.common.selectors import (searchstate_accept, match_user_group, yes,
-                                       one_line_rset, two_lines_rset, one_etype_rset,
-                                       authenticated_user,
-                                       match_search_state, chainfirst, chainall)
+from cubicweb.common.selectors import (
+    yes, one_line_rset, two_lines_rset, one_etype_rset, relation_possible,
+    non_final_entity,
+    authenticated_user, match_user_groups, match_search_state,
+    has_editable_relation, has_permission, has_add_permission,
+    )
 
-from cubicweb.web.action import Action, EntityAction,  LinkToEntityAction
-from cubicweb.web.views import linksearch_select_url, linksearch_match
+from cubicweb.web.action import Action
+from cubicweb.web.views import linksearch_select_url
 from cubicweb.web.views.baseviews import vid_from_rset
 
 _ = unicode
 
+def match_searched_etype(cls, req, rset, row=None, col=None, **kwargs):
+    return req.match_search_state(rset)
+
+def view_is_not_default_view(cls, req, rset, row, col, **kwargs):
+    # interesting if it propose another view than the current one
+    vid = req.form.get('vid')
+    if vid and vid != vid_from_rset(req, rset, cls.schema):
+        return 1
+    return 0
+
+def addable_etype_empty_rset(cls, req, rset, **kwargs):
+    if rset is not None and not rset.rowcount:
+        rqlst = rset.syntax_tree()
+        if len(rqlst.children) > 1:
+            return 0
+        select = rqlst.children[0]
+        if len(select.defined_vars) == 1 and len(select.solutions) == 1:
+            rset._searched_etype = select.solutions[0].itervalues().next()
+            eschema = cls.schema.eschema(rset._searched_etype)
+            if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
+                   and eschema.has_perm(req, 'add'):
+                return 1
+    return 0
+
 # generic primary actions #####################################################
 
-class SelectAction(EntityAction):
+class SelectAction(Action):
     """base class for link search actions. By default apply on
     any size entity result search it the current state is 'linksearch'
     if accept match.
     """
-    category = 'mainactions'    
-    __selectors__ = (searchstate_accept,)
-    search_states = ('linksearch',)
-    order = 0
+    id = 'select'
+    __selectors__ = (match_search_state('linksearch'),
+                     match_searched_etype)
     
-    id = 'select'
     title = _('select')
-    
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        return linksearch_match(req, rset)
+    category = 'mainactions'    
+    order = 0
     
     def url(self):
         return linksearch_select_url(self.req, self.rset)
 
 
 class CancelSelectAction(Action):
+    id = 'cancel'
+    __selectors__ = (match_search_state('linksearch'),)
+    
+    title = _('cancel select')
     category = 'mainactions'
-    search_states = ('linksearch',)
     order = 10
     
-    id = 'cancel'
-    title = _('cancel select')
-    
     def url(self):
-        target, link_eid, r_type, searched_type = self.req.search_state[1]
-        return self.build_url(rql="Any X WHERE X eid %s" % link_eid,
+        target, eid, r_type, searched_type = self.req.search_state[1]
+        return self.build_url(str(eid),
                               vid='edition', __mode='normal')
 
 
 class ViewAction(Action):
-    category = 'mainactions'    
-    __selectors__ = (match_user_group, searchstate_accept)
-    require_groups = ('users', 'managers')
-    order = 0
-    
     id = 'view'
-    title = _('view')
+    __selectors__ = (match_search_state('normal'),
+                     match_user_groups('users', 'managers'),
+                     view_is_not_default_view,
+                     non_final_entity())
     
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        # interesting if it propose another view than the current one
-        vid = req.form.get('vid')
-        if vid and vid != vid_from_rset(req, rset, cls.schema):
-            return 1
-        return 0
+    title = _('view')
+    category = 'mainactions'    
+    order = 0
     
     def url(self):
         params = self.req.form.copy()
@@ -79,76 +95,60 @@
                               **params)
 
 
-class ModifyAction(EntityAction):
-    category = 'mainactions'
-    __selectors__ = (one_line_rset, searchstate_accept)
-    #__selectors__ = searchstate_accept,
-    schema_action = 'update'
-    order = 10
-    
+class ModifyAction(Action):
     id = 'edit'
-    title = _('modify')
+    __selectors__ = (match_search_state('normal'),
+                     one_line_rset, 
+                     has_permission('update') | has_editable_relation('add'))
     
-    @classmethod
-    def has_permission(cls, entity, action):
-        if entity.has_perm(action):
-            return True
-        # if user has no update right but it can modify some relation,
-        # display action anyway
-        for dummy in entity.srelations_by_category(('generic', 'metadata'),
-                                                   'add'):
-            return True
-        for rschema, targetschemas, role in entity.relations_by_category(
-            ('primary', 'secondary'), 'add'):
-            if not rschema.is_final():
-                return True
-        return False
+    title = _('modify')
+    category = 'mainactions'
+    order = 10
 
     def url(self):
         entity = self.rset.get_entity(self.row or 0, self.col or 0)
         return entity.absolute_url(vid='edition')
         
 
-class MultipleEditAction(EntityAction):
+class MultipleEditAction(Action):
+    id = 'muledit' # XXX get strange conflicts if id='edit'
+    __selectors__ = (match_search_state('normal'),
+                     two_lines_rset, one_etype_rset,
+                     has_permission('update'))
+
+    title = _('modify')
     category = 'mainactions'
-    __selectors__ = (two_lines_rset, one_etype_rset,
-                     searchstate_accept)
-    schema_action = 'update'
     order = 10
     
-    id = 'muledit' # XXX get strange conflicts if id='edit'
-    title = _('modify')
-    
     def url(self):
         return self.build_url('view', rql=self.rset.rql, vid='muledit')
 
 
 # generic secondary actions ###################################################
 
-class ManagePermissions(LinkToEntityAction):
-    accepts = ('Any',)
-    category = 'moreactions'
+class ManagePermissions(Action):
     id = 'addpermission'
+    __selectors__ = (
+        (match_user_groups('managers') 
+         | relation_possible('require_permission', 'subject', 'EPermission')),
+                   )
+
     title = _('manage permissions')
+    category = 'moreactions'
     order = 100
-
-    etype = 'EPermission'
-    rtype = 'require_permission'
-    target = 'object'
     
     def url(self):
         return self.rset.get_entity(0, 0).absolute_url(vid='security')
 
     
-class DeleteAction(EntityAction):
+class DeleteAction(Action):
+    id = 'delete'
+    __selectors__ = (has_permission('delete'),)
+    
+    title = _('delete')
     category = 'moreactions' 
-    __selectors__ = (searchstate_accept,)
-    schema_action = 'delete'
     order = 20
     
-    id = 'delete'
-    title = _('delete')
-    
     def url(self):
         if len(self.rset) == 1:
             entity = self.rset.get_entity(0, 0)
@@ -156,14 +156,14 @@
         return self.build_url(rql=self.rset.printable_rql(), vid='deleteconf')
     
         
-class CopyAction(EntityAction):
+class CopyAction(Action):
+    id = 'copy'
+    __selectors__ = (has_permission('add'),)
+    
+    title = _('copy')
     category = 'moreactions'
-    schema_action = 'add'
     order = 30
     
-    id = 'copy'
-    title = _('copy')
-    
     def url(self):
         entity = self.rset.get_entity(self.row or 0, self.col or 0)
         return entity.absolute_url(vid='copy')
@@ -173,35 +173,16 @@
     """when we're seeing more than one entity with the same type, propose to
     add a new one
     """
+    id = 'addentity'
+    __selectors__ = (match_search_state('normal'),
+                     (addable_etype_empty_rset
+                      # XXX has_add_permission in the middle so '&' is available
+                      | (two_lines_rset & has_add_permission() & one_etype_rset ))
+                     )
+
     category = 'moreactions'
-    id = 'addentity'
     order = 40
     
-    def etype_rset_selector(cls, req, rset, **kwargs):
-        if rset is not None and not rset.rowcount:
-            rqlst = rset.syntax_tree()
-            if len(rqlst.children) > 1:
-                return 0
-            select = rqlst.children[0]
-            if len(select.defined_vars) == 1 and len(select.solutions) == 1:
-                rset._searched_etype = select.solutions[0].itervalues().next()
-                eschema = cls.schema.eschema(rset._searched_etype)
-                if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
-                       and eschema.has_perm(req, 'add'):
-                    return 1
-        return 0
-
-    def has_add_perm_selector(cls, req, rset, **kwargs):
-        eschema = cls.schema.eschema(rset.description[0][0])
-        if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
-               and eschema.has_perm(req, 'add'):
-            return 1
-        return 0
-    __selectors__ = (match_search_state,
-                     chainfirst(etype_rset_selector,
-                                chainall(two_lines_rset, one_etype_rset,
-                                         has_add_perm_selector)))
-
     @property
     def rsettype(self):
         if self.rset:
@@ -219,36 +200,36 @@
 # logged user actions #########################################################
 
 class UserPreferencesAction(Action):
+    id = 'myprefs'
+    __selectors__ = (authenticated_user,)
+    
+    title = _('user preferences')
     category = 'useractions'
-    __selectors__ = authenticated_user,
     order = 10
-    
-    id = 'myprefs'
-    title = _('user preferences')
 
     def url(self):
         return self.build_url(self.id)
 
 
 class UserInfoAction(Action):
+    id = 'myinfos'
+    __selectors__ = (authenticated_user,)
+    
+    title = _('personnal informations')
     category = 'useractions'
-    __selectors__ = authenticated_user,
     order = 20
-    
-    id = 'myinfos'
-    title = _('personnal informations')
 
     def url(self):
         return self.build_url('euser/%s'%self.req.user.login, vid='edition')
 
 
 class LogoutAction(Action):
+    id = 'logout'
+    __selectors__ = (authenticated_user,)
+    
+    title = _('logout')
     category = 'useractions'
-    __selectors__ = authenticated_user,
     order = 30
-    
-    id = 'logout'
-    title = _('logout')
 
     def url(self):
         return self.build_url(self.id)
@@ -257,60 +238,35 @@
 # site actions ################################################################
 
 class ManagersAction(Action):
+    __abstract__ = True
+    __selectors__ = (match_user_groups('managers'),)
+
     category = 'siteactions'
-    __abstract__ = True
-    __selectors__ = match_user_group,
-    require_groups = ('managers',)
 
     def url(self):
         return self.build_url(self.id)
 
     
 class SiteConfigurationAction(ManagersAction):
-    order = 10
     id = 'siteconfig'
     title = _('site configuration')
+    order = 10
 
     
 class ManageAction(ManagersAction):
-    order = 20
     id = 'manage'
     title = _('manage')
+    order = 20
 
 
 class ViewSchemaAction(Action):
+    id = 'schema'
+    __selectors__ = (yes,)
+    
+    title = _("site schema")
     category = 'siteactions'
-    id = 'schema'
-    title = _("site schema")
-    __selectors__ = yes,
     order = 30
     
     def url(self):
         return self.build_url(self.id)
 
-
-# content type specific actions ###############################################
-
-class FollowAction(EntityAction):
-    category = 'mainactions'
-    accepts = ('Bookmark',)
-    
-    id = 'follow'
-    title = _('follow')
-    
-    def url(self):
-        return self.rset.get_entity(self.row or 0, self.col or 0).actual_url()
-
-class UserPreferencesEntityAction(EntityAction):
-    __selectors__ = EntityAction.__selectors__ + (one_line_rset, match_user_group,)
-    require_groups = ('owners', 'managers')
-    category = 'mainactions'
-    accepts = ('EUser',)
-    
-    id = 'prefs'
-    title = _('preferences')
-    
-    def url(self):
-        login = self.rset.get_entity(self.row or 0, self.col or 0).login
-        return self.build_url('euser/%s'%login, vid='epropertiesform')
-
--- a/web/views/baseviews.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/baseviews.py	Mon Feb 16 18:26:13 2009 +0100
@@ -29,7 +29,7 @@
                                    ajax_replace_url, rql_for_eid, simple_sgml_tag)
 from cubicweb.common.view import EntityView, AnyRsetView, EmptyRsetView
 from cubicweb.web.httpcache import MaxAgeHTTPCacheManager
-from cubicweb.web.views import vid_from_rset, linksearch_select_url, linksearch_match
+from cubicweb.web.views import vid_from_rset, linksearch_select_url
 
 _ = unicode
 
@@ -826,7 +826,7 @@
     def cell_call(self, row, col):
         entity = self.entity(row, col)
         erset = entity.as_rset()
-        if linksearch_match(self.req, erset):
+        if self.req.match_search_state(erset):
             self.w(u'<a href="%s" title="%s">%s</a>&nbsp;<a href="%s" title="%s">[...]</a>' % (
                 html_escape(linksearch_select_url(self.req, erset)),
                 self.req._('select this entity'),
--- a/web/views/bookmark.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/bookmark.py	Mon Feb 16 18:26:13 2009 +0100
@@ -9,13 +9,26 @@
 from logilab.mtconverter import html_escape
 
 from cubicweb import Unauthorized
+from cubicweb.common.selectors import implements
 from cubicweb.web.htmlwidgets import BoxWidget, BoxMenu, RawBoxItem
+from cubicweb.web.action import Action
 from cubicweb.web.box import UserRQLBoxTemplate
 from cubicweb.web.views.baseviews import PrimaryView
 
 
+class FollowAction(Action):
+    id = 'follow'
+    __selectors__ = (implements('Bookmark'),)
+
+    title = _('follow')
+    category = 'mainactions'
+    
+    def url(self):
+        return self.rset.get_entity(self.row or 0, self.col or 0).actual_url()
+
+
 class BookmarkPrimaryView(PrimaryView):
-    accepts = ('Bookmark',)
+    __selectors__ = (implements('Bookmark'),)
         
     def cell_call(self, row, col):
         """the primary view for bookmark entity"""
--- a/web/views/embedding.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/embedding.py	Mon Feb 16 18:26:13 2009 +0100
@@ -3,7 +3,7 @@
 
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -17,8 +17,8 @@
 from cubicweb import urlquote # XXX should use view.url_quote method
 from cubicweb.interfaces import IEmbedable
 from cubicweb.common.uilib import soup2xhtml
-from cubicweb.common.selectors import (one_line_rset, score_entity_selector,
-                                    match_search_state, implement_interface)
+from cubicweb.common.selectors import (one_line_rset, score_entity,
+                                       match_search_state, implements)
 from cubicweb.common.view import NOINDEX, NOFOLLOW
 from cubicweb.web.controller import Controller
 from cubicweb.web.action import Action
@@ -75,30 +75,28 @@
         return self.vreg.main_template(req, self.template, body=body)
 
 
+def entity_has_embedable_url(entity):
+    """return 1 if the entity provides an allowed embedable url"""
+    url = entity.embeded_url()
+    if not url or not url.strip():
+        return 0
+    allowed = entity.config['embed-allowed']
+    if allowed is None or not allowed.match(url):
+        return 0
+    return 1
+
+
 class EmbedAction(Action):
     """display an 'embed' link on entity implementing `embeded_url` method
     if the returned url match embeding configuration
     """
     id = 'embed'
-    controller = 'embed'
-    __selectors__ = (one_line_rset, match_search_state,
-                     implement_interface, score_entity_selector)
-    accepts_interfaces = (IEmbedable,)
+    __selectors__ = (one_line_rset, match_search_state('normal'),
+                     implements(IEmbedable),
+                     score_entity(entity_has_embedable_url))
     
     title = _('embed')
-        
-    @classmethod
-    def score_entity(cls, entity):
-        """return a score telling how well I can display the given 
-        entity instance (required by the value_selector)
-        """
-        url = entity.embeded_url()
-        if not url or not url.strip():
-            return 0
-        allowed = cls.config['embed-allowed']
-        if allowed is None or not allowed.match(url):
-            return 0
-        return 1
+    controller = 'embed'
     
     def url(self, row=0):
         entity = self.rset.get_entity(row, 0)
@@ -132,6 +130,7 @@
                 url = '%s?custom_css=%s' % (url, self.custom_css)
         return '<a href="%s"' % url
 
+
 class absolutize_links:
     def __init__(self, embedded_url, tag, custom_css=None):
         self.embedded_url = embedded_url
@@ -152,7 +151,8 @@
     for rgx, repl in filters:
         body = rgx.sub(repl, body)
     return body
-    
+
+
 def embed_external_page(url, prefix, headers=None, custom_css=None):
     req = Request(url, headers=(headers or {}))
     content = urlopen(req).read()
--- a/web/views/euser.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/euser.py	Mon Feb 16 18:26:13 2009 +0100
@@ -10,12 +10,30 @@
 from logilab.mtconverter import html_escape
 
 from cubicweb.schema import display_name
+from cubicweb.common.selectors import one_line_rset, implements, match_user_groups
 from cubicweb.web import INTERNAL_FIELD_VALUE
 from cubicweb.web.form import EntityForm
+from cubicweb.web.action import Action
 from cubicweb.web.views.baseviews import PrimaryView, EntityView
 
+
+class UserPreferencesEntityAction(Action):
+    id = 'prefs'
+    __selectors__ = (one_line_rset,
+                     implements('EUser'),
+                     match_user_groups('owners', 'managers'))
+    
+    title = _('preferences')
+    category = 'mainactions'
+    
+    def url(self):
+        login = self.rset.get_entity(self.row or 0, self.col or 0).login
+        return self.build_url('euser/%s'%login, vid='epropertiesform')
+
+
 class EUserPrimaryView(PrimaryView):
-    accepts = ('EUser',)
+    __selectors__ = (implements('EUser'),)
+    
     skip_attrs = ('firstname', 'surname')
     
     def iter_relations(self, entity):
@@ -34,7 +52,8 @@
                                  ]
 class FoafView(EntityView):
     id = 'foaf'
-    accepts = ('EUser',)
+    __selectors__ = (implements('EUser'),)
+    
     title = _('foaf')
     templatable = False
     content_type = 'text/xml'
@@ -54,7 +73,6 @@
                       <foaf:maker rdf:resource="%s"/>
                       <foaf:primaryTopic rdf:resource="%s"/>
                    </foaf:PersonalProfileDocument>''' % (entity.absolute_url(), entity.absolute_url()))
-                      
         self.w(u'<foaf:Person rdf:ID="%s">\n' % entity.eid)
         self.w(u'<foaf:name>%s</foaf:name>\n' % html_escape(entity.dc_long_title()))
         if entity.surname:
@@ -68,11 +86,13 @@
             self.w(u'<foaf:mbox>%s</foaf:mbox>\n' % html_escape(emailaddr))
         self.w(u'</foaf:Person>\n')
 
+
 class FoafUsableView(FoafView):
     id = 'foaf_usable'
   
     def call(self):
         self.cell_call(0, 0)
+
             
 class EditGroups(EntityForm):
     """displays a simple euser / egroups editable table"""
--- a/web/views/idownloadable.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/idownloadable.py	Mon Feb 16 18:26:13 2009 +0100
@@ -1,7 +1,7 @@
 """Specific views for entities implementing IDownloadable
 
 :organization: Logilab
-:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
 __docformat__ = "restructuredtext en"
@@ -10,8 +10,8 @@
 
 from cubicweb.interfaces import IDownloadable
 from cubicweb.common.mttransforms import ENGINE
-from cubicweb.common.selectors import (one_line_rset, score_entity_selector,
-                                       implement_interface, match_context_prop)
+from cubicweb.common.selectors import (one_line_rset, score_entity,
+                                       implements, match_context_prop)
 from cubicweb.web.box import EntityBoxTemplate
 from cubicweb.web.views import baseviews
 
@@ -35,8 +35,7 @@
     
 class DownloadBox(EntityBoxTemplate):
     id = 'download_box'
-    __selectors__ = (one_line_rset, implement_interface, match_context_prop)
-    accepts_interfaces = (IDownloadable,)
+    __selectors__ = (one_line_rset, implements(IDownloadable), match_context_prop)
     order = 10
     def cell_call(self, row, col, title=None, label=None, **kwargs):
         entity = self.entity(row, col)
@@ -44,12 +43,11 @@
 
 
 class DownloadView(baseviews.EntityView):
-    """this view is replacing the deprecated 'download' controller and allow downloading
-    of entities providing the necessary interface
+    """this view is replacing the deprecated 'download' controller and allow
+    downloading of entities providing the necessary interface
     """
     id = 'download'
-    __selectors__ = (one_line_rset, implement_interface)
-    accepts_interfaces = (IDownloadable,)
+    __selectors__ = (one_line_rset, implements(IDownloadable))
 
     templatable = False
     content_type = 'application/octet-stream'
@@ -76,10 +74,9 @@
 class DownloadLinkView(baseviews.EntityView):
     """view displaying a link to download the file"""
     id = 'downloadlink'
+    __selectors__ = (implements(IDownloadable),)
     title = None # should not be listed in possible views
-    __selectors__ = (implement_interface,)
 
-    accepts_interfaces = (IDownloadable,)
     
     def cell_call(self, row, col, title=None, **kwargs):
         entity = self.entity(row, col)
@@ -89,9 +86,8 @@
 
                                                                                 
 class IDownloadablePrimaryView(baseviews.PrimaryView):
-    __selectors__ = (implement_interface,)
+    __selectors__ = (implements(IDownloadable),)
     #skip_attrs = ('eid', 'data',) # XXX
-    accepts_interfaces = (IDownloadable,)
 
     def render_entity_title(self, entity):
         self.w(u'<h1>%s %s</h1>'
@@ -122,10 +118,7 @@
 
 
 class IDownloadableLineView(baseviews.OneLineView):
-    __selectors__ = (implement_interface,)
-    # don't kick default oneline view
-    accepts_interfaces = (IDownloadable,)
-    
+    __selectors__ = (implements(IDownloadable),)
 
     def cell_call(self, row, col, title=None, **kwargs):
         """the secondary view is a link to download the file"""
@@ -137,11 +130,18 @@
                (url, name, durl, self.req._('download')))
 
 
+def is_image(entity):
+    mt = entity.download_content_type()
+    if not (mt and mt.startswith('image/')):
+        return 0
+    return 1
+    
 class ImageView(baseviews.EntityView):
-    __selectors__ = (implement_interface, score_entity_selector)
     id = 'image'
+    __selectors__ = (implements(IDownloadable),
+                     score_entity(is_image))
+    
     title = _('image')
-    accepts_interfaces = (IDownloadable,)
     
     def call(self):
         rset = self.rset
@@ -149,13 +149,6 @@
             self.w(u'<div class="efile">')
             self.wview(self.id, rset, row=i, col=0)
             self.w(u'</div>')
-
-    @classmethod
-    def score_entity(cls, entity):
-        mt = entity.download_content_type()
-        if not (mt and mt.startswith('image/')):
-            return 0
-        return 1
     
     def cell_call(self, row, col):
         entity = self.entity(row, col)
--- a/web/views/management.py	Mon Feb 16 16:24:24 2009 +0100
+++ b/web/views/management.py	Mon Feb 16 18:26:13 2009 +0100
@@ -14,7 +14,7 @@
 from cubicweb.common.utils import UStringIO
 from cubicweb.common.view import AnyRsetView, StartupView, EntityView
 from cubicweb.common.uilib import html_traceback, rest_traceback
-from cubicweb.common.selectors import (yes, one_line_rset,
+from cubicweb.common.selectors import (yes, one_line_rset, match_user_groups,
                                        accept_rset, none_rset,
                                        chainfirst, chainall)
 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
@@ -293,7 +293,7 @@
 def css_class(someclass):
     return someclass and 'class="%s"' % someclass or ''
 
-class SystemEpropertiesForm(FormMixIn, StartupView):
+class SystemEPropertiesForm(FormMixIn, StartupView):
     controller = 'edit'
     id = 'systemepropertiesform'
     title = _('site configuration')
@@ -461,24 +461,23 @@
         w(u'<input type="hidden" name="%s" value="%s"/>' % (eid_param('edits-pkey', eid), ''))
 
 
-class EpropertiesForm(SystemEpropertiesForm):
+
+def is_user_prefs(cls, req, rset, row, col):
+    return req.user.eid == rset[row or 0 ][col or 0]
+
+class EPropertiesForm(SystemEPropertiesForm):
     id = 'epropertiesform'
-    title = _('preferences')
-    require_groups = ('users', 'managers') # we don't want guests to be able to come here
-    __selectors__ = chainfirst(none_rset,
-                               chainall(one_line_rset, accept_rset)),
+    __selectors__ = (
+        # we don't want guests to be able to come here
+        match_user_groups('users', 'managers'), 
+        chainfirst(none_rset),
+                   chainall(one_line_rset, is_user_prefs),
+                   chainall(one_line_rset, match_user_groups('managers'))
+        )
+        
     accepts = ('EUser',)
 
-    @classmethod
-    def accept_rset(cls, req, rset, row, col):
-        if row is None:
-            row = 0
-        score = super(EpropertiesForm, cls).accept_rset(req, rset, row, col)
-        # check current user is the rset user or he is in the managers group
-        if score and (req.user.eid == rset[row][col or 0]
-                      or req.user.matching_groups('managers')):
-            return score
-        return 0
+    title = _('preferences')
 
     @property
     def user(self):
@@ -493,7 +492,7 @@
                                 'P for_user U, U eid %(x)s', {'x': self.user.eid})
 
     def form_row_hiddens(self, w, entity, key):
-        super(EpropertiesForm, self).form_row_hiddens(w, entity, key)
+        super(EPropertiesForm, self).form_row_hiddens(w, entity, key)
         # if user is in the managers group and the property is being created,
         # we have to set for_user explicitly
         if not entity.has_eid() and self.user.matching_groups('managers'):