--- a/selectors.py Fri Feb 05 08:11:38 2010 +0100
+++ b/selectors.py Fri Feb 05 08:53:33 2010 +0100
@@ -136,7 +136,7 @@
return 0
-# abstract selectors ##########################################################
+# abstract selectors / mixin helpers ###########################################
class PartialSelectorMixIn(object):
"""convenience mix-in for selectors that will look into the containing
@@ -176,19 +176,28 @@
class EClassSelector(Selector):
- """abstract class for selectors working on the entity classes of the result
- set. Its __call__ method has the following behaviour:
+ """abstract class for selectors working on *entity class(es)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity's class
- * if 'entity' find in kwargs, return the score returned by the score_class
- method for this entity's class
- * elif row is specified, return the score returned by the score_class method
- called with the entity class found in the specified cell
- * else return the sum of score returned by the score_class method for each
- entity type found in the specified column, unless:
+ * elif `row` is specified, return score for the class of the entity
+ found in the specified cell, using column specified by `col` or 0
+
+ * else return the sum of scores for each entity class found in the column
+ specified specified by the `col` argument or in column 0 if not specified,
+ unless:
+
+ - `once_is_enough` is False (the default) and some entity class is scored
+ to 0, in which case 0 is returned
+
- `once_is_enough` is True, in which case the first non-zero score is
returned
- - `once_is_enough` is False, in which case if score_class return 0, 0 is
- returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
"""
def __init__(self, once_is_enough=False, accept_none=True):
self.once_is_enough = once_is_enough
@@ -230,22 +239,35 @@
class EntitySelector(EClassSelector):
- """abstract class for selectors working on the entity instances of the
- result set. Its __call__ method has the following behaviour:
+ """abstract class for selectors working on *entity instance(s)* specified
+ explicitly or found of the result set.
+
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, return score for this entity
- * if 'entity' find in kwargs, return the score returned by the score_entity
- method for this entity
- * if row is specified, return the score returned by the score_entity method
- called with the entity instance found in the specified cell
- * else return the sum of score returned by the score_entity method for each
- entity found in the specified column, unless:
+ * elif `row` is specified, return score for the entity found in the
+ specified cell, using column specified by `col` or 0
+
+ * else return the sum of scores for each entity found in the column
+ specified specified by the `col` argument or in column 0 if not specified,
+ unless:
+
+ - `once_is_enough` is False (the default) and some entity is scored
+ to 0, in which case 0 is returned
+
- `once_is_enough` is True, in which case the first non-zero score is
returned
- - `once_is_enough` is False, in which case if score_class return 0, 0 is
- returned
+
+ - `accept_none` is False and some cell in the column has a None value
+ (this may occurs with outer join)
- note: None values (resulting from some outer join in the query) are not
- considered.
+ .. 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.
"""
@lltrace
@@ -259,6 +281,8 @@
col = col or 0
for row, rowvalue in enumerate(rset.rows):
if rowvalue[col] is None: # outer join
+ if not self.accept_none:
+ return 0
continue
escore = self.score(req, rset, row, col)
if not escore and not self.once_is_enough:
@@ -283,178 +307,9 @@
raise NotImplementedError()
-# very basic selectors ########################################################
-
-@objectify_selector
-@lltrace
-def none_rset(cls, req, rset=None, **kwargs):
- """accept no result set (e.g. given rset is None)"""
- if rset is None:
- return 1
- return 0
-
-@objectify_selector
-@lltrace
-def any_rset(cls, req, rset=None, **kwargs):
- """accept result set, whatever the number of result it contains"""
- if rset is not None:
- return 1
- return 0
-
-@objectify_selector
-@lltrace
-def nonempty_rset(cls, req, rset=None, **kwargs):
- """accept any non empty result set"""
- if rset is not None and rset.rowcount:
- return 1
- return 0
-
-@objectify_selector
-@lltrace
-def empty_rset(cls, req, rset=None, **kwargs):
- """accept empty result set"""
- if rset is not None and rset.rowcount == 0:
- return 1
- return 0
-
-@objectify_selector
-@lltrace
-def one_line_rset(cls, req, rset=None, row=None, **kwargs):
- """if row is specified, accept result set with a single line of result,
- else accepts anyway
- """
- if rset is not None and (row is not None or rset.rowcount == 1):
- return 1
- return 0
-
-
-class multi_lines_rset(Selector):
- def __init__(self, nb=None):
- self.expected = nb
-
- def match_expected(self, num):
- if self.expected is None:
- return num > 1
- return num == self.expected
-
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- return rset is not None and self.match_expected(rset.rowcount)
-
-
-class multi_columns_rset(multi_lines_rset):
-
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- return rset and self.match_expected(len(rset.rows[0])) or 0 # *must not* return None
-
-
-@objectify_selector
-@lltrace
-def paginated_rset(cls, req, rset=None, **kwargs):
- """accept result set with more lines than the page size.
-
- Page size is searched in (respecting order):
- * a page_size argument
- * a page_size form parameters
- * the navigation.page-size property
- """
- page_size = kwargs.get('page_size')
- if page_size is None:
- page_size = req.form.get('page_size')
- if page_size is None:
- page_size = req.property_value('navigation.page-size')
- else:
- page_size = int(page_size)
- if rset is None or rset.rowcount <= page_size:
- return 0
- return 1
-
-@objectify_selector
-@lltrace
-def sorted_rset(cls, req, rset=None, **kwargs):
- """accept sorted result set"""
- rqlst = rset.syntax_tree()
- if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
- return 0
- return 2
-
-@objectify_selector
-@lltrace
-def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
- """accept result set where entities in the specified column (or 0) are all
- of the same type
- """
- if rset is None:
- return 0
- if len(rset.column_types(col)) != 1:
- return 0
- return 1
-
-
-class multi_etypes_rset(multi_lines_rset):
-
- @lltrace
- def __call__(self, cls, req, rset=None, col=0, **kwargs):
- return rset and self.match_expected(len(rset.column_types(col)))
-
-
-class non_final_entity(EClassSelector):
- """accept if entity type found in the result set is non final.
-
- See `EClassSelector` documentation for behaviour when row is not specified.
- """
- def score(self, cls, req, etype):
- if etype in BASE_TYPES:
- return 0
- return 1
-
-
-@objectify_selector
-@lltrace
-def authenticated_user(cls, req, *args, **kwargs):
- """accept if user is authenticated"""
- if req.cnx.anonymous_connection:
- return 0
- return 1
-
-def anonymous_user():
- return ~ authenticated_user()
-
-@objectify_selector
-@lltrace
-def primary_view(cls, req, rset=None, row=None, col=0, view=None, **kwargs):
- """accept if view given as named argument is a primary view, or if no view
- is given
- """
- if view is not None and not view.is_primary():
- return 0
- return 1
-
-@objectify_selector
-@lltrace
-def match_context_prop(cls, req, rset=None, row=None, col=0, context=None,
- **kwargs):
- """accept if:
- * no context given
- * context (`basestring`) is matching the context property value for the
- given cls
- """
- propval = req.property_value('%s.%s.context' % (cls.__registry__,
- cls.__regid__))
- if not propval:
- propval = cls.context
- if context is not None and propval and context != propval:
- return 0
- return 1
-
-
-class match_search_state(Selector):
- """accept if the current request search state is in one of the expected
- states given to the initializer
-
- :param expected: either 'normal' or 'linksearch' (eg searching for an
- object to create a relation with another)
+class ExpectedValueSelector(Selector):
+ """Take a list of expected values as initializer argument, check
+ _get_value method return one of these expected values.
"""
def __init__(self, *expected):
assert expected, self
@@ -465,212 +320,326 @@
','.join(sorted(str(s) for s in self.expected)))
@lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- try:
- if not req.search_state[0] in self.expected:
- return 0
- except AttributeError:
- return 1 # class doesn't care about search state, accept it
- return 1
+ def __call__(self, cls, req, **kwargs):
+ if self._get_value(cls, req, **kwargs) in self.expected:
+ return 1
+ return 0
+
+ def _get_value(self, cls, req, **kwargs):
+ raise NotImplementedError()
-class match_form_params(match_search_state):
- """accept if parameters specified as initializer arguments are specified
- in request's form parameters
+# bare selectors ##############################################################
- :param *expected: parameters (eg `basestring`) which are expected to be
- found in request's form parameters
+class match_kwargs(ExpectedValueSelector):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in the input context. When multiple parameters are
+ specified, all of them should be specified in the input context. Return a
+ score corresponding to the number of expected parameters.
"""
@lltrace
- def __call__(self, cls, req, *args, **kwargs):
- score = 0
- for param in self.expected:
- if not param in req.form:
- return 0
- score += 1
- return len(self.expected)
-
-
-class match_kwargs(match_search_state):
- """accept if parameters specified as initializer arguments are specified
- in named arguments given to the selector
-
- :param *expected: parameters (eg `basestring`) which are expected to be
- found in named arguments (kwargs)
- """
-
- @lltrace
- def __call__(self, cls, req, *args, **kwargs):
+ def __call__(self, cls, req, **kwargs):
for arg in self.expected:
if not arg in kwargs:
return 0
return len(self.expected)
-class match_user_groups(match_search_state):
- """accept if logged users is in at least one of the given groups. Returned
- score is the number of groups in which the user is.
-
- If the special 'owners' group is given:
- * if row is specified check the entity at the given row/col is owned by the
- logged user
- * if row is not specified check all entities in col are owned by the logged
- user
-
- :param *required_groups: name of groups (`basestring`) in which the logged
- user should be
- """
-
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- user = req.user
- if user is None:
- return int('guests' in self.expected)
- score = user.matching_groups(self.expected)
- if not score and 'owners' in self.expected and rset:
- if row is not None:
- if not user.owns(rset[row][col]):
- return 0
- score = 1
- else:
- score = all(user.owns(r[col]) for r in rset)
- return score
-
+class appobject_selectable(Selector):
+ """return 1 if another appobject is selectable using the same input context.
-class match_transition(match_search_state):
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
- try:
- # XXX check this is a transition that apply to the object?
- if not kwargs['transition'].name in self.expected:
- return 0
- except KeyError:
- return 0
- return 1
-
-
-class match_view(match_search_state):
- """accept if the current view is in one of the expected vid given to the
- initializer
+ Initializer arguments:
+ * `registry`, a registry name
+ * `regid`, an object identifier in this registry
"""
- @lltrace
- def __call__(self, cls, req, rset=None, row=None, col=0, view=None, **kwargs):
- if view is None or not view.__regid__ in self.expected:
- return 0
- return 1
-
-
-class appobject_selectable(Selector):
- """accept with another appobject is selectable using selector's input
- context.
-
- :param registry: a registry name (`basestring`)
- :param oid: an object identifier (`basestring`)
- """
- def __init__(self, registry, oid):
+ def __init__(self, registry, regid):
self.registry = registry
- self.oid = oid
+ self.regid = regid
def __call__(self, cls, req, **kwargs):
try:
- req.vreg[self.registry].select(self.oid, req, **kwargs)
+ req.vreg[self.registry].select(self.regid, req, **kwargs)
return 1
except NoSelectableObject:
return 0
-# not so basic selectors ######################################################
+# rset selectors ##############################################################
+
+@objectify_selector
+@lltrace
+def none_rset(cls, req, rset=None, **kwargs):
+ """Return 1 if the result set is None (eg usually not specified)."""
+ if rset is None:
+ return 1
+ return 0
+
+
+# XXX == ~ none_rset
+@objectify_selector
+@lltrace
+def any_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for any result set, whatever the number of rows in it, even 0."""
+ if rset is not None:
+ return 1
+ return 0
+
+
+@objectify_selector
+@lltrace
+def nonempty_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for result set containing one ore more rows."""
+ if rset is not None and rset.rowcount:
+ return 1
+ return 0
+
+
+# XXX == ~ nonempty_rset
+@objectify_selector
+@lltrace
+def empty_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for result set which doesn't contain any row."""
+ if rset is not None and rset.rowcount == 0:
+ return 1
+ return 0
+
+
+# XXX == multi_lines_rset(1)
+@objectify_selector
+@lltrace
+def one_line_rset(cls, req, rset=None, row=None, **kwargs):
+ """Return 1 if the result set is of size 1 or if a specific row in the
+ result set is specified ('row' argument).
+ """
+ if rset is not None and (row is not None or rset.rowcount == 1):
+ return 1
+ return 0
+
+
+class multi_lines_rset(Selector):
+ """If `nb`is specified, return 1 if the result set has exactly `nb` row of
+ result. Else (`nb` is None), return 1 if the result set contains *at least*
+ two rows.
+ """
+ def __init__(self, nb=None):
+ self.expected = nb
+
+ def match_expected(self, num):
+ if self.expected is None:
+ return num > 1
+ return num == self.expected
+
+ @lltrace
+ def __call__(self, cls, req, rset=None, **kwargs):
+ return rset is not None and self.match_expected(rset.rowcount)
+
+
+class multi_columns_rset(multi_lines_rset):
+ """If `nb`is specified, return 1 if the result set has exactly `nb` column
+ per row. Else (`nb` is None), return 1 if the result set contains *at least*
+ two columns per row. Return 0 for empty result set.
+ """
+
+ @lltrace
+ def __call__(self, cls, req, rset=None, **kwargs):
+ # 'or 0' since we *must not* return None
+ return rset and self.match_expected(len(rset.rows[0])) or 0
+
+
+@objectify_selector
+@lltrace
+def paginated_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for result set with more rows than a page size.
+
+ Page size is searched in (respecting order):
+ * a `page_size` argument
+ * a `page_size` form parameters
+ * the :ref:`navigation.page-size` property
+ """
+ if rset is None:
+ return 0
+ page_size = kwargs.get('page_size')
+ if page_size is None:
+ page_size = req.form.get('page_size')
+ if page_size is None:
+ page_size = req.property_value('navigation.page-size')
+ else:
+ page_size = int(page_size)
+ if rset.rowcount <= page_size:
+ return 0
+ return 1
+
+
+@objectify_selector
+@lltrace
+def sorted_rset(cls, req, rset=None, **kwargs):
+ """Return 1 for sorted result set (e.g. from an RQL query containing an
+ :ref:ORDERBY clause.
+ """
+ if rset is None:
+ return 0
+ rqlst = rset.syntax_tree()
+ if len(rqlst.children) > 1 or not rqlst.children[0].orderby:
+ return 0
+ return 2
+
+
+# XXX == multi_etypes_rset(1)
+@objectify_selector
+@lltrace
+def one_etype_rset(cls, req, rset=None, col=0, **kwargs):
+ """Return 1 if the result set contains entities which are all of the same
+ type in the column specified by the `col` argument of the input context, or
+ in column 0.
+ """
+ if rset is None:
+ return 0
+ if len(rset.column_types(col)) != 1:
+ return 0
+ return 1
+
+
+class multi_etypes_rset(multi_lines_rset):
+ """If `nb` is specified, return 1 if the result set contains `nb` different
+ types of entities in the column specified by the `col` argument of the input
+ context, or in column 0. If `nb` is None, return 1 if the result set contains
+ *at least* two different types of entities.
+ """
+
+ @lltrace
+ def __call__(self, cls, req, rset=None, col=0, **kwargs):
+ # 'or 0' since we *must not* return None
+ return rset and self.match_expected(len(rset.column_types(col))) or 0
+
+
+# entity selectors #############################################################
+
+class non_final_entity(EClassSelector):
+ """Return 1 for entity of a non final entity type(s). Remember, "final"
+ entity types are String, Int, etc... This is equivalent to
+ `implements('Any')` but more optimized.
+
+ See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
+ class lookup / score rules according to the input context.
+ """
+ def score(self, cls, req, etype):
+ if etype in BASE_TYPES:
+ return 0
+ return 1
+
class implements(ImplementsMixIn, EClassSelector):
- """accept if entity classes found in the result set implements at least one
- of the interfaces given as argument. Returned score is the number of
- implemented interfaces.
-
- See `EClassSelector` documentation for behaviour when row is not specified.
+ """Return non-zero score for entity that are of the given type(s) or
+ implements at least one of the given interface(s). If multiple arguments are
+ given, matching one of them is enough.
- :param *expected_ifaces: expected interfaces. An interface may be a class
- or an entity type (e.g. `basestring`) in which case
- the associated class will be searched in the
- registry (at selection time)
+ Entity types should be given as string, the corresponding class will be
+ fetched from the entity types registry at selection time.
- note: when interface is an entity class, the score will reflect class
- proximity so the most specific object'll be selected
+ See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
+ class lookup / score rules according to the input context.
+
+ .. note:: when interface is an entity class, the score will reflect class
+ proximity so the most specific object will be selected.
"""
def score_class(self, eclass, req):
return self.score_interfaces(req, eclass, eclass)
-class specified_etype_implements(implements):
- """accept if entity class specified using an 'etype' parameters in name
- argument or request form implements at least one of the interfaces given as
- argument. Returned score is the number of implemented interfaces.
+class score_entity(EntitySelector):
+ """Return score according to an arbitrary function given as argument which
+ will be called with input content entity as argument.
+
+ This is a very useful selector that will usually interest you since it
+ allows a lot of things without having to write a specific selector.
+
+ See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
+ lookup / score rules according to the input context.
+ """
+ def __init__(self, scorefunc, once_is_enough=False):
+ super(score_entity, self).__init__(once_is_enough)
+ def intscore(*args, **kwargs):
+ score = scorefunc(*args, **kwargs)
+ if not score:
+ return 0
+ if isinstance(score, (int, long)):
+ return score
+ return 1
+ self.score_entity = intscore
+
+
+class relation_possible(EntitySelector):
+ """Return 1 for entity that supports the relation, provided that the
+ request's user may do some `action` on it (see below).
+
+ The relation is specified by the following initializer arguments:
- :param *expected_ifaces: expected interfaces. An interface may be a class
- or an entity type (e.g. `basestring`) in which case
- the associated class will be searched in the
- registry (at selection time)
+ * `rtype`, the name of the relation
+
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'
+
+ * `target_etype`, optional name of an entity type that should be supported
+ at the other end of the relation
+
+ * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete',
+ default to 'read') which must be granted to the user, else a 0 score will
+ be returned
- note: when interface is an entity class, the score will reflect class
- proximity so the most specific object'll be selected
+ * `strict`, boolean (default to False) telling what to do when the user has
+ not globally the permission for the action (eg the action is not granted
+ to one of the user's groups)
+
+ - when strict is False, if there are some local role defined for this
+ action (e.g. using rql expressions), then the permission will be
+ considered as granted
+
+ - when strict is True, then the permission will be actually checked for
+ each entity
+
+ Setting `strict` to True impacts performance for large result set since
+ you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour
+ while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s
+ one. See those classes documentation for entity lookup / score rules
+ according to the input context.
"""
- @lltrace
- def __call__(self, cls, req, *args, **kwargs):
- try:
- etype = kwargs['etype']
- except KeyError:
- try:
- etype = req.form['etype']
- except KeyError:
- return 0
- else:
- # only check this is a known type if etype comes from req.form,
- # else we want the error to propagate
- try:
- etype = req.vreg.case_insensitive_etypes[etype.lower()]
- req.form['etype'] = etype
- except KeyError:
- return 0
- score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
- if score:
- eschema = req.vreg.schema.eschema(etype)
- if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
- return score
- return 0
-
-
-class relation_possible(EClassSelector):
- """accept if entity class found in the result set support the relation.
-
- See `EClassSelector` documentation for behaviour when row is not specified.
-
- :param rtype: a relation type (`basestring`)
- :param role: the role of the result set entity in the relation. 'subject' or
- 'object', default to 'subject'.
- :param target_type: if specified, check the relation's end may be of this
- target type (`basestring`)
- :param action: a relation schema action (one of 'read', 'add', 'delete')
- which must be granted to the logged user, else a 0 score will
- be returned
- """
def __init__(self, rtype, role='subject', target_etype=None,
- action='read', once_is_enough=False):
- super(relation_possible, self).__init__(once_is_enough)
+ action='read', strict=False, **kwargs):
+ super(relation_possible, self).__init__(**kwargs)
self.rtype = rtype
self.role = role
self.target_etype = target_etype
self.action = action
+ self.strict = strict
- def score_class(self, eclass, req):
+ # hack hack hack
+ def __call__(self, cls, req, **kwargs):
+ if self.strict:
+ return EntitySelector.__call__(self, cls, req, **kwargs)
+ return EClassSelector.__call__(self, cls, req, **kwargs)
+
+ def score(self, *args):
+ if self.strict:
+ return EntitySelector.score(self, *args)
+ return EClassSelector.score(self, *args)
+
+ def _get_rschema(self, eclass):
eschema = eclass.e_schema
try:
if self.role == 'object':
- rschema = eschema.objrels[self.rtype]
+ return eschema.objrels[self.rtype]
else:
- rschema = eschema.subjrels[self.rtype]
+ return eschema.subjrels[self.rtype]
except KeyError:
- return 0
+ return None
+
+ def score_class(self, eclass, req):
+ rschema = self._get_rschema(eclass)
+ if rschema is None:
+ return 0 # relation not supported
+ eschema = eclass.e_schema
if self.target_etype is not None:
try:
rdef = rschema.role_rdef(eschema, self.target_etype, self.role)
@@ -682,55 +651,10 @@
return rschema.may_have_permission(self.action, req, eschema, self.role)
return 1
-
-class partial_relation_possible(PartialSelectorMixIn, relation_possible):
- """partial version of the relation_possible selector
-
- The selector will look for class attributes to find its missing
- information. The list of attributes required on the class
- for this selector are:
-
- - `rtype`: same as `rtype` parameter of the `relation_possible` selector
-
- - `role`: this attribute will be passed to the `cubicweb.role` function
- to determine the role of class in the relation
-
- - `etype` (optional): the entity type on the other side of the relation
-
- :param action: a relation schema action (one of 'read', 'add', 'delete')
- which must be granted to the logged user, else a 0 score will
- be returned
- """
- def __init__(self, action='read', once_is_enough=False):
- super(partial_relation_possible, self).__init__(None, None, None,
- action, once_is_enough)
-
- def complete(self, cls):
- self.rtype = cls.rtype
- self.role = role(cls)
- self.target_etype = getattr(cls, 'etype', None)
-
-
-class may_add_relation(EntitySelector):
- """accept if the relation can be added to an entity found in the result set
- by the logged user.
-
- See `EntitySelector` documentation for behaviour when row is not specified.
-
- :param rtype: a relation type (`basestring`)
- :param role: the role of the result set entity in the relation. 'subject' or
- 'object', default to 'subject'.
- """
-
- def __init__(self, rtype, role='subject', target_etype=None,
- once_is_enough=False):
- super(may_add_relation, self).__init__(once_is_enough)
- self.rtype = rtype
- self.role = role
- self.target_etype = target_etype
-
def score_entity(self, entity):
- rschema = entity._cw.vreg.schema.rschema(self.rtype)
+ rschema = self._get_rschema(entity)
+ if rschema is None:
+ return 0 # relation not supported
if self.target_etype is not None:
rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role)
if self.role == 'subject':
@@ -741,47 +665,50 @@
return 1
-class partial_may_add_relation(PartialSelectorMixIn, may_add_relation):
- """partial version of the may_add_relation selector
-
- The selector will look for class attributes to find its missing
- information. The list of attributes required on the class
- for this selector are:
-
- - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+class partial_relation_possible(PartialSelectorMixIn, relation_possible):
+ """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for
+ attributes of the selected class to get information which is otherwise
+ expected by the initializer, except for `action` and `strict` which are kept
+ as initializer arguments.
- - `role`: this attribute will be passed to the `cubicweb.role` function
- to determine the role of class in the relation.
-
- :param action: a relation schema action (one of 'read', 'add', 'delete')
- which must be granted to the logged user, else a 0 score will
- be returned
+ This is useful to predefine selector of an abstract class designed to be
+ customized.
"""
- def __init__(self, once_is_enough=False):
- super(partial_may_add_relation, self).__init__(None, once_is_enough=once_is_enough)
+ def __init__(self, action='read', **kwargs):
+ super(partial_relation_possible, self).__init__(None, None, None,
+ action, **kwargs)
def complete(self, cls):
self.rtype = cls.rtype
self.role = role(cls)
self.target_etype = getattr(cls, 'etype', None)
+ if self.target_etype is not None:
+ warn('[3.6] please rename etype to target_etype on %s' % cls,
+ DeprecationWarning)
+ else:
+ self.target_etype = getattr(cls, 'target_etype', None)
class has_related_entities(EntitySelector):
- """accept if entity found in the result set has some linked entities using
- the specified relation (optionaly filtered according to the specified target
- type). Checks first if the relation is possible.
+ """Return 1 if entity support the specified relation and has some linked
+ entities by this relation , optionaly filtered according to the specified
+ target type.
- See `EntitySelector` documentation for behaviour when row is not specified.
+ The relation is specified by the following initializer arguments:
+
+ * `rtype`, the name of the relation
- :param rtype: a relation type (`basestring`)
- :param role: the role of the result set entity in the relation. 'subject' or
- 'object', default to 'subject'.
- :param target_type: if specified, check the relation's end may be of this
- target type (`basestring`)
+ * `role`, the role of the entity in the relation, either 'subject' or
+ 'object', default to 'subject'.
+
+ * `target_etype`, optional name of an entity type that should be found
+ at the other end of the relation
+
+ See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
+ lookup / score rules according to the input context.
"""
- def __init__(self, rtype, role='subject', target_etype=None,
- once_is_enough=False):
- super(has_related_entities, self).__init__(once_is_enough)
+ def __init__(self, rtype, role='subject', target_etype=None, **kwargs):
+ super(has_related_entities, self).__init__(**kwargs)
self.rtype = rtype
self.role = role
self.target_etype = target_etype
@@ -797,52 +724,52 @@
class partial_has_related_entities(PartialSelectorMixIn, has_related_entities):
- """partial version of the has_related_entities selector
-
- The selector will look for class attributes to find its missing
- information. The list of attributes required on the class
- for this selector are:
-
- - `rtype`: same as `rtype` parameter of the `relation_possible` selector
+ """Same as :class:~`cubicweb.selectors.has_related_entity`, but will look
+ for attributes of the selected class to get information which is otherwise
+ expected by the initializer.
- - `role`: this attribute will be passed to the `cubicweb.role` function
- to determine the role of class in the relation.
-
- - `etype` (optional): the entity type on the other side of the relation
+ This is useful to predefine selector of an abstract class designed to be
+ customized.
+ """
+ def __init__(self, **kwargs):
+ super(partial_has_related_entities, self).__init__(None, None, None,
+ **kwargs)
- :param action: a relation schema action (one of 'read', 'add', 'delete')
- which must be granted to the logged user, else a 0 score will
- be returned
- """
- def __init__(self, once_is_enough=False):
- super(partial_has_related_entities, self).__init__(None, None,
- None, once_is_enough)
def complete(self, cls):
self.rtype = cls.rtype
self.role = role(cls)
self.target_etype = getattr(cls, 'etype', None)
+ if self.target_etype is not None:
+ warn('[3.6] please rename etype to target_etype on %s' % cls,
+ DeprecationWarning)
+ else:
+ self.target_etype = getattr(cls, 'target_etype', None)
class has_permission(EntitySelector):
- """accept if user has the permission to do the requested action on a result
- set entity.
+ """Return non-zero score if request's user has the permission to do the
+ requested action on the entity. `action` is an entity schema action (eg one
+ of 'read', 'add', 'delete', 'update').
- * if row is specified, return 1 if user has the permission on the entity
- instance found in the specified cell
- * else return a positive score if user has the permission for every entity
- in the found in the specified column
+ Here are entity lookup / scoring rules:
+
+ * if `entity` is specified, check permission is granted for this entity
- note: None values (resulting from some outer join in the query) are not
- considered.
+ * elif `row` is specified, check permission is granted for the entity found
+ in the specified cell
- :param action: an entity schema action (eg 'read'/'add'/'delete'/'update')
+ * else check permission is granted for each entity found in the column
+ specified specified by the `col` argument or in column 0
"""
- def __init__(self, action, once_is_enough=False):
- super(has_permission, self).__init__(once_is_enough)
+ def __init__(self, action):
self.action = action
+ # don't use EntitySelector.__call__ but this optimized implementation to
+ # avoid considering each entity when it's not necessary
@lltrace
def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ if kwargs.get('entity'):
+ return self.score_entity(kwargs['entity'])
if rset is None:
return 0
user = req.user
@@ -882,32 +809,47 @@
class has_add_permission(EClassSelector):
- """accept if logged user has the add permission on entity class found in the
- result set, and class is not a strict subobject.
+ """Return 1 if request's user has the add permission on entity type
+ specified in the `etype` initializer argument, or according to entity found
+ in the input content if not specified.
- See `EClassSelector` documentation for behaviour when row is not specified.
+ It also check that then entity type is not a strict subobject (e.g. may only
+ be used as a composed of another entity).
+
+ See :class:`~cubicweb.selectors.EClassSelector` documentation for entity
+ class lookup / score rules according to the input context when `etype` is
+ not specified.
"""
- def score(self, cls, req, etype):
- eschema = req.vreg.schema.eschema(etype)
- if not (eschema.final or eschema.is_subobject(strict=True)) \
- and eschema.has_perm(req, 'add'):
- return 1
- return 0
+ def __init__(self, etype=None, **kwargs):
+ super(has_add_permission, self).__init__(**kwargs)
+ self.etype = etype
+
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ if self.etype is None:
+ return super(has_add_permission, self).__call__(cls, req, **kwargs)
+ return self.score(cls, req, self.etype)
+
+ def score_class(self, eclass, req):
+ eschema = eclass.e_schema
+ if eschema.final or eschema.is_subobject(strict=True) \
+ or not eschema.has_perm(req, 'add'):
+ return 0
+ return 1
class rql_condition(EntitySelector):
- """accept if an arbitrary rql return some results for an eid found in the
- result set. Returned score is the number of items returned by the rql
+ """Return non-zero score if arbitrary rql specified in `expression`
+ initializer argument return some results for entity found in the input
+ context. Returned score is the number of items returned by the rql
condition.
- See `EntitySelector` documentation for behaviour when row is not specified.
+ `expression` is expected to be a string containing an rql expression, which
+ must use 'X' variable to represent the context entity and may use 'U' to
+ represent the request's user.
- :param expression: basestring containing an rql expression, which should use
- X variable to represent the context entity and may use U
- to represent the logged user
-
- return the sum of the number of items returned by the rql condition as score
- or 0 at the first entity scoring to zero.
+ See :class:`~cubicweb.selectors.EntitySelector` documentation for entity
+ lookup / score rules according to the input context.
"""
def __init__(self, expression, once_is_enough=False):
super(rql_condition, self).__init__(once_is_enough)
@@ -917,6 +859,9 @@
rql = 'Any X WHERE X eid %%(x)s, %s' % expression
self.rql = rql
+ def __repr__(self):
+ return u'<rql_condition "%s" at %x>' % (self.rql, id(self))
+
def score(self, req, rset, row, col):
try:
return len(req.execute(self.rql, {'x': rset[row][col],
@@ -924,29 +869,210 @@
except Unauthorized:
return 0
- def __repr__(self):
- return u'<rql_condition "%s" at %x>' % (self.rql, id(self))
+# logged user selectors ########################################################
+
+@objectify_selector
+@lltrace
+def authenticated_user(cls, req, **kwargs):
+ """Return 1 if the user is authenticated (e.g. not the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ if req.cnx.anonymous_connection:
+ return 0
+ return 1
+
+
+# XXX == ~ authenticated_user()
+def anonymous_user():
+ """Return 1 if the user is not authenticated (e.g. is the anonymous user).
+
+ May only be used on the web side, not on the data repository side.
+ """
+ return ~ authenticated_user()
+
+
+class match_user_groups(ExpectedValueSelector):
+ """Return a non-zero score if request's user is in at least one of the
+ groups given as initializer argument. Returned score is the number of groups
+ in which the user is.
+
+ If the special 'owners' group is given and `rset` is specified in the input
+ context:
+
+ * if `row` is specified check the entity at the given `row`/`col` (default
+ to 0) is owned by the user
+
+ * else check all entities in `col` (default to 0) are owned by the user
+ """
+
+ @lltrace
+ def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
+ user = req.user
+ if user is None:
+ return int('guests' in self.expected)
+ score = user.matching_groups(self.expected)
+ if not score and 'owners' in self.expected and rset:
+ if row is not None:
+ if not user.owns(rset[row][col]):
+ return 0
+ score = 1
+ else:
+ score = all(user.owns(r[col]) for r in rset)
+ return score
+
+
+# Web request selectors ########################################################
+
+@objectify_selector
+@lltrace
+def primary_view(cls, req, view=None, **kwargs):
+ """Return 1 if:
+
+ * *no view is specified* in the input context
+
+ * a view is specified and its `.is_primary()` method return True
+
+ This selector is usually used by contextual components that only want to
+ appears for the primary view of an entity.
+ """
+ if view is not None and not view.is_primary():
+ return 0
+ return 1
+
+
+class match_view(ExpectedValueSelector):
+ """Return 1 if a view is specified an as its registry id is in one of the
+ expected view id given to the initializer.
+ """
+ @lltrace
+ def __call__(self, cls, req, view=None, **kwargs):
+ if view is None or not view.__regid__ in self.expected:
+ return 0
+ return 1
-class score_entity(EntitySelector):
- """accept if some arbitrary function return a positive score for an entity
- found in the result set.
+@objectify_selector
+@lltrace
+def match_context_prop(cls, req, context=None, **kwargs):
+ """Return 1 if:
+
+ * no `context` is specified in input context (take care to confusion, here
+ `context` refers to a string given as an argument to the input context...)
+
+ * specified `context` is matching the context property value for the
+ appobject using this selector
+
+ * the appobject's context property value is None
+
+ This selector is usually used by contextual components that want to appears
+ in a configurable place.
+ """
+ if context is None:
+ return 0
+ propval = req.property_value('%s.%s.context' % (cls.__registry__,
+ cls.__regid__))
+ if not propval:
+ propval = cls.context
+ if propval and context != propval:
+ return 0
+ return 1
+
- See `EntitySelector` documentation for behaviour when row is not specified.
+class match_search_state(ExpectedValueSelector):
+ """Return 1 if the current request search state is in one of the expected
+ states given to the initializer.
+
+ Known search states are either 'normal' or 'linksearch' (eg searching for an
+ object to create a relation with another).
+
+ This selector is usually used by action that want to appears or not according
+ to the ui search state.
+ """
- :param scorefunc: callable expected to take an entity as argument and to
- return a score >= 0
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ try:
+ if not req.search_state[0] in self.expected:
+ return 0
+ except AttributeError:
+ return 1 # class doesn't care about search state, accept it
+ return 1
+
+
+class match_form_params(ExpectedValueSelector):
+ """Return non-zero score if parameter names specified as initializer
+ arguments are specified in request's form parameters. When multiple
+ parameters are specified, all of them should be found in req.form. Return a
+ score corresponding to the number of expected parameters.
"""
- def __init__(self, scorefunc, once_is_enough=False):
- super(score_entity, self).__init__(once_is_enough)
- def intscore(*args, **kwargs):
- score = scorefunc(*args, **kwargs)
- if not score:
+
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ for param in self.expected:
+ if not param in req.form:
return 0
- if isinstance(score, (int, long)):
+ return len(self.expected)
+
+
+class specified_etype_implements(implements):
+ """Return non-zero score if the entity type specified by an 'etype' key
+ searched in (by priority) input context kwargs and request form parameters
+ match a known entity type (case insensitivly), and it's associated entity
+ class is of one of the type(s) given to the initializer or implements at
+ least one of the given interfaces. If multiple arguments are given, matching
+ one of them is enough.
+
+ Entity types should be given as string, the corresponding class will be
+ fetched from the entity types registry at selection time.
+
+ .. note:: when interface is an entity class, the score will reflect class
+ proximity so the most specific object will be selected.
+
+ This selector is usually used by views holding entity creation forms (since
+ we've no result set to work on).
+ """
+
+ @lltrace
+ def __call__(self, cls, req, **kwargs):
+ try:
+ etype = kwargs['etype']
+ except KeyError:
+ try:
+ etype = req.form['etype']
+ except KeyError:
+ return 0
+ else:
+ # only check this is a known type if etype comes from req.form,
+ # else we want the error to propagate
+ try:
+ etype = req.vreg.case_insensitive_etypes[etype.lower()]
+ req.form['etype'] = etype
+ except KeyError:
+ return 0
+ score = self.score_class(req.vreg['etypes'].etype_class(etype), req)
+ if score:
+ eschema = req.vreg.schema.eschema(etype)
+ if eschema.has_local_role('add') or eschema.has_perm(req, 'add'):
return score
+ return 0
+
+
+# Other selectors ##############################################################
+
+
+class match_transition(ExpectedValueSelector):
+ """Return 1 if a `transition` argument is found in the input context which
+ has a `.name` attribute matching one of the expected names given to the
+ initializer.
+ """
+ @lltrace
+ def __call__(self, cls, req, transition=None, **kwargs):
+ # XXX check this is a transition that apply to the object?
+ if transition is not None and getattr(transition, 'name', None) in self.expected:
return 1
- self.score_entity = intscore
+ return 0
+
## deprecated stuff ############################################################