selectors.py
brancholdstable
changeset 5422 0865e1e90674
parent 5421 8167de96c523
child 5423 e15abfdcce38
child 5424 8ecbcbff9777
--- a/selectors.py	Wed Mar 24 10:23:31 2010 +0100
+++ b/selectors.py	Wed Apr 28 11:54:13 2010 +0200
@@ -1,44 +1,195 @@
-"""This file contains some basic selectors required by application objects.
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# logilab-common is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+""".. _Selectors:
+
+Selectors
+---------
+
+Using and combining existant selectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+You can combine selectors using the `&`, `|` and `~` operators.
+
+When two selectors are combined using the `&` operator, it means that
+both should return a positive score. On success, the sum of scores is
+returned.
+
+When two selectors are combined using the `|` operator, it means that
+one of them should return a positive score. On success, the first
+positive score is returned.
+
+You can also "negate" a selector by precedeing it by the `~` unary operator.
+
+Of course you can use parenthesis to balance expressions.
+
+Example
+~~~~~~~
+
+The goal: when on a blog, one wants the RSS link to refer to blog entries, not to
+the blog entity itself.
 
-A selector is responsible to score how well an object may be used with a
-given context by returning a score.
+To do that, one defines a method on entity classes that returns the
+RSS stream url for a given entity. The default implementation on
+:class:`~cubicweb.entities.AnyEntity` (the generic entity class used
+as base for all others) and a specific implementation on `Blog` will
+do what we want.
+
+But when we have a result set containing several `Blog` entities (or
+different entities), we don't know on which entity to call the
+aforementioned method. In this case, we keep the generic behaviour.
+
+Hence we have two cases here, one for a single-entity rsets, the other for
+multi-entities rsets.
+
+In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
+
+.. sourcecode:: python
+
+  class RSSIconBox(ExtResourcesBoxTemplate):
+    ''' just display the RSS icon on uniform result set '''
+    __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
+
+It takes into account:
+
+* the inherited selection criteria (one has to look them up in the class
+  hierarchy to know the details)
 
-In CubicWeb Usually the context consists for a request object, a result set
-or None, a specific row/col in the result set, etc...
+* :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
+  containing non final entities (a 'final entity' being synonym for entity
+  attributes type, eg `String`, `Int`, etc)
+
+This matches our second case. Hence we have to provide a specific component for
+the first case:
+
+.. sourcecode:: python
+
+  class EntityRSSIconBox(RSSIconBox):
+    '''just display the RSS icon on uniform result set for a single entity'''
+    __select__ = RSSIconBox.__select__ & one_line_rset()
+
+Here, one adds the :class:`~cubicweb.selectors.one_line_rset` selector, which
+filters result sets of size 1. Thus, on a result set containing multiple
+entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
+selectable. However for a result set with one entity, the `EntityRSSIconBox`
+class will have a higher score than `RSSIconBox`, which is what we wanted.
+
+Of course, once this is done, you have to:
+
+* fill in the call method of `EntityRSSIconBox`
+
+* provide the default implementation of the method returning the RSS stream url
+  on :class:`~cubicweb.entities.AnyEntity`
+
+* redefine this method on `Blog`.
 
 
-If you have trouble with selectors, especially if the objet (typically
-a view or a component) you want to use is not selected and you want to
-know which one(s) of its selectors fail (e.g. returns 0), you can use
-`traced_selection` or even direclty `TRACED_OIDS`.
+When to use selectors?
+~~~~~~~~~~~~~~~~~~~~~~
+
+Selectors are to be used whenever arises the need of dispatching on the shape or
+content of a result set or whatever else context (value in request form params,
+authenticated user groups, etc...). That is, almost all the time.
+
+Here is a quick example:
+
+.. sourcecode:: python
 
-`TRACED_OIDS` is a tuple of traced object ids. The special value
-'all' may be used to log selectors for all objects.
+    class UserLink(component.Component):
+	'''if the user is the anonymous user, build a link to login else a link
+	to the connected user object with a loggout link
+	'''
+	__regid__ = 'loggeduserlink'
 
-For instance, say that the following code yields a `NoSelectableObject`
-exception::
-
-    self.view('calendar', myrset)
+	def call(self):
+	    if self._cw.cnx.anonymous_connection:
+		# display login link
+		...
+	    else:
+		# display a link to the connected user object with a loggout link
+		...
 
-You can log the selectors involved for *calendar* by replacing the line
-above by::
+The proper way to implement this with |cubicweb| is two have two different
+classes sharing the same identifier but with different selectors so you'll get
+the correct one according to the context.
+
+.. sourcecode:: python
+
+    class UserLink(component.Component):
+	'''display a link to the connected user object with a loggout link'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & authenticated_user()
 
-    # in Python2.5
-    from cubicweb.selectors import traced_selection
-    with traced_selection():
-        self.view('calendar', myrset)
+	def call(self):
+            # display useractions and siteactions
+	    ...
 
-    # in Python2.4
-    from cubicweb import selectors
-    selectors.TRACED_OIDS = ('calendar',)
-    self.view('calendar', myrset)
-    selectors.TRACED_OIDS = ()
+    class AnonUserLink(component.Component):
+	'''build a link to login'''
+	__regid__ = 'loggeduserlink'
+	__select__ = component.Component.__select__ & anonymous_user()
+
+	def call(self):
+	    # display login link
+            ...
+
+The big advantage, aside readability once you're familiar with the
+system, is that your cube becomes much more easily customizable by
+improving componentization.
 
 
-:organization: Logilab
-:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+.. _CustomSelectors:
+
+Defining your own selectors
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autodocstring:: cubicweb.appobject::objectify_selector
+
+In other cases, you can take a look at the following abstract base classes:
+
+.. autoclass:: cubicweb.selectors.ExpectedValueSelector
+.. autoclass:: cubicweb.selectors.EClassSelector
+.. autoclass:: cubicweb.selectors.EntitySelector
+
+Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
+or below the :func:`objectify_selector` decorator of your selector function so it gets
+traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
+
+.. autofunction:: cubicweb.selectors.lltrace
+
+.. note::
+  Selectors __call__ should *always* return a positive integer, and shall never
+  return `None`.
+
+
+.. _DebuggingSelectors:
+
+Debugging selection
+~~~~~~~~~~~~~~~~~~~
+
+Once in a while, one needs to understand why a view (or any application object)
+is, or is not selected appropriately. Looking at which selectors fired (or did
+not) is the way. The :class:`cubicweb.selectors.traced_selection` context
+manager to help with that, *if you're running your instance in debug mode*.
+
+.. autoclass:: cubicweb.selectors.traced_selection
+
+
+.. |cubicweb| replace:: *CubicWeb*
 """
 __docformat__ = "restructuredtext en"
 
@@ -60,47 +211,68 @@
 
 # helpers for debugging selectors
 SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
-TRACED_OIDS = ()
+TRACED_OIDS = None
+
+def _trace_selector(cls, selector, args, ret):
+    # /!\ lltrace decorates pure function or __call__ method, this
+    #     means argument order may be different
+    if isinstance(cls, Selector):
+        selname = str(cls)
+        vobj = args[0]
+    else:
+        selname = selector.__name__
+        vobj = cls
+    if TRACED_OIDS == 'all' or class_regid(vobj) in TRACED_OIDS:
+        #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
+        print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
 
 def lltrace(selector):
+    """use this decorator on your selectors so the becomes traceable with
+    :class:`traced_selection`
+    """
     # don't wrap selectors if not in development mode
     if CubicWebConfiguration.mode == 'system': # XXX config.debug
         return selector
     def traced(cls, *args, **kwargs):
-        # /!\ lltrace decorates pure function or __call__ method, this
-        #     means argument order may be different
-        if isinstance(cls, Selector):
-            selname = str(cls)
-            vobj = args[0]
-        else:
-            selname = selector.__name__
-            vobj = cls
-        oid = class_regid(vobj)
         ret = selector(cls, *args, **kwargs)
-        if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
-            #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
-            print '%s -> %s for %s(%s)' % (selname, ret, vobj, vobj.__regid__)
+        if TRACED_OIDS is not None:
+            _trace_selector(cls, selector, args, ret)
         return ret
     traced.__name__ = selector.__name__
     traced.__doc__ = selector.__doc__
     return traced
 
 class traced_selection(object):
-    """selector debugging helper.
-
+    """
     Typical usage is :
 
-    >>> with traced_selection():
-    ...     # some code in which you want to debug selectors
-    ...     # for all objects
+    .. sourcecode:: python
+
+        >>> from cubicweb.selectors import traced_selection
+        >>> with traced_selection():
+        ...     # some code in which you want to debug selectors
+        ...     # for all objects
 
-    or
+    Don't forget the 'from __future__ import with_statement' at the module top-level
+    if you're using python prior to 2.6.
+
+    This will yield lines like this in the logs::
+
+        selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
 
-    >>> with traced_selection( ('oid1', 'oid2') ):
-    ...     # some code in which you want to debug selectors
-    ...     # for objects with id 'oid1' and 'oid2'
+    You can also give to :class:`traced_selection` the identifiers of objects on
+    which you want to debug selection ('oid1' and 'oid2' in the example above).
+
+    .. sourcecode:: python
 
+        >>> with traced_selection( ('regid1', 'regid2') ):
+        ...     # some code in which you want to debug selectors
+        ...     # for objects with __regid__ 'regid1' and 'regid2'
+
+    A potentially usefull point to set up such a tracing function is
+    the `cubicweb.vregistry.Registry.select` method body.
     """
+
     def __init__(self, traced='all'):
         self.traced = traced
 
@@ -110,7 +282,7 @@
 
     def __exit__(self, exctype, exc, traceback):
         global TRACED_OIDS
-        TRACED_OIDS = ()
+        TRACED_OIDS = None
         return traceback is None
 
 
@@ -260,12 +432,12 @@
       - `accept_none` is False and some cell in the column has a None value
         (this may occurs with outer join)
 
-    .. 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.
+    .. Note::
+       using :class:`EntitySelector` or :class:`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
@@ -306,8 +478,12 @@
 
 
 class ExpectedValueSelector(Selector):
-    """Take a list of expected values as initializer argument, check
-    _get_value method return one of these expected values.
+    """Take a list of expected values as initializer argument and store them
+    into the :attr:`expected` set attribute.
+
+    You should implements the :meth:`_get_value(cls, req, **kwargs)` method
+    which should return the value for the given context. The selector will then
+    return 1 if the value is expected, else 0.
     """
     def __init__(self, *expected):
         assert expected, self
@@ -345,10 +521,12 @@
 
 
 class appobject_selectable(Selector):
-    """return 1 if another appobject is selectable using the same input context.
+    """Return 1 if another appobject is selectable using the same input context.
 
     Initializer arguments:
+
     * `registry`, a registry name
+
     * `regid`, an object identifier in this registry
     """
     def __init__(self, registry, regid):
@@ -1080,6 +1258,24 @@
             return 1
         return 0
 
+class is_in_state(score_entity):
+    """return 1 if entity is in one of the states given as argument list
+
+    you should use this instead of your own score_entity x: x.state == 'bla'
+    selector to avoid some gotchas:
+
+    * possible views gives a fake entity with no state
+    * you must use the latest tr info, not entity.state for repository side
+      checking of the current state
+    """
+    def __init__(self, *states):
+        def score(entity, states=set(states)):
+            try:
+                return entity.latest_trinfo().new_state.name in states
+            except AttributeError:
+                return None
+        super(is_in_state, self).__init__(score)
+
 
 ## deprecated stuff ############################################################