selectors.py
branchstable
changeset 5147 70181998897f
parent 5143 43afbdd5c8b4
child 5174 78438ad513ca
child 5281 d01a02d07a57
equal deleted inserted replaced
5146:fe56baf63ecb 5147:70181998897f
     1 """This file contains some basic selectors required by application objects.
     1 # :organization: Logilab
     2 
     2 # :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
     3 A selector is responsible to score how well an object may be used with a
     3 # :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     4 given context by returning a score.
     4 # :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
     5 
     5 """.. _Selectors:
     6 In CubicWeb Usually the context consists for a request object, a result set
     6 
     7 or None, a specific row/col in the result set, etc...
     7 Selectors
     8 
     8 ---------
     9 
     9 
    10 If you have trouble with selectors, especially if the objet (typically
    10 Using and combining existant selectors
    11 a view or a component) you want to use is not selected and you want to
    11 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    12 know which one(s) of its selectors fail (e.g. returns 0), you can use
    12 
    13 `traced_selection` or even direclty `TRACED_OIDS`.
    13 You can combine selectors using the `&`, `|` and `~` operators.
    14 
    14 
    15 `TRACED_OIDS` is a tuple of traced object ids. The special value
    15 When two selectors are combined using the `&` operator (formerly `chainall`), it
    16 'all' may be used to log selectors for all objects.
    16 means that both should return a positive score. On success, the sum of scores is returned.
    17 
    17 
    18 For instance, say that the following code yields a `NoSelectableObject`
    18 When two selectors are combined using the `|` operator (former `chainfirst`), it
    19 exception::
    19 means that one of them should return a positive score. On success, the first
    20 
    20 positive score is returned.
    21     self.view('calendar', myrset)
    21 
    22 
    22 You can also "negate" a selector by precedeing it by the `~` unary operator.
    23 You can log the selectors involved for *calendar* by replacing the line
    23 
    24 above by::
    24 Of course you can use parens to balance expressions.
    25 
    25 
    26     from cubicweb.selectors import traced_selection
    26 .. Note:
    27     with traced_selection():
    27   When one chains selectors, the final score is the sum of the score of each
    28         self.view('calendar', myrset)
    28   individual selector (unless one of them returns 0, in which case the object is
    29 
    29   non selectable)
    30 With python 2.5, think to add:
    30 
    31 
    31 
    32     from __future__ import with_statement
    32 Example
    33 
    33 ~~~~~~~
    34 at the top of your module.
    34 
    35 
    35 The goal: when on a Blog, one wants the RSS link to refer to blog entries, not to
    36 :organization: Logilab
    36 the blog entity itself.
    37 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
    37 
    38 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
    38 To do that, one defines a method on entity classes that returns the RSS stream
    39 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
    39 url for a given entity. The default implementation on
       
    40 :class:`~cubicweb.entities.AnyEntity` (the generic entity class used as base for
       
    41 all others) and a specific implementation on Blog will do what we want.
       
    42 
       
    43 But when we have a result set containing several Blog entities (or different
       
    44 entities), we don't know on which entity to call the aforementioned method. In
       
    45 this case, we keep the generic behaviour.
       
    46 
       
    47 Hence we have two cases here, one for a single-entity rsets, the other for
       
    48 multi-entities rsets.
       
    49 
       
    50 In web/views/boxes.py lies the RSSIconBox class. Look at its selector:
       
    51 
       
    52 .. sourcecode:: python
       
    53 
       
    54   class RSSIconBox(ExtResourcesBoxTemplate):
       
    55     '''just display the RSS icon on uniform result set'''
       
    56     __select__ = ExtResourcesBoxTemplate.__select__ & non_final_entity()
       
    57 
       
    58 It takes into account:
       
    59 
       
    60 * the inherited selection criteria (one has to look them up in the class
       
    61   hierarchy to know the details)
       
    62 
       
    63 * :class:`~cubicweb.selectors.non_final_entity`, which filters on result sets
       
    64   containing non final entities (a 'final entity' being synonym for entity
       
    65   attributes type, eg `String`, `Int`, etc)
       
    66 
       
    67 This matches our second case. Hence we have to provide a specific component for
       
    68 the first case:
       
    69 
       
    70 .. sourcecode:: python
       
    71 
       
    72   class EntityRSSIconBox(RSSIconBox):
       
    73     '''just display the RSS icon on uniform result set for a single entity'''
       
    74     __select__ = RSSIconBox.__select__ & one_line_rset()
       
    75 
       
    76 Here, one adds the :class:`~cubicweb.selectors.one_line_rset` selector, which
       
    77 filters result sets of size 1. Thus, on a result set containing multiple
       
    78 entities, :class:`one_line_rset` makes the EntityRSSIconBox class non
       
    79 selectable. However for a result set with one entity, the `EntityRSSIconBox`
       
    80 class will have a higher score than `RSSIconBox`, which is what we wanted.
       
    81 
       
    82 Of course, once this is done, you have to:
       
    83 
       
    84 * fill in the call method of `EntityRSSIconBox`
       
    85 
       
    86 * provide the default implementation of the method returning the RSS stream url
       
    87   on :class:`~cubicweb.entities.AnyEntity`
       
    88 
       
    89 * redefine this method on `Blog`.
       
    90 
       
    91 
       
    92 When to use selectors?
       
    93 ~~~~~~~~~~~~~~~~~~~~~~
       
    94 
       
    95 Selectors are to be used whenever arises the need of dispatching on the shape or
       
    96 content of a result set or whatever else context (value in request form params,
       
    97 authenticated user groups, etc...). That is, almost all the time.
       
    98 
       
    99 Here is a quick example:
       
   100 
       
   101 .. sourcecode:: python
       
   102 
       
   103     class UserLink(component.Component):
       
   104 	'''if the user is the anonymous user, build a link to login else a link
       
   105 	to the connected user object with a loggout link
       
   106 	'''
       
   107 	__regid__ = 'loggeduserlink'
       
   108 
       
   109 	def call(self):
       
   110 	    if self._cw.cnx.anonymous_connection:
       
   111 		# display login link
       
   112 		...
       
   113 	    else:
       
   114 		# display a link to the connected user object with a loggout link
       
   115 		...
       
   116 
       
   117 The proper way to implement this with |cubicweb| is two have two different
       
   118 classes sharing the same identifier but with different selectors so you'll get
       
   119 the correct one according to the context:
       
   120 
       
   121 
       
   122     class UserLink(component.Component):
       
   123 	'''display a link to the connected user object with a loggout link'''
       
   124 	__regid__ = 'loggeduserlink'
       
   125 	__select__ = component.Component.__select__ & authenticated_user()
       
   126 
       
   127 	def call(self):
       
   128             # display useractions and siteactions
       
   129 	    ...
       
   130 
       
   131     class AnonUserLink(component.Component):
       
   132 	'''build a link to login'''
       
   133 	__regid__ = 'loggeduserlink'
       
   134 	__select__ = component.Component.__select__ & anonymous_user()
       
   135 
       
   136 	def call(self):
       
   137 	    # display login link
       
   138             ...
       
   139 
       
   140 The big advantage, aside readibily once you're familiar with the system, is that
       
   141 your cube becomes much more easily customizable by improving componentization.
       
   142 
       
   143 
       
   144 .. _CustomSelectors:
       
   145 
       
   146 Defining your own selectors
       
   147 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   148 
       
   149 .. autodocstring:: cubicweb.appobject::objectify_selector
       
   150 
       
   151 In other case, you can take a look at the following abstract base classes:
       
   152 
       
   153 .. autoclass:: cubicweb.selectors.ExpectedValueSelector
       
   154 .. autoclass:: cubicweb.selectors.EClassSelector
       
   155 .. autoclass:: cubicweb.selectors.EntitySelector
       
   156 
       
   157 Also, think to use the :func:`lltrace` decorator on your selector class' :meth:`__call__` method
       
   158 or below the :func:`objectify_selector` decorator of your selector function so it gets
       
   159 traceable when :class:`traced_selection` is activated (see :ref:`DebuggingSelectors`).
       
   160 
       
   161 .. autofunction:: cubicweb.selectors.lltrace
       
   162 
       
   163 .. Note::
       
   164   Selectors __call__ should *always* return a positive integer, and shall never
       
   165   return `None`.
       
   166 
       
   167 
       
   168 .. _DebuggingSelectors:
       
   169 
       
   170 Debugging selection
       
   171 ~~~~~~~~~~~~~~~~~~~
       
   172 
       
   173 Once in a while, one needs to understand why a view (or any application object)
       
   174 is, or is not selected appropriately. Looking at which selectors fired (or did
       
   175 not) is the way. The :class:`cubicweb.selectors.traced_selection` context
       
   176 manager to help with that, *if you're running your instance in debug mode*.
       
   177 
       
   178 .. autoclass:: cubicweb.selectors.traced_selection
       
   179 
       
   180 
       
   181 .. |cubicweb| replace:: *CubicWeb*
    40 """
   182 """
    41 __docformat__ = "restructuredtext en"
   183 __docformat__ = "restructuredtext en"
    42 
   184 
    43 import logging
   185 import logging
    44 from warnings import warn
   186 from warnings import warn
    88     traced.__name__ = selector.__name__
   230     traced.__name__ = selector.__name__
    89     traced.__doc__ = selector.__doc__
   231     traced.__doc__ = selector.__doc__
    90     return traced
   232     return traced
    91 
   233 
    92 class traced_selection(object):
   234 class traced_selection(object):
    93     """selector debugging helper.
   235     """
    94 
       
    95     Typical usage is :
   236     Typical usage is :
    96 
   237 
    97     >>> with traced_selection():
   238     .. sourcecode:: python
    98     ...     # some code in which you want to debug selectors
   239 
    99     ...     # for all objects
   240         >>> from cubicweb.selectors import traced_selection
   100 
   241         >>> with traced_selection():
   101     or
   242         ...     # some code in which you want to debug selectors
   102 
   243         ...     # for all objects
   103     >>> with traced_selection( ('oid1', 'oid2') ):
   244 
   104     ...     # some code in which you want to debug selectors
   245     Don't forget the 'from __future__ import with_statement' at the module top-level
   105     ...     # for objects with id 'oid1' and 'oid2'
   246     if you're using python prior to 2.6.
       
   247 
       
   248     This will yield lines like this in the logs::
       
   249 
       
   250         selector one_line_rset returned 0 for <class 'cubicweb.web.views.basecomponents.WFHistoryVComponent'>
       
   251 
       
   252     You can also give to :class:`traced_selection` the identifiers of objects on
       
   253     which you want to debug selection ('oid1' and 'oid2' in the example above).
       
   254 
       
   255     .. sourcecode:: python
       
   256 
       
   257         >>> with traced_selection( ('oid1', 'oid2') ):
       
   258         ...     # some code in which you want to debug selectors
       
   259         ...     # for objects with id 'oid1' and 'oid2'
   106 
   260 
   107     """
   261     """
   108     def __init__(self, traced='all'):
   262     def __init__(self, traced='all'):
   109         self.traced = traced
   263         self.traced = traced
   110 
   264 
   262         returned
   416         returned
   263 
   417 
   264       - `accept_none` is False and some cell in the column has a None value
   418       - `accept_none` is False and some cell in the column has a None value
   265         (this may occurs with outer join)
   419         (this may occurs with outer join)
   266 
   420 
   267     .. note::
   421     .. Note::
   268        using EntitySelector or EClassSelector as base selector class impacts
   422        using :class:`EntitySelector` or :class:`EClassSelector` as base selector
   269        performance, since when no entity or row is specified the later works on
   423        class impacts performance, since when no entity or row is specified the
   270        every different *entity class* found in the result set, while the former
   424        later works on every different *entity class* found in the result set,
   271        works on each *entity* (eg each row of the result set), which may be much
   425        while the former works on each *entity* (eg each row of the result set),
   272        more costly.
   426        which may be much more costly.
   273     """
   427     """
   274 
   428 
   275     @lltrace
   429     @lltrace
   276     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
   430     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
   277         if not rset and not kwargs.get('entity'):
   431         if not rset and not kwargs.get('entity'):
   308     def score_entity(self, entity):
   462     def score_entity(self, entity):
   309         raise NotImplementedError()
   463         raise NotImplementedError()
   310 
   464 
   311 
   465 
   312 class ExpectedValueSelector(Selector):
   466 class ExpectedValueSelector(Selector):
   313     """Take a list of expected values as initializer argument, check
   467     """Take a list of expected values as initializer argument and store them
   314     _get_value method return one of these expected values.
   468     into the :attr:`expected` set attribute.
       
   469 
       
   470     You should implements the :meth:`_get_value(cls, req, **kwargs)` method
       
   471     which should return the value for the given context. The selector will then
       
   472     return 1 if the value is expected, else 0.
   315     """
   473     """
   316     def __init__(self, *expected):
   474     def __init__(self, *expected):
   317         assert expected, self
   475         assert expected, self
   318         self.expected = frozenset(expected)
   476         self.expected = frozenset(expected)
   319 
   477 
   347                 return 0
   505                 return 0
   348         return len(self.expected)
   506         return len(self.expected)
   349 
   507 
   350 
   508 
   351 class appobject_selectable(Selector):
   509 class appobject_selectable(Selector):
   352     """return 1 if another appobject is selectable using the same input context.
   510     """Return 1 if another appobject is selectable using the same input context.
   353 
   511 
   354     Initializer arguments:
   512     Initializer arguments:
       
   513 
   355     * `registry`, a registry name
   514     * `registry`, a registry name
       
   515 
   356     * `regid`, an object identifier in this registry
   516     * `regid`, an object identifier in this registry
   357     """
   517     """
   358     def __init__(self, registry, regid):
   518     def __init__(self, registry, regid):
   359         self.registry = registry
   519         self.registry = registry
   360         self.regid = regid
   520         self.regid = regid