diff -r 02b52bf9f5f8 -r 0865e1e90674 selectors.py --- 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 . +""".. _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 - >>> 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 ############################################################