selectors.py
changeset 1808 aa09e20dd8c0
parent 1784 f0fb914e57db
child 1887 7e19c94ce0d7
child 1907 0f3363d24239
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
       
     1 """This file contains some basic selectors required by application objects.
       
     2 
       
     3 A selector is responsible to score how well an object may be used with a
       
     4 given context by returning a score.
       
     5 
       
     6 In CubicWeb Usually the context consists for a request object, a result set
       
     7 or None, a specific row/col in the result set, etc...
       
     8 
       
     9 
       
    10 If you have trouble with selectors, especially if the objet (typically
       
    11 a view or a component) you want to use is not selected and you want to
       
    12 know which one(s) of its selectors fail (e.g. returns 0), you can use
       
    13 `traced_selection` or even direclty `TRACED_OIDS`.
       
    14 
       
    15 `TRACED_OIDS` is a tuple of traced object ids. The special value
       
    16 'all' may be used to log selectors for all objects.
       
    17 
       
    18 For instance, say that the following code yields a `NoSelectableObject`
       
    19 exception::
       
    20 
       
    21     self.view('calendar', myrset)
       
    22 
       
    23 You can log the selectors involved for *calendar* by replacing the line
       
    24 above by::
       
    25 
       
    26     # in Python2.5
       
    27     from cubicweb.selectors import traced_selection
       
    28     with traced_selection():
       
    29         self.view('calendar', myrset)
       
    30 
       
    31     # in Python2.4
       
    32     from cubicweb import selectors
       
    33     selectors.TRACED_OIDS = ('calendar',)
       
    34     self.view('calendar', myrset)
       
    35     selectors.TRACED_OIDS = ()
       
    36 
       
    37 
       
    38 :organization: Logilab
       
    39 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
    40 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
    41 """
       
    42 
       
    43 __docformat__ = "restructuredtext en"
       
    44 
       
    45 import logging
       
    46 from warnings import warn
       
    47 
       
    48 from logilab.common.compat import all
       
    49 from logilab.common.deprecation import deprecated_function
       
    50 from logilab.common.interface import implements as implements_iface
       
    51 
       
    52 from yams import BASE_TYPES
       
    53 
       
    54 from cubicweb import (Unauthorized, NoSelectableObject, NotAnEntity,
       
    55                       role, typed_eid)
       
    56 from cubicweb.vregistry import (NoSelectableObject, Selector,
       
    57                                 chainall, objectify_selector)
       
    58 from cubicweb.cwconfig import CubicWebConfiguration
       
    59 from cubicweb.schema import split_expression
       
    60 
       
    61 # helpers for debugging selectors
       
    62 SELECTOR_LOGGER = logging.getLogger('cubicweb.selectors')
       
    63 TRACED_OIDS = ()
       
    64 
       
    65 def lltrace(selector):
       
    66     # don't wrap selectors if not in development mode
       
    67     if CubicWebConfiguration.mode == 'installed':
       
    68         return selector
       
    69     def traced(cls, *args, **kwargs):
       
    70         # /!\ lltrace decorates pure function or __call__ method, this
       
    71         #     means argument order may be different
       
    72         if isinstance(cls, Selector):
       
    73             selname = str(cls)
       
    74             vobj = args[0]
       
    75         else:
       
    76             selname = selector.__name__
       
    77             vobj = cls
       
    78         oid = vobj.id
       
    79         ret = selector(cls, *args, **kwargs)
       
    80         if TRACED_OIDS == 'all' or oid in TRACED_OIDS:
       
    81             #SELECTOR_LOGGER.warning('selector %s returned %s for %s', selname, ret, cls)
       
    82             print 'selector %s returned %s for %s' % (selname, ret, vobj)
       
    83         return ret
       
    84     traced.__name__ = selector.__name__
       
    85     return traced
       
    86 
       
    87 class traced_selection(object):
       
    88     """selector debugging helper.
       
    89 
       
    90     Typical usage is :
       
    91 
       
    92     >>> with traced_selection():
       
    93     ...     # some code in which you want to debug selectors
       
    94     ...     # for all objects
       
    95 
       
    96     or
       
    97 
       
    98     >>> with traced_selection( ('oid1', 'oid2') ):
       
    99     ...     # some code in which you want to debug selectors
       
   100     ...     # for objects with id 'oid1' and 'oid2'
       
   101 
       
   102     """
       
   103     def __init__(self, traced='all'):
       
   104         self.traced = traced
       
   105 
       
   106     def __enter__(self):
       
   107         global TRACED_OIDS
       
   108         TRACED_OIDS = self.traced
       
   109 
       
   110     def __exit__(self, exctype, exc, traceback):
       
   111         global TRACED_OIDS
       
   112         TRACED_OIDS = ()
       
   113         return traceback is None
       
   114 
       
   115 
       
   116 def score_interface(cls_or_inst, cls, iface):
       
   117     """Return true if the give object (maybe an instance or class) implements
       
   118     the interface.
       
   119     """
       
   120     if getattr(iface, '__registry__', None) == 'etypes':
       
   121         # adjust score if the interface is an entity class
       
   122         parents = cls_or_inst.parent_classes()
       
   123         if iface is cls:
       
   124             return len(parents) + 4
       
   125         if iface is parents[-1]: # Any
       
   126             return 1
       
   127         for index, basecls in enumerate(reversed(parents[:-1])):
       
   128             if iface is basecls:
       
   129                 return index + 3
       
   130         return 0
       
   131     if implements_iface(cls_or_inst, iface):
       
   132         # implenting an interface takes precedence other special Any interface
       
   133         return 2
       
   134     return 0
       
   135 
       
   136 
       
   137 # abstract selectors ##########################################################
       
   138 
       
   139 class PartialSelectorMixIn(object):
       
   140     """convenience mix-in for selectors that will look into the containing
       
   141     class to find missing information.
       
   142 
       
   143     cf. `cubicweb.web.action.LinkToEntityAction` for instance
       
   144     """
       
   145     def __call__(self, cls, *args, **kwargs):
       
   146         self.complete(cls)
       
   147         return super(PartialSelectorMixIn, self).__call__(cls, *args, **kwargs)
       
   148 
       
   149 
       
   150 class ImplementsMixIn(object):
       
   151     """mix-in class for selectors checking implemented interfaces of something
       
   152     """
       
   153     def __init__(self, *expected_ifaces):
       
   154         super(ImplementsMixIn, self).__init__()
       
   155         self.expected_ifaces = expected_ifaces
       
   156 
       
   157     def __str__(self):
       
   158         return '%s(%s)' % (self.__class__.__name__,
       
   159                            ','.join(str(s) for s in self.expected_ifaces))
       
   160 
       
   161     def score_interfaces(self, cls_or_inst, cls):
       
   162         score = 0
       
   163         vreg, eschema = cls_or_inst.vreg, cls_or_inst.e_schema
       
   164         for iface in self.expected_ifaces:
       
   165             if isinstance(iface, basestring):
       
   166                 # entity type
       
   167                 try:
       
   168                     iface = vreg.etype_class(iface)
       
   169                 except KeyError:
       
   170                     continue # entity type not in the schema
       
   171             score += score_interface(cls_or_inst, cls, iface)
       
   172         return score
       
   173 
       
   174 
       
   175 class EClassSelector(Selector):
       
   176     """abstract class for selectors working on the entity classes of the result
       
   177     set. Its __call__ method has the following behaviour:
       
   178 
       
   179     * if row is specified, return the score returned by the score_class method
       
   180       called with the entity class found in the specified cell
       
   181     * else return the sum of score returned by the score_class method for each
       
   182       entity type found in the specified column, unless:
       
   183       - `once_is_enough` is True, in which case the first non-zero score is
       
   184         returned
       
   185       - `once_is_enough` is False, in which case if score_class return 0, 0 is
       
   186         returned
       
   187     """
       
   188     def __init__(self, once_is_enough=False):
       
   189         self.once_is_enough = once_is_enough
       
   190 
       
   191     @lltrace
       
   192     def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
       
   193         if not rset:
       
   194             return 0
       
   195         score = 0
       
   196         if row is None:
       
   197             for etype in rset.column_types(col):
       
   198                 if etype is None: # outer join
       
   199                     continue
       
   200                 escore = self.score(cls, req, etype)
       
   201                 if not escore and not self.once_is_enough:
       
   202                     return 0
       
   203                 elif self.once_is_enough:
       
   204                     return escore
       
   205                 score += escore
       
   206         else:
       
   207             etype = rset.description[row][col]
       
   208             if etype is not None:
       
   209                 score = self.score(cls, req, etype)
       
   210         return score
       
   211 
       
   212     def score(self, cls, req, etype):
       
   213         if etype in BASE_TYPES:
       
   214             return 0
       
   215         return self.score_class(cls.vreg.etype_class(etype), req)
       
   216 
       
   217     def score_class(self, eclass, req):
       
   218         raise NotImplementedError()
       
   219 
       
   220 
       
   221 class EntitySelector(EClassSelector):
       
   222     """abstract class for selectors working on the entity instances of the
       
   223     result set. Its __call__ method has the following behaviour:
       
   224 
       
   225     * if 'entity' find in kwargs, return the score returned by the score_entity
       
   226       method for this entity
       
   227     * if row is specified, return the score returned by the score_entity method
       
   228       called with the entity instance found in the specified cell
       
   229     * else return the sum of score returned by the score_entity method for each
       
   230       entity found in the specified column, unless:
       
   231       - `once_is_enough` is True, in which case the first non-zero score is
       
   232         returned
       
   233       - `once_is_enough` is False, in which case if score_class return 0, 0 is
       
   234         returned
       
   235 
       
   236     note: None values (resulting from some outer join in the query) are not
       
   237           considered.
       
   238     """
       
   239 
       
   240     @lltrace
       
   241     def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
       
   242         if not rset and not kwargs.get('entity'):
       
   243             return 0
       
   244         score = 0
       
   245         if kwargs.get('entity'):
       
   246             score = self.score_entity(kwargs['entity'])
       
   247         elif row is None:
       
   248             for row, rowvalue in enumerate(rset.rows):
       
   249                 if rowvalue[col] is None: # outer join
       
   250                     continue
       
   251                 escore = self.score(req, rset, row, col)
       
   252                 if not escore and not self.once_is_enough:
       
   253                     return 0
       
   254                 elif self.once_is_enough:
       
   255                     return escore
       
   256                 score += escore
       
   257         else:
       
   258             etype = rset.description[row][col]
       
   259             if etype is not None: # outer join
       
   260                 score = self.score(req, rset, row, col)
       
   261         return score
       
   262 
       
   263     def score(self, req, rset, row, col):
       
   264         try:
       
   265             return self.score_entity(rset.get_entity(row, col))
       
   266         except NotAnEntity:
       
   267             return 0
       
   268 
       
   269     def score_entity(self, entity):
       
   270         raise NotImplementedError()
       
   271 
       
   272 
       
   273 # very basic selectors ########################################################
       
   274 
       
   275 class yes(Selector):
       
   276     """return arbitrary score"""
       
   277     def __init__(self, score=1):
       
   278         self.score = score
       
   279     def __call__(self, *args, **kwargs):
       
   280         return self.score
       
   281 
       
   282 @objectify_selector
       
   283 @lltrace
       
   284 def none_rset(cls, req, rset, *args, **kwargs):
       
   285     """accept no result set (e.g. given rset is None)"""
       
   286     if rset is None:
       
   287         return 1
       
   288     return 0
       
   289 
       
   290 @objectify_selector
       
   291 @lltrace
       
   292 def any_rset(cls, req, rset, *args, **kwargs):
       
   293     """accept result set, whatever the number of result it contains"""
       
   294     if rset is not None:
       
   295         return 1
       
   296     return 0
       
   297 
       
   298 @objectify_selector
       
   299 @lltrace
       
   300 def nonempty_rset(cls, req, rset, *args, **kwargs):
       
   301     """accept any non empty result set"""
       
   302     if rset is not None and rset.rowcount:
       
   303         return 1
       
   304     return 0
       
   305 
       
   306 @objectify_selector
       
   307 @lltrace
       
   308 def empty_rset(cls, req, rset, *args, **kwargs):
       
   309     """accept empty result set"""
       
   310     if rset is not None and rset.rowcount == 0:
       
   311         return 1
       
   312     return 0
       
   313 
       
   314 @objectify_selector
       
   315 @lltrace
       
   316 def one_line_rset(cls, req, rset, row=None, *args, **kwargs):
       
   317     """if row is specified, accept result set with a single line of result,
       
   318     else accepts anyway
       
   319     """
       
   320     if rset is not None and (row is not None or rset.rowcount == 1):
       
   321         return 1
       
   322     return 0
       
   323 
       
   324 @objectify_selector
       
   325 @lltrace
       
   326 def two_lines_rset(cls, req, rset, *args, **kwargs):
       
   327     """accept result set with *at least* two lines of result"""
       
   328     if rset is not None and rset.rowcount > 1:
       
   329         return 1
       
   330     return 0
       
   331 
       
   332 @objectify_selector
       
   333 @lltrace
       
   334 def two_cols_rset(cls, req, rset, *args, **kwargs):
       
   335     """accept result set with at least one line and two columns of result"""
       
   336     if rset is not None and rset.rowcount and len(rset.rows[0]) > 1:
       
   337         return 1
       
   338     return 0
       
   339 
       
   340 @objectify_selector
       
   341 @lltrace
       
   342 def paginated_rset(cls, req, rset, *args, **kwargs):
       
   343     """accept result set with more lines than the page size.
       
   344 
       
   345     Page size is searched in (respecting order):
       
   346     * a page_size argument
       
   347     * a page_size form parameters
       
   348     * the navigation.page-size property
       
   349     """
       
   350     page_size = kwargs.get('page_size')
       
   351     if page_size is None:
       
   352         page_size = req.form.get('page_size')
       
   353         if page_size is None:
       
   354             page_size = req.property_value('navigation.page-size')
       
   355         else:
       
   356             page_size = int(page_size)
       
   357     if rset is None or rset.rowcount <= page_size:
       
   358         return 0
       
   359     return 1
       
   360 
       
   361 @objectify_selector
       
   362 @lltrace
       
   363 def sorted_rset(cls, req, rset, row=None, col=0, **kwargs):
       
   364     """accept sorted result set"""
       
   365     rqlst = rset.syntax_tree()
       
   366     if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
       
   367         return 0
       
   368     return 2
       
   369 
       
   370 @objectify_selector
       
   371 @lltrace
       
   372 def one_etype_rset(cls, req, rset, row=None, col=0, *args, **kwargs):
       
   373     """accept result set where entities in the specified column (or 0) are all
       
   374     of the same type
       
   375     """
       
   376     if rset is None:
       
   377         return 0
       
   378     if len(rset.column_types(col)) != 1:
       
   379         return 0
       
   380     return 1
       
   381 
       
   382 @objectify_selector
       
   383 @lltrace
       
   384 def two_etypes_rset(cls, req, rset, row=None, col=0, **kwargs):
       
   385     """accept result set where entities in the specified column (or 0) are not
       
   386     of the same type
       
   387     """
       
   388     if rset:
       
   389         etypes = rset.column_types(col)
       
   390         if len(etypes) > 1:
       
   391             return 1
       
   392     return 0
       
   393 
       
   394 class non_final_entity(EClassSelector):
       
   395     """accept if entity type found in the result set is non final.
       
   396 
       
   397     See `EClassSelector` documentation for behaviour when row is not specified.
       
   398     """
       
   399     def score(self, cls, req, etype):
       
   400         if etype in BASE_TYPES:
       
   401             return 0
       
   402         return 1
       
   403 
       
   404 @objectify_selector
       
   405 @lltrace
       
   406 def authenticated_user(cls, req, *args, **kwargs):
       
   407     """accept if user is authenticated"""
       
   408     if req.cnx.anonymous_connection:
       
   409         return 0
       
   410     return 1
       
   411 
       
   412 def anonymous_user():
       
   413     return ~ authenticated_user()
       
   414 
       
   415 @objectify_selector
       
   416 @lltrace
       
   417 def primary_view(cls, req, rset, row=None, col=0, view=None, **kwargs):
       
   418     """accept if view given as named argument is a primary view, or if no view
       
   419     is given
       
   420     """
       
   421     if view is not None and not view.is_primary():
       
   422         return 0
       
   423     return 1
       
   424 
       
   425 @objectify_selector
       
   426 @lltrace
       
   427 def match_context_prop(cls, req, rset, row=None, col=0, context=None,
       
   428                        **kwargs):
       
   429     """accept if:
       
   430     * no context given
       
   431     * context (`basestring`) is matching the context property value for the
       
   432       given cls
       
   433     """
       
   434     propval = req.property_value('%s.%s.context' % (cls.__registry__, cls.id))
       
   435     if not propval:
       
   436         propval = cls.context
       
   437     if context is not None and propval and context != propval:
       
   438         return 0
       
   439     return 1
       
   440 
       
   441 
       
   442 class match_search_state(Selector):
       
   443     """accept if the current request search state is in one of the expected
       
   444     states given to the initializer
       
   445 
       
   446     :param expected: either 'normal' or 'linksearch' (eg searching for an
       
   447                      object to create a relation with another)
       
   448     """
       
   449     def __init__(self, *expected):
       
   450         assert expected, self
       
   451         self.expected = frozenset(expected)
       
   452 
       
   453     def __str__(self):
       
   454         return '%s(%s)' % (self.__class__.__name__,
       
   455                            ','.join(sorted(str(s) for s in self.expected)))
       
   456 
       
   457     @lltrace
       
   458     def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
       
   459         try:
       
   460             if not req.search_state[0] in self.expected:
       
   461                 return 0
       
   462         except AttributeError:
       
   463             return 1 # class doesn't care about search state, accept it
       
   464         return 1
       
   465 
       
   466 
       
   467 class match_form_params(match_search_state):
       
   468     """accept if parameters specified as initializer arguments are specified
       
   469     in request's form parameters
       
   470 
       
   471     :param *expected: parameters (eg `basestring`) which are expected to be
       
   472                       found in request's form parameters
       
   473     """
       
   474 
       
   475     @lltrace
       
   476     def __call__(self, cls, req, *args, **kwargs):
       
   477         score = 0
       
   478         for param in self.expected:
       
   479             val = req.form.get(param)
       
   480             if not val:
       
   481                 return 0
       
   482             score += 1
       
   483         return len(self.expected)
       
   484 
       
   485 
       
   486 class match_kwargs(match_search_state):
       
   487     """accept if parameters specified as initializer arguments are specified
       
   488     in named arguments given to the selector
       
   489 
       
   490     :param *expected: parameters (eg `basestring`) which are expected to be
       
   491                       found in named arguments (kwargs)
       
   492     """
       
   493 
       
   494     @lltrace
       
   495     def __call__(self, cls, req, *args, **kwargs):
       
   496         for arg in self.expected:
       
   497             if not arg in kwargs:
       
   498                 return 0
       
   499         return len(self.expected)
       
   500 
       
   501 
       
   502 class match_user_groups(match_search_state):
       
   503     """accept if logged users is in at least one of the given groups. Returned
       
   504     score is the number of groups in which the user is.
       
   505 
       
   506     If the special 'owners' group is given:
       
   507     * if row is specified check the entity at the given row/col is owned by the
       
   508       logged user
       
   509     * if row is not specified check all entities in col are owned by the logged
       
   510       user
       
   511 
       
   512     :param *required_groups: name of groups (`basestring`) in which the logged
       
   513                              user should be
       
   514     """
       
   515 
       
   516     @lltrace
       
   517     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
   518         user = req.user
       
   519         if user is None:
       
   520             return int('guests' in self.expected)
       
   521         score = user.matching_groups(self.expected)
       
   522         if not score and 'owners' in self.expected and rset:
       
   523             if row is not None:
       
   524                 if not user.owns(rset[row][col]):
       
   525                     return 0
       
   526                 score = 1
       
   527             else:
       
   528                 score = all(user.owns(r[col]) for r in rset)
       
   529         return score
       
   530 
       
   531 
       
   532 class match_transition(match_search_state):
       
   533     @lltrace
       
   534     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
       
   535         try:
       
   536             trname = req.execute('Any XN WHERE X is Transition, X eid %(x)s, X name XN',
       
   537                                  {'x': typed_eid(req.form['treid'])})[0][0]
       
   538         except (KeyError, IndexError):
       
   539             return 0
       
   540         # XXX check this is a transition that apply to the object?
       
   541         if not trname in self.expected:
       
   542             return 0
       
   543         return 1
       
   544 
       
   545 
       
   546 class match_view(match_search_state):
       
   547     """accept if the current view is in one of the expected vid given to the
       
   548     initializer
       
   549     """
       
   550     @lltrace
       
   551     def __call__(self, cls, req, rset, row=None, col=0, view=None, **kwargs):
       
   552         if view is None or not view.id in self.expected:
       
   553             return 0
       
   554         return 1
       
   555 
       
   556 
       
   557 class appobject_selectable(Selector):
       
   558     """accept with another appobject is selectable using selector's input
       
   559     context.
       
   560 
       
   561     :param registry: a registry name (`basestring`)
       
   562     :param oid: an object identifier (`basestring`)
       
   563     """
       
   564     def __init__(self, registry, oid):
       
   565         self.registry = registry
       
   566         self.oid = oid
       
   567 
       
   568     def __call__(self, cls, req, rset, *args, **kwargs):
       
   569         try:
       
   570             cls.vreg.select_object(self.registry, self.oid, req, rset, *args, **kwargs)
       
   571             return 1
       
   572         except NoSelectableObject:
       
   573             return 0
       
   574 
       
   575 
       
   576 # not so basic selectors ######################################################
       
   577 
       
   578 class implements(ImplementsMixIn, EClassSelector):
       
   579     """accept if entity classes found in the result set implements at least one
       
   580     of the interfaces given as argument. Returned score is the number of
       
   581     implemented interfaces.
       
   582 
       
   583     See `EClassSelector` documentation for behaviour when row is not specified.
       
   584 
       
   585     :param *expected_ifaces: expected interfaces. An interface may be a class
       
   586                              or an entity type (e.g. `basestring`) in which case
       
   587                              the associated class will be searched in the
       
   588                              registry (at selection time)
       
   589 
       
   590     note: when interface is an entity class, the score will reflect class
       
   591           proximity so the most specific object'll be selected
       
   592     """
       
   593     def score_class(self, eclass, req):
       
   594         return self.score_interfaces(eclass, eclass)
       
   595 
       
   596 
       
   597 class specified_etype_implements(implements):
       
   598     """accept if entity class specified using an 'etype' parameters in name
       
   599     argument or request form implements at least one of the interfaces given as
       
   600     argument. Returned score is the number of implemented interfaces.
       
   601 
       
   602     :param *expected_ifaces: expected interfaces. An interface may be a class
       
   603                              or an entity type (e.g. `basestring`) in which case
       
   604                              the associated class will be searched in the
       
   605                              registry (at selection time)
       
   606 
       
   607     note: when interface is an entity class, the score will reflect class
       
   608           proximity so the most specific object'll be selected
       
   609     """
       
   610 
       
   611     @lltrace
       
   612     def __call__(self, cls, req, *args, **kwargs):
       
   613         try:
       
   614             etype = req.form['etype']
       
   615         except KeyError:
       
   616             try:
       
   617                 etype = kwargs['etype']
       
   618             except KeyError:
       
   619                 return 0
       
   620         return self.score_class(cls.vreg.etype_class(etype), req)
       
   621 
       
   622 
       
   623 class entity_implements(ImplementsMixIn, EntitySelector):
       
   624     """accept if entity instances found in the result set implements at least one
       
   625     of the interfaces given as argument. Returned score is the number of
       
   626     implemented interfaces.
       
   627 
       
   628     See `EntitySelector` documentation for behaviour when row is not specified.
       
   629 
       
   630     :param *expected_ifaces: expected interfaces. An interface may be a class
       
   631                              or an entity type (e.g. `basestring`) in which case
       
   632                              the associated class will be searched in the
       
   633                              registry (at selection time)
       
   634 
       
   635     note: when interface is an entity class, the score will reflect class
       
   636           proximity so the most specific object'll be selected
       
   637     """
       
   638     def score_entity(self, entity):
       
   639         return self.score_interfaces(entity, entity.__class__)
       
   640 
       
   641 
       
   642 class relation_possible(EClassSelector):
       
   643     """accept if entity class found in the result set support the relation.
       
   644 
       
   645     See `EClassSelector` documentation for behaviour when row is not specified.
       
   646 
       
   647     :param rtype: a relation type (`basestring`)
       
   648     :param role: the role of the result set entity in the relation. 'subject' or
       
   649                  'object', default to 'subject'.
       
   650     :param target_type: if specified, check the relation's end may be of this
       
   651                         target type (`basestring`)
       
   652     :param action: a relation schema action (one of 'read', 'add', 'delete')
       
   653                    which must be granted to the logged user, else a 0 score will
       
   654                    be returned
       
   655     """
       
   656     def __init__(self, rtype, role='subject', target_etype=None,
       
   657                  action='read', once_is_enough=False):
       
   658         super(relation_possible, self).__init__(once_is_enough)
       
   659         self.rtype = rtype
       
   660         self.role = role
       
   661         self.target_etype = target_etype
       
   662         self.action = action
       
   663 
       
   664     @lltrace
       
   665     def __call__(self, cls, req, *args, **kwargs):
       
   666         rschema = cls.schema.rschema(self.rtype)
       
   667         if not (rschema.has_perm(req, self.action)
       
   668                 or rschema.has_local_role(self.action)):
       
   669             return 0
       
   670         score = super(relation_possible, self).__call__(cls, req, *args, **kwargs)
       
   671         return score
       
   672 
       
   673     def score_class(self, eclass, req):
       
   674         eschema = eclass.e_schema
       
   675         try:
       
   676             if self.role == 'object':
       
   677                 rschema = eschema.object_relation(self.rtype)
       
   678             else:
       
   679                 rschema = eschema.subject_relation(self.rtype)
       
   680         except KeyError:
       
   681             return 0
       
   682         if self.target_etype is not None:
       
   683             try:
       
   684                 if self.role == 'subject':
       
   685                     return int(self.target_etype in rschema.objects(eschema))
       
   686                 else:
       
   687                     return int(self.target_etype in rschema.subjects(eschema))
       
   688             except KeyError:
       
   689                 return 0
       
   690         return 1
       
   691 
       
   692 
       
   693 class partial_relation_possible(PartialSelectorMixIn, relation_possible):
       
   694     """partial version of the relation_possible selector
       
   695 
       
   696     The selector will look for class attributes to find its missing
       
   697     information. The list of attributes required on the class
       
   698     for this selector are:
       
   699 
       
   700     - `rtype`: same as `rtype` parameter of the `relation_possible` selector
       
   701 
       
   702     - `role`: this attribute will be passed to the `cubicweb.role` function
       
   703       to determine the role of class in the relation
       
   704 
       
   705     - `etype` (optional): the entity type on the other side of the relation
       
   706 
       
   707     :param action: a relation schema action (one of 'read', 'add', 'delete')
       
   708                    which must be granted to the logged user, else a 0 score will
       
   709                    be returned
       
   710     """
       
   711     def __init__(self, action='read', once_is_enough=False):
       
   712         super(partial_relation_possible, self).__init__(None, None, None,
       
   713                                                         action, once_is_enough)
       
   714 
       
   715     def complete(self, cls):
       
   716         self.rtype = cls.rtype
       
   717         self.role = role(cls)
       
   718         self.target_etype = getattr(cls, 'etype', None)
       
   719 
       
   720 
       
   721 class may_add_relation(EntitySelector):
       
   722     """accept if the relation can be added to an entity found in the result set
       
   723     by the logged user.
       
   724 
       
   725     See `EntitySelector` documentation for behaviour when row is not specified.
       
   726 
       
   727     :param rtype: a relation type (`basestring`)
       
   728     :param role: the role of the result set entity in the relation. 'subject' or
       
   729                  'object', default to 'subject'.
       
   730     """
       
   731 
       
   732     def __init__(self, rtype, role='subject', once_is_enough=False):
       
   733         super(may_add_relation, self).__init__(once_is_enough)
       
   734         self.rtype = rtype
       
   735         self.role = role
       
   736 
       
   737     def score_entity(self, entity):
       
   738         rschema = entity.schema.rschema(self.rtype)
       
   739         if self.role == 'subject':
       
   740             if not rschema.has_perm(entity.req, 'add', fromeid=entity.eid):
       
   741                 return 0
       
   742         elif not rschema.has_perm(entity.req, 'add', toeid=entity.eid):
       
   743             return 0
       
   744         return 1
       
   745 
       
   746 
       
   747 class partial_may_add_relation(PartialSelectorMixIn, may_add_relation):
       
   748     """partial version of the may_add_relation selector
       
   749 
       
   750     The selector will look for class attributes to find its missing
       
   751     information. The list of attributes required on the class
       
   752     for this selector are:
       
   753 
       
   754     - `rtype`: same as `rtype` parameter of the `relation_possible` selector
       
   755 
       
   756     - `role`: this attribute will be passed to the `cubicweb.role` function
       
   757       to determine the role of class in the relation.
       
   758 
       
   759     :param action: a relation schema action (one of 'read', 'add', 'delete')
       
   760                    which must be granted to the logged user, else a 0 score will
       
   761                    be returned
       
   762     """
       
   763     def __init__(self, once_is_enough=False):
       
   764         super(partial_may_add_relation, self).__init__(None, None, once_is_enough)
       
   765 
       
   766     def complete(self, cls):
       
   767         self.rtype = cls.rtype
       
   768         self.role = role(cls)
       
   769 
       
   770 
       
   771 class has_related_entities(EntitySelector):
       
   772     """accept if entity found in the result set has some linked entities using
       
   773     the specified relation (optionaly filtered according to the specified target
       
   774     type). Checks first if the relation is possible.
       
   775 
       
   776     See `EntitySelector` documentation for behaviour when row is not specified.
       
   777 
       
   778     :param rtype: a relation type (`basestring`)
       
   779     :param role: the role of the result set entity in the relation. 'subject' or
       
   780                  'object', default to 'subject'.
       
   781     :param target_type: if specified, check the relation's end may be of this
       
   782                         target type (`basestring`)
       
   783     """
       
   784     def __init__(self, rtype, role='subject', target_etype=None,
       
   785                  once_is_enough=False):
       
   786         super(has_related_entities, self).__init__(once_is_enough)
       
   787         self.rtype = rtype
       
   788         self.role = role
       
   789         self.target_etype = target_etype
       
   790 
       
   791     def score_entity(self, entity):
       
   792         relpossel = relation_possible(self.rtype, self.role, self.target_etype)
       
   793         if not relpossel.score_class(entity.__class__, entity.req):
       
   794             return 0
       
   795         rset = entity.related(self.rtype, self.role)
       
   796         if self.target_etype:
       
   797             return any(r for r in rset.description if r[0] == self.target_etype)
       
   798         return rset and 1 or 0
       
   799 
       
   800 
       
   801 class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
       
   802     """partial version of the has_related_entities selector
       
   803 
       
   804     The selector will look for class attributes to find its missing
       
   805     information. The list of attributes required on the class
       
   806     for this selector are:
       
   807 
       
   808     - `rtype`: same as `rtype` parameter of the `relation_possible` selector
       
   809 
       
   810     - `role`: this attribute will be passed to the `cubicweb.role` function
       
   811       to determine the role of class in the relation.
       
   812 
       
   813     - `etype` (optional): the entity type on the other side of the relation
       
   814 
       
   815     :param action: a relation schema action (one of 'read', 'add', 'delete')
       
   816                    which must be granted to the logged user, else a 0 score will
       
   817                    be returned
       
   818     """
       
   819     def __init__(self, once_is_enough=False):
       
   820         super(partial_has_related_entities, self).__init__(None, None,
       
   821                                                            None, once_is_enough)
       
   822     def complete(self, cls):
       
   823         self.rtype = cls.rtype
       
   824         self.role = role(cls)
       
   825         self.target_etype = getattr(cls, 'etype', None)
       
   826 
       
   827 
       
   828 class has_permission(EntitySelector):
       
   829     """accept if user has the permission to do the requested action on a result
       
   830     set entity.
       
   831 
       
   832     * if row is specified, return 1 if user has the permission on the entity
       
   833       instance found in the specified cell
       
   834     * else return a positive score if user has the permission for every entity
       
   835       in the found in the specified column
       
   836 
       
   837     note: None values (resulting from some outer join in the query) are not
       
   838           considered.
       
   839 
       
   840     :param action: an entity schema action (eg 'read'/'add'/'delete'/'update')
       
   841     """
       
   842     def __init__(self, action, once_is_enough=False):
       
   843         super(has_permission, self).__init__(once_is_enough)
       
   844         self.action = action
       
   845 
       
   846     @lltrace
       
   847     def __call__(self, cls, req, rset, row=None, col=0, **kwargs):
       
   848         if rset is None:
       
   849             return 0
       
   850         user = req.user
       
   851         action = self.action
       
   852         if row is None:
       
   853             score = 0
       
   854             need_local_check = []
       
   855             geteschema = cls.schema.eschema
       
   856             for etype in rset.column_types(0):
       
   857                 if etype in BASE_TYPES:
       
   858                     return 0
       
   859                 eschema = geteschema(etype)
       
   860                 if not user.matching_groups(eschema.get_groups(action)):
       
   861                     if eschema.has_local_role(action):
       
   862                         # have to ckeck local roles
       
   863                         need_local_check.append(eschema)
       
   864                         continue
       
   865                     else:
       
   866                         # even a local role won't be enough
       
   867                         return 0
       
   868                 score += 1
       
   869             if need_local_check:
       
   870                 # check local role for entities of necessary types
       
   871                 for i, row in enumerate(rset):
       
   872                     if not rset.description[i][0] in need_local_check:
       
   873                         continue
       
   874                     if not self.score(req, rset, i, col):
       
   875                         return 0
       
   876                 score += 1
       
   877             return score
       
   878         return self.score(req, rset, row, col)
       
   879 
       
   880     def score_entity(self, entity):
       
   881         if entity.has_perm(self.action):
       
   882             return 1
       
   883         return 0
       
   884 
       
   885 
       
   886 class has_add_permission(EClassSelector):
       
   887     """accept if logged user has the add permission on entity class found in the
       
   888     result set, and class is not a strict subobject.
       
   889 
       
   890     See `EClassSelector` documentation for behaviour when row is not specified.
       
   891     """
       
   892     def score(self, cls, req, etype):
       
   893         eschema = cls.schema.eschema(etype)
       
   894         if not (eschema.is_final() or eschema.is_subobject(strict=True)) \
       
   895                and eschema.has_perm(req, 'add'):
       
   896             return 1
       
   897         return 0
       
   898 
       
   899 
       
   900 class rql_condition(EntitySelector):
       
   901     """accept if an arbitrary rql return some results for an eid found in the
       
   902     result set. Returned score is the number of items returned by the rql
       
   903     condition.
       
   904 
       
   905     See `EntitySelector` documentation for behaviour when row is not specified.
       
   906 
       
   907     :param expression: basestring containing an rql expression, which should use
       
   908                        X variable to represent the context entity and may use U
       
   909                        to represent the logged user
       
   910 
       
   911     return the sum of the number of items returned by the rql condition as score
       
   912     or 0 at the first entity scoring to zero.
       
   913     """
       
   914     def __init__(self, expression, once_is_enough=False):
       
   915         super(rql_condition, self).__init__(once_is_enough)
       
   916         if 'U' in frozenset(split_expression(expression)):
       
   917             rql = 'Any X WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression
       
   918         else:
       
   919             rql = 'Any X WHERE X eid %%(x)s, %s' % expression
       
   920         self.rql = rql
       
   921 
       
   922     def score(self, req, rset, row, col):
       
   923         try:
       
   924             return len(req.execute(self.rql, {'x': rset[row][col],
       
   925                                               'u': req.user.eid}, 'x'))
       
   926         except Unauthorized:
       
   927             return 0
       
   928 
       
   929 
       
   930 class but_etype(EntitySelector):
       
   931     """accept if the given entity types are not found in the result set.
       
   932 
       
   933     See `EntitySelector` documentation for behaviour when row is not specified.
       
   934 
       
   935     :param *etypes: entity types (`basestring`) which should be refused
       
   936     """
       
   937     def __init__(self, *etypes):
       
   938         super(but_etype, self).__init__()
       
   939         self.but_etypes = etypes
       
   940 
       
   941     def score(self, req, rset, row, col):
       
   942         if rset.description[row][col] in self.but_etypes:
       
   943             return 0
       
   944         return 1
       
   945 
       
   946 
       
   947 class score_entity(EntitySelector):
       
   948     """accept if some arbitrary function return a positive score for an entity
       
   949     found in the result set.
       
   950 
       
   951     See `EntitySelector` documentation for behaviour when row is not specified.
       
   952 
       
   953     :param scorefunc: callable expected to take an entity as argument and to
       
   954                       return a score >= 0
       
   955     """
       
   956     def __init__(self, scorefunc, once_is_enough=False):
       
   957         super(score_entity, self).__init__(once_is_enough)
       
   958         self.score_entity = scorefunc
       
   959 
       
   960 
       
   961 # XXX DEPRECATED ##############################################################
       
   962 
       
   963 yes_selector = deprecated_function(yes)
       
   964 norset_selector = deprecated_function(none_rset)
       
   965 rset_selector = deprecated_function(any_rset)
       
   966 anyrset_selector = deprecated_function(nonempty_rset)
       
   967 emptyrset_selector = deprecated_function(empty_rset)
       
   968 onelinerset_selector = deprecated_function(one_line_rset)
       
   969 twolinerset_selector = deprecated_function(two_lines_rset)
       
   970 twocolrset_selector = deprecated_function(two_cols_rset)
       
   971 largerset_selector = deprecated_function(paginated_rset)
       
   972 sortedrset_selector = deprecated_function(sorted_rset)
       
   973 oneetyperset_selector = deprecated_function(one_etype_rset)
       
   974 multitype_selector = deprecated_function(two_etypes_rset)
       
   975 anonymous_selector = deprecated_function(anonymous_user)
       
   976 not_anonymous_selector = deprecated_function(authenticated_user)
       
   977 primaryview_selector = deprecated_function(primary_view)
       
   978 contextprop_selector = deprecated_function(match_context_prop)
       
   979 
       
   980 def nfentity_selector(cls, req, rset, row=None, col=0, **kwargs):
       
   981     return non_final_entity()(cls, req, rset, row, col)
       
   982 nfentity_selector = deprecated_function(nfentity_selector)
       
   983 
       
   984 def implement_interface(cls, req, rset, row=None, col=0, **kwargs):
       
   985     return implements(*cls.accepts_interfaces)(cls, req, rset, row, col)
       
   986 _interface_selector = deprecated_function(implement_interface)
       
   987 interface_selector = deprecated_function(implement_interface)
       
   988 implement_interface = deprecated_function(implement_interface, 'use implements')
       
   989 
       
   990 def accept_etype(cls, req, *args, **kwargs):
       
   991     """check etype presence in request form *and* accepts conformance"""
       
   992     return specified_etype_implements(*cls.accepts)(cls, req, *args)
       
   993 etype_form_selector = deprecated_function(accept_etype)
       
   994 accept_etype = deprecated_function(accept_etype, 'use specified_etype_implements')
       
   995 
       
   996 def searchstate_selector(cls, req, rset, row=None, col=0, **kwargs):
       
   997     return match_search_state(cls.search_states)(cls, req, rset, row, col)
       
   998 searchstate_selector = deprecated_function(searchstate_selector)
       
   999 
       
  1000 def match_user_group(cls, req, rset=None, row=None, col=0, **kwargs):
       
  1001     return match_user_groups(*cls.require_groups)(cls, req, rset, row, col, **kwargs)
       
  1002 in_group_selector = deprecated_function(match_user_group)
       
  1003 match_user_group = deprecated_function(match_user_group)
       
  1004 
       
  1005 def has_relation(cls, req, rset, row=None, col=0, **kwargs):
       
  1006     return relation_possible(cls.rtype, role(cls), cls.etype,
       
  1007                              getattr(cls, 'require_permission', 'read'))(cls, req, rset, row, col, **kwargs)
       
  1008 has_relation = deprecated_function(has_relation)
       
  1009 
       
  1010 def one_has_relation(cls, req, rset, row=None, col=0, **kwargs):
       
  1011     return relation_possible(cls.rtype, role(cls), cls.etype,
       
  1012                              getattr(cls, 'require_permission', 'read',
       
  1013                                      once_is_enough=True))(cls, req, rset, row, col, **kwargs)
       
  1014 one_has_relation = deprecated_function(one_has_relation, 'use relation_possible selector')
       
  1015 
       
  1016 def accept_rset(cls, req, rset, row=None, col=0, **kwargs):
       
  1017     """simply delegate to cls.accept_rset method"""
       
  1018     return implements(*cls.accepts)(cls, req, rset, row=row, col=col)
       
  1019 accept_rset_selector = deprecated_function(accept_rset)
       
  1020 accept_rset = deprecated_function(accept_rset, 'use implements selector')
       
  1021 
       
  1022 accept = chainall(non_final_entity(), accept_rset, name='accept')
       
  1023 accept_selector = deprecated_function(accept)
       
  1024 accept = deprecated_function(accept, 'use implements selector')
       
  1025 
       
  1026 accept_one = deprecated_function(chainall(one_line_rset, accept,
       
  1027                                           name='accept_one'))
       
  1028 accept_one_selector = deprecated_function(accept_one)
       
  1029 
       
  1030 
       
  1031 def _rql_condition(cls, req, rset, row=None, col=0, **kwargs):
       
  1032     if cls.condition:
       
  1033         return rql_condition(cls.condition)(cls, req, rset, row, col)
       
  1034     return 1
       
  1035 _rqlcondition_selector = deprecated_function(_rql_condition)
       
  1036 
       
  1037 rqlcondition_selector = deprecated_function(chainall(non_final_entity(), one_line_rset, _rql_condition,
       
  1038                          name='rql_condition'))
       
  1039 
       
  1040 def but_etype_selector(cls, req, rset, row=None, col=0, **kwargs):
       
  1041     return but_etype(cls.etype)(cls, req, rset, row, col)
       
  1042 but_etype_selector = deprecated_function(but_etype_selector)
       
  1043 
       
  1044 @lltrace
       
  1045 def etype_rtype_selector(cls, req, rset, row=None, col=0, **kwargs):
       
  1046     schema = cls.schema
       
  1047     perm = getattr(cls, 'require_permission', 'read')
       
  1048     if hasattr(cls, 'etype'):
       
  1049         eschema = schema.eschema(cls.etype)
       
  1050         if not (eschema.has_perm(req, perm) or eschema.has_local_role(perm)):
       
  1051             return 0
       
  1052     if hasattr(cls, 'rtype'):
       
  1053         rschema = schema.rschema(cls.rtype)
       
  1054         if not (rschema.has_perm(req, perm) or rschema.has_local_role(perm)):
       
  1055             return 0
       
  1056     return 1
       
  1057 etype_rtype_selector = deprecated_function(etype_rtype_selector)
       
  1058 
       
  1059 #req_form_params_selector = deprecated_function(match_form_params) # form_params
       
  1060 #kwargs_selector = deprecated_function(match_kwargs) # expected_kwargs
       
  1061 
       
  1062 # compound selectors ##########################################################
       
  1063 
       
  1064 searchstate_accept = chainall(nonempty_rset(), accept,
       
  1065                               name='searchstate_accept')
       
  1066 searchstate_accept_selector = deprecated_function(searchstate_accept)
       
  1067 
       
  1068 searchstate_accept_one = chainall(one_line_rset, accept, _rql_condition,
       
  1069                                   name='searchstate_accept_one')
       
  1070 searchstate_accept_one_selector = deprecated_function(searchstate_accept_one)
       
  1071 
       
  1072 searchstate_accept = deprecated_function(searchstate_accept)
       
  1073 searchstate_accept_one = deprecated_function(searchstate_accept_one)
       
  1074 
       
  1075 
       
  1076 def unbind_method(selector):
       
  1077     def new_selector(registered):
       
  1078         # get the unbound method
       
  1079         if hasattr(registered, 'im_func'):
       
  1080             registered = registered.im_func
       
  1081         # don't rebind since it will be done automatically during
       
  1082         # the assignment, inside the destination class body
       
  1083         return selector(registered)
       
  1084     new_selector.__name__ = selector.__name__
       
  1085     return new_selector
       
  1086 
       
  1087 
       
  1088 def deprecate(registered, msg):
       
  1089     # get the unbound method
       
  1090     if hasattr(registered, 'im_func'):
       
  1091         registered = registered.im_func
       
  1092     def _deprecate(cls, vreg):
       
  1093         warn(msg, DeprecationWarning)
       
  1094         return registered(cls, vreg)
       
  1095     return _deprecate
       
  1096 
       
  1097 @unbind_method
       
  1098 def require_group_compat(registered):
       
  1099     def plug_selector(cls, vreg):
       
  1100         cls = registered(cls, vreg)
       
  1101         if getattr(cls, 'require_groups', None):
       
  1102             warn('use "match_user_groups(group1, group2)" instead of using require_groups',
       
  1103                  DeprecationWarning)
       
  1104             cls.__select__ &= match_user_groups(cls.require_groups)
       
  1105         return cls
       
  1106     return plug_selector
       
  1107 
       
  1108 @unbind_method
       
  1109 def accepts_compat(registered):
       
  1110     def plug_selector(cls, vreg):
       
  1111         cls = registered(cls, vreg)
       
  1112         if getattr(cls, 'accepts', None):
       
  1113             warn('use "implements("EntityType", IFace)" instead of using accepts on %s'
       
  1114                  % cls,
       
  1115                  DeprecationWarning)
       
  1116             cls.__select__ &= implements(*cls.accepts)
       
  1117         return cls
       
  1118     return plug_selector
       
  1119 
       
  1120 @unbind_method
       
  1121 def accepts_etype_compat(registered):
       
  1122     def plug_selector(cls, vreg):
       
  1123         cls = registered(cls, vreg)
       
  1124         if getattr(cls, 'accepts', None):
       
  1125             warn('use "specified_etype_implements("EntityType", IFace)" instead of using accepts',
       
  1126                  DeprecationWarning)
       
  1127             cls.__select__ &= specified_etype_implements(*cls.accepts)
       
  1128         return cls
       
  1129     return plug_selector
       
  1130 
       
  1131 @unbind_method
       
  1132 def condition_compat(registered):
       
  1133     def plug_selector(cls, vreg):
       
  1134         cls = registered(cls, vreg)
       
  1135         if getattr(cls, 'condition', None):
       
  1136             warn('use "use rql_condition(expression)" instead of using condition',
       
  1137                  DeprecationWarning)
       
  1138             cls.__select__ &= rql_condition(cls.condition)
       
  1139         return cls
       
  1140     return plug_selector
       
  1141 
       
  1142 @unbind_method
       
  1143 def has_relation_compat(registered):
       
  1144     def plug_selector(cls, vreg):
       
  1145         cls = registered(cls, vreg)
       
  1146         if getattr(cls, 'etype', None):
       
  1147             warn('use relation_possible selector instead of using etype_rtype',
       
  1148                  DeprecationWarning)
       
  1149             cls.__select__ &= relation_possible(cls.rtype, role(cls),
       
  1150                                                 getattr(cls, 'etype', None),
       
  1151                                                 action=getattr(cls, 'require_permission', 'read'))
       
  1152         return cls
       
  1153     return plug_selector