diff -r a35c76ffed92 -r c57c8176b8c2 selectors.py --- 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'' % (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'' % (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 ############################################################