281 |
305 |
282 def score_entity(self, entity): |
306 def score_entity(self, entity): |
283 raise NotImplementedError() |
307 raise NotImplementedError() |
284 |
308 |
285 |
309 |
286 # very basic selectors ######################################################## |
310 class ExpectedValueSelector(Selector): |
|
311 """Take a list of expected values as initializer argument, check |
|
312 _get_value method return one of these expected values. |
|
313 """ |
|
314 def __init__(self, *expected): |
|
315 assert expected, self |
|
316 self.expected = frozenset(expected) |
|
317 |
|
318 def __str__(self): |
|
319 return '%s(%s)' % (self.__class__.__name__, |
|
320 ','.join(sorted(str(s) for s in self.expected))) |
|
321 |
|
322 @lltrace |
|
323 def __call__(self, cls, req, **kwargs): |
|
324 if self._get_value(cls, req, **kwargs) in self.expected: |
|
325 return 1 |
|
326 return 0 |
|
327 |
|
328 def _get_value(self, cls, req, **kwargs): |
|
329 raise NotImplementedError() |
|
330 |
|
331 |
|
332 # bare selectors ############################################################## |
|
333 |
|
334 class match_kwargs(ExpectedValueSelector): |
|
335 """Return non-zero score if parameter names specified as initializer |
|
336 arguments are specified in the input context. When multiple parameters are |
|
337 specified, all of them should be specified in the input context. Return a |
|
338 score corresponding to the number of expected parameters. |
|
339 """ |
|
340 |
|
341 @lltrace |
|
342 def __call__(self, cls, req, **kwargs): |
|
343 for arg in self.expected: |
|
344 if not arg in kwargs: |
|
345 return 0 |
|
346 return len(self.expected) |
|
347 |
|
348 |
|
349 class appobject_selectable(Selector): |
|
350 """return 1 if another appobject is selectable using the same input context. |
|
351 |
|
352 Initializer arguments: |
|
353 * `registry`, a registry name |
|
354 * `regid`, an object identifier in this registry |
|
355 """ |
|
356 def __init__(self, registry, regid): |
|
357 self.registry = registry |
|
358 self.regid = regid |
|
359 |
|
360 def __call__(self, cls, req, **kwargs): |
|
361 try: |
|
362 req.vreg[self.registry].select(self.regid, req, **kwargs) |
|
363 return 1 |
|
364 except NoSelectableObject: |
|
365 return 0 |
|
366 |
|
367 |
|
368 # rset selectors ############################################################## |
287 |
369 |
288 @objectify_selector |
370 @objectify_selector |
289 @lltrace |
371 @lltrace |
290 def none_rset(cls, req, rset=None, **kwargs): |
372 def none_rset(cls, req, rset=None, **kwargs): |
291 """accept no result set (e.g. given rset is None)""" |
373 """Return 1 if the result set is None (eg usually not specified).""" |
292 if rset is None: |
374 if rset is None: |
293 return 1 |
375 return 1 |
294 return 0 |
376 return 0 |
295 |
377 |
|
378 |
|
379 # XXX == ~ none_rset |
296 @objectify_selector |
380 @objectify_selector |
297 @lltrace |
381 @lltrace |
298 def any_rset(cls, req, rset=None, **kwargs): |
382 def any_rset(cls, req, rset=None, **kwargs): |
299 """accept result set, whatever the number of result it contains""" |
383 """Return 1 for any result set, whatever the number of rows in it, even 0.""" |
300 if rset is not None: |
384 if rset is not None: |
301 return 1 |
385 return 1 |
302 return 0 |
386 return 0 |
303 |
387 |
|
388 |
304 @objectify_selector |
389 @objectify_selector |
305 @lltrace |
390 @lltrace |
306 def nonempty_rset(cls, req, rset=None, **kwargs): |
391 def nonempty_rset(cls, req, rset=None, **kwargs): |
307 """accept any non empty result set""" |
392 """Return 1 for result set containing one ore more rows.""" |
308 if rset is not None and rset.rowcount: |
393 if rset is not None and rset.rowcount: |
309 return 1 |
394 return 1 |
310 return 0 |
395 return 0 |
311 |
396 |
|
397 |
|
398 # XXX == ~ nonempty_rset |
312 @objectify_selector |
399 @objectify_selector |
313 @lltrace |
400 @lltrace |
314 def empty_rset(cls, req, rset=None, **kwargs): |
401 def empty_rset(cls, req, rset=None, **kwargs): |
315 """accept empty result set""" |
402 """Return 1 for result set which doesn't contain any row.""" |
316 if rset is not None and rset.rowcount == 0: |
403 if rset is not None and rset.rowcount == 0: |
317 return 1 |
404 return 1 |
318 return 0 |
405 return 0 |
319 |
406 |
|
407 |
|
408 # XXX == multi_lines_rset(1) |
320 @objectify_selector |
409 @objectify_selector |
321 @lltrace |
410 @lltrace |
322 def one_line_rset(cls, req, rset=None, row=None, **kwargs): |
411 def one_line_rset(cls, req, rset=None, row=None, **kwargs): |
323 """if row is specified, accept result set with a single line of result, |
412 """Return 1 if the result set is of size 1 or if a specific row in the |
324 else accepts anyway |
413 result set is specified ('row' argument). |
325 """ |
414 """ |
326 if rset is not None and (row is not None or rset.rowcount == 1): |
415 if rset is not None and (row is not None or rset.rowcount == 1): |
327 return 1 |
416 return 1 |
328 return 0 |
417 return 0 |
329 |
418 |
330 |
419 |
331 class multi_lines_rset(Selector): |
420 class multi_lines_rset(Selector): |
|
421 """If `nb`is specified, return 1 if the result set has exactly `nb` row of |
|
422 result. Else (`nb` is None), return 1 if the result set contains *at least* |
|
423 two rows. |
|
424 """ |
332 def __init__(self, nb=None): |
425 def __init__(self, nb=None): |
333 self.expected = nb |
426 self.expected = nb |
334 |
427 |
335 def match_expected(self, num): |
428 def match_expected(self, num): |
336 if self.expected is None: |
429 if self.expected is None: |
337 return num > 1 |
430 return num > 1 |
338 return num == self.expected |
431 return num == self.expected |
339 |
432 |
340 @lltrace |
433 @lltrace |
341 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
434 def __call__(self, cls, req, rset=None, **kwargs): |
342 return rset is not None and self.match_expected(rset.rowcount) |
435 return rset is not None and self.match_expected(rset.rowcount) |
343 |
436 |
344 |
437 |
345 class multi_columns_rset(multi_lines_rset): |
438 class multi_columns_rset(multi_lines_rset): |
346 |
439 """If `nb`is specified, return 1 if the result set has exactly `nb` column |
347 @lltrace |
440 per row. Else (`nb` is None), return 1 if the result set contains *at least* |
348 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
441 two columns per row. Return 0 for empty result set. |
349 return rset and self.match_expected(len(rset.rows[0])) or 0 # *must not* return None |
442 """ |
|
443 |
|
444 @lltrace |
|
445 def __call__(self, cls, req, rset=None, **kwargs): |
|
446 # 'or 0' since we *must not* return None |
|
447 return rset and self.match_expected(len(rset.rows[0])) or 0 |
350 |
448 |
351 |
449 |
352 @objectify_selector |
450 @objectify_selector |
353 @lltrace |
451 @lltrace |
354 def paginated_rset(cls, req, rset=None, **kwargs): |
452 def paginated_rset(cls, req, rset=None, **kwargs): |
355 """accept result set with more lines than the page size. |
453 """Return 1 for result set with more rows than a page size. |
356 |
454 |
357 Page size is searched in (respecting order): |
455 Page size is searched in (respecting order): |
358 * a page_size argument |
456 * a `page_size` argument |
359 * a page_size form parameters |
457 * a `page_size` form parameters |
360 * the navigation.page-size property |
458 * the :ref:`navigation.page-size` property |
361 """ |
459 """ |
|
460 if rset is None: |
|
461 return 0 |
362 page_size = kwargs.get('page_size') |
462 page_size = kwargs.get('page_size') |
363 if page_size is None: |
463 if page_size is None: |
364 page_size = req.form.get('page_size') |
464 page_size = req.form.get('page_size') |
365 if page_size is None: |
465 if page_size is None: |
366 page_size = req.property_value('navigation.page-size') |
466 page_size = req.property_value('navigation.page-size') |
367 else: |
467 else: |
368 page_size = int(page_size) |
468 page_size = int(page_size) |
369 if rset is None or rset.rowcount <= page_size: |
469 if rset.rowcount <= page_size: |
370 return 0 |
470 return 0 |
371 return 1 |
471 return 1 |
|
472 |
372 |
473 |
373 @objectify_selector |
474 @objectify_selector |
374 @lltrace |
475 @lltrace |
375 def sorted_rset(cls, req, rset=None, **kwargs): |
476 def sorted_rset(cls, req, rset=None, **kwargs): |
376 """accept sorted result set""" |
477 """Return 1 for sorted result set (e.g. from an RQL query containing an |
|
478 :ref:ORDERBY clause. |
|
479 """ |
|
480 if rset is None: |
|
481 return 0 |
377 rqlst = rset.syntax_tree() |
482 rqlst = rset.syntax_tree() |
378 if len(rqlst.children) > 1 or not rqlst.children[0].orderby: |
483 if len(rqlst.children) > 1 or not rqlst.children[0].orderby: |
379 return 0 |
484 return 0 |
380 return 2 |
485 return 2 |
381 |
486 |
|
487 |
|
488 # XXX == multi_etypes_rset(1) |
382 @objectify_selector |
489 @objectify_selector |
383 @lltrace |
490 @lltrace |
384 def one_etype_rset(cls, req, rset=None, col=0, **kwargs): |
491 def one_etype_rset(cls, req, rset=None, col=0, **kwargs): |
385 """accept result set where entities in the specified column (or 0) are all |
492 """Return 1 if the result set contains entities which are all of the same |
386 of the same type |
493 type in the column specified by the `col` argument of the input context, or |
|
494 in column 0. |
387 """ |
495 """ |
388 if rset is None: |
496 if rset is None: |
389 return 0 |
497 return 0 |
390 if len(rset.column_types(col)) != 1: |
498 if len(rset.column_types(col)) != 1: |
391 return 0 |
499 return 0 |
392 return 1 |
500 return 1 |
393 |
501 |
394 |
502 |
395 class multi_etypes_rset(multi_lines_rset): |
503 class multi_etypes_rset(multi_lines_rset): |
|
504 """If `nb` is specified, return 1 if the result set contains `nb` different |
|
505 types of entities in the column specified by the `col` argument of the input |
|
506 context, or in column 0. If `nb` is None, return 1 if the result set contains |
|
507 *at least* two different types of entities. |
|
508 """ |
396 |
509 |
397 @lltrace |
510 @lltrace |
398 def __call__(self, cls, req, rset=None, col=0, **kwargs): |
511 def __call__(self, cls, req, rset=None, col=0, **kwargs): |
399 return rset and self.match_expected(len(rset.column_types(col))) |
512 # 'or 0' since we *must not* return None |
400 |
513 return rset and self.match_expected(len(rset.column_types(col))) or 0 |
|
514 |
|
515 |
|
516 # entity selectors ############################################################# |
401 |
517 |
402 class non_final_entity(EClassSelector): |
518 class non_final_entity(EClassSelector): |
403 """accept if entity type found in the result set is non final. |
519 """Return 1 for entity of a non final entity type(s). Remember, "final" |
404 |
520 entity types are String, Int, etc... This is equivalent to |
405 See `EClassSelector` documentation for behaviour when row is not specified. |
521 `implements('Any')` but more optimized. |
|
522 |
|
523 See :class:`~cubicweb.selectors.EClassSelector` documentation for entity |
|
524 class lookup / score rules according to the input context. |
406 """ |
525 """ |
407 def score(self, cls, req, etype): |
526 def score(self, cls, req, etype): |
408 if etype in BASE_TYPES: |
527 if etype in BASE_TYPES: |
409 return 0 |
528 return 0 |
410 return 1 |
529 return 1 |
411 |
530 |
412 |
531 |
413 @objectify_selector |
|
414 @lltrace |
|
415 def authenticated_user(cls, req, *args, **kwargs): |
|
416 """accept if user is authenticated""" |
|
417 if req.cnx.anonymous_connection: |
|
418 return 0 |
|
419 return 1 |
|
420 |
|
421 def anonymous_user(): |
|
422 return ~ authenticated_user() |
|
423 |
|
424 @objectify_selector |
|
425 @lltrace |
|
426 def primary_view(cls, req, rset=None, row=None, col=0, view=None, **kwargs): |
|
427 """accept if view given as named argument is a primary view, or if no view |
|
428 is given |
|
429 """ |
|
430 if view is not None and not view.is_primary(): |
|
431 return 0 |
|
432 return 1 |
|
433 |
|
434 @objectify_selector |
|
435 @lltrace |
|
436 def match_context_prop(cls, req, rset=None, row=None, col=0, context=None, |
|
437 **kwargs): |
|
438 """accept if: |
|
439 * no context given |
|
440 * context (`basestring`) is matching the context property value for the |
|
441 given cls |
|
442 """ |
|
443 propval = req.property_value('%s.%s.context' % (cls.__registry__, |
|
444 cls.__regid__)) |
|
445 if not propval: |
|
446 propval = cls.context |
|
447 if context is not None and propval and context != propval: |
|
448 return 0 |
|
449 return 1 |
|
450 |
|
451 |
|
452 class match_search_state(Selector): |
|
453 """accept if the current request search state is in one of the expected |
|
454 states given to the initializer |
|
455 |
|
456 :param expected: either 'normal' or 'linksearch' (eg searching for an |
|
457 object to create a relation with another) |
|
458 """ |
|
459 def __init__(self, *expected): |
|
460 assert expected, self |
|
461 self.expected = frozenset(expected) |
|
462 |
|
463 def __str__(self): |
|
464 return '%s(%s)' % (self.__class__.__name__, |
|
465 ','.join(sorted(str(s) for s in self.expected))) |
|
466 |
|
467 @lltrace |
|
468 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
469 try: |
|
470 if not req.search_state[0] in self.expected: |
|
471 return 0 |
|
472 except AttributeError: |
|
473 return 1 # class doesn't care about search state, accept it |
|
474 return 1 |
|
475 |
|
476 |
|
477 class match_form_params(match_search_state): |
|
478 """accept if parameters specified as initializer arguments are specified |
|
479 in request's form parameters |
|
480 |
|
481 :param *expected: parameters (eg `basestring`) which are expected to be |
|
482 found in request's form parameters |
|
483 """ |
|
484 |
|
485 @lltrace |
|
486 def __call__(self, cls, req, *args, **kwargs): |
|
487 score = 0 |
|
488 for param in self.expected: |
|
489 if not param in req.form: |
|
490 return 0 |
|
491 score += 1 |
|
492 return len(self.expected) |
|
493 |
|
494 |
|
495 class match_kwargs(match_search_state): |
|
496 """accept if parameters specified as initializer arguments are specified |
|
497 in named arguments given to the selector |
|
498 |
|
499 :param *expected: parameters (eg `basestring`) which are expected to be |
|
500 found in named arguments (kwargs) |
|
501 """ |
|
502 |
|
503 @lltrace |
|
504 def __call__(self, cls, req, *args, **kwargs): |
|
505 for arg in self.expected: |
|
506 if not arg in kwargs: |
|
507 return 0 |
|
508 return len(self.expected) |
|
509 |
|
510 |
|
511 class match_user_groups(match_search_state): |
|
512 """accept if logged users is in at least one of the given groups. Returned |
|
513 score is the number of groups in which the user is. |
|
514 |
|
515 If the special 'owners' group is given: |
|
516 * if row is specified check the entity at the given row/col is owned by the |
|
517 logged user |
|
518 * if row is not specified check all entities in col are owned by the logged |
|
519 user |
|
520 |
|
521 :param *required_groups: name of groups (`basestring`) in which the logged |
|
522 user should be |
|
523 """ |
|
524 |
|
525 @lltrace |
|
526 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
527 user = req.user |
|
528 if user is None: |
|
529 return int('guests' in self.expected) |
|
530 score = user.matching_groups(self.expected) |
|
531 if not score and 'owners' in self.expected and rset: |
|
532 if row is not None: |
|
533 if not user.owns(rset[row][col]): |
|
534 return 0 |
|
535 score = 1 |
|
536 else: |
|
537 score = all(user.owns(r[col]) for r in rset) |
|
538 return score |
|
539 |
|
540 |
|
541 class match_transition(match_search_state): |
|
542 @lltrace |
|
543 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
544 try: |
|
545 # XXX check this is a transition that apply to the object? |
|
546 if not kwargs['transition'].name in self.expected: |
|
547 return 0 |
|
548 except KeyError: |
|
549 return 0 |
|
550 return 1 |
|
551 |
|
552 |
|
553 class match_view(match_search_state): |
|
554 """accept if the current view is in one of the expected vid given to the |
|
555 initializer |
|
556 """ |
|
557 @lltrace |
|
558 def __call__(self, cls, req, rset=None, row=None, col=0, view=None, **kwargs): |
|
559 if view is None or not view.__regid__ in self.expected: |
|
560 return 0 |
|
561 return 1 |
|
562 |
|
563 |
|
564 class appobject_selectable(Selector): |
|
565 """accept with another appobject is selectable using selector's input |
|
566 context. |
|
567 |
|
568 :param registry: a registry name (`basestring`) |
|
569 :param oid: an object identifier (`basestring`) |
|
570 """ |
|
571 def __init__(self, registry, oid): |
|
572 self.registry = registry |
|
573 self.oid = oid |
|
574 |
|
575 def __call__(self, cls, req, **kwargs): |
|
576 try: |
|
577 req.vreg[self.registry].select(self.oid, req, **kwargs) |
|
578 return 1 |
|
579 except NoSelectableObject: |
|
580 return 0 |
|
581 |
|
582 |
|
583 # not so basic selectors ###################################################### |
|
584 |
|
585 class implements(ImplementsMixIn, EClassSelector): |
532 class implements(ImplementsMixIn, EClassSelector): |
586 """accept if entity classes found in the result set implements at least one |
533 """Return non-zero score for entity that are of the given type(s) or |
587 of the interfaces given as argument. Returned score is the number of |
534 implements at least one of the given interface(s). If multiple arguments are |
588 implemented interfaces. |
535 given, matching one of them is enough. |
589 |
536 |
590 See `EClassSelector` documentation for behaviour when row is not specified. |
537 Entity types should be given as string, the corresponding class will be |
591 |
538 fetched from the entity types registry at selection time. |
592 :param *expected_ifaces: expected interfaces. An interface may be a class |
539 |
593 or an entity type (e.g. `basestring`) in which case |
540 See :class:`~cubicweb.selectors.EClassSelector` documentation for entity |
594 the associated class will be searched in the |
541 class lookup / score rules according to the input context. |
595 registry (at selection time) |
542 |
596 |
543 .. note:: when interface is an entity class, the score will reflect class |
597 note: when interface is an entity class, the score will reflect class |
544 proximity so the most specific object will be selected. |
598 proximity so the most specific object'll be selected |
|
599 """ |
545 """ |
600 def score_class(self, eclass, req): |
546 def score_class(self, eclass, req): |
601 return self.score_interfaces(req, eclass, eclass) |
547 return self.score_interfaces(req, eclass, eclass) |
602 |
548 |
603 |
549 |
604 class specified_etype_implements(implements): |
550 class score_entity(EntitySelector): |
605 """accept if entity class specified using an 'etype' parameters in name |
551 """Return score according to an arbitrary function given as argument which |
606 argument or request form implements at least one of the interfaces given as |
552 will be called with input content entity as argument. |
607 argument. Returned score is the number of implemented interfaces. |
553 |
608 |
554 This is a very useful selector that will usually interest you since it |
609 :param *expected_ifaces: expected interfaces. An interface may be a class |
555 allows a lot of things without having to write a specific selector. |
610 or an entity type (e.g. `basestring`) in which case |
556 |
611 the associated class will be searched in the |
557 See :class:`~cubicweb.selectors.EntitySelector` documentation for entity |
612 registry (at selection time) |
558 lookup / score rules according to the input context. |
613 |
559 """ |
614 note: when interface is an entity class, the score will reflect class |
560 def __init__(self, scorefunc, once_is_enough=False): |
615 proximity so the most specific object'll be selected |
561 super(score_entity, self).__init__(once_is_enough) |
616 """ |
562 def intscore(*args, **kwargs): |
617 |
563 score = scorefunc(*args, **kwargs) |
618 @lltrace |
564 if not score: |
619 def __call__(self, cls, req, *args, **kwargs): |
|
620 try: |
|
621 etype = kwargs['etype'] |
|
622 except KeyError: |
|
623 try: |
|
624 etype = req.form['etype'] |
|
625 except KeyError: |
|
626 return 0 |
565 return 0 |
627 else: |
566 if isinstance(score, (int, long)): |
628 # only check this is a known type if etype comes from req.form, |
|
629 # else we want the error to propagate |
|
630 try: |
|
631 etype = req.vreg.case_insensitive_etypes[etype.lower()] |
|
632 req.form['etype'] = etype |
|
633 except KeyError: |
|
634 return 0 |
|
635 score = self.score_class(req.vreg['etypes'].etype_class(etype), req) |
|
636 if score: |
|
637 eschema = req.vreg.schema.eschema(etype) |
|
638 if eschema.has_local_role('add') or eschema.has_perm(req, 'add'): |
|
639 return score |
567 return score |
640 return 0 |
568 return 1 |
641 |
569 self.score_entity = intscore |
642 |
570 |
643 class relation_possible(EClassSelector): |
571 |
644 """accept if entity class found in the result set support the relation. |
572 class relation_possible(EntitySelector): |
645 |
573 """Return 1 for entity that supports the relation, provided that the |
646 See `EClassSelector` documentation for behaviour when row is not specified. |
574 request's user may do some `action` on it (see below). |
647 |
575 |
648 :param rtype: a relation type (`basestring`) |
576 The relation is specified by the following initializer arguments: |
649 :param role: the role of the result set entity in the relation. 'subject' or |
577 |
650 'object', default to 'subject'. |
578 * `rtype`, the name of the relation |
651 :param target_type: if specified, check the relation's end may be of this |
579 |
652 target type (`basestring`) |
580 * `role`, the role of the entity in the relation, either 'subject' or |
653 :param action: a relation schema action (one of 'read', 'add', 'delete') |
581 'object', default to 'subject' |
654 which must be granted to the logged user, else a 0 score will |
582 |
655 be returned |
583 * `target_etype`, optional name of an entity type that should be supported |
656 """ |
584 at the other end of the relation |
|
585 |
|
586 * `action`, a relation schema action (e.g. one of 'read', 'add', 'delete', |
|
587 default to 'read') which must be granted to the user, else a 0 score will |
|
588 be returned |
|
589 |
|
590 * `strict`, boolean (default to False) telling what to do when the user has |
|
591 not globally the permission for the action (eg the action is not granted |
|
592 to one of the user's groups) |
|
593 |
|
594 - when strict is False, if there are some local role defined for this |
|
595 action (e.g. using rql expressions), then the permission will be |
|
596 considered as granted |
|
597 |
|
598 - when strict is True, then the permission will be actually checked for |
|
599 each entity |
|
600 |
|
601 Setting `strict` to True impacts performance for large result set since |
|
602 you'll then get the :class:`~cubicweb.selectors.EntitySelector` behaviour |
|
603 while otherwise you get the :class:`~cubicweb.selectors.EClassSelector`'s |
|
604 one. See those classes documentation for entity lookup / score rules |
|
605 according to the input context. |
|
606 """ |
|
607 |
657 def __init__(self, rtype, role='subject', target_etype=None, |
608 def __init__(self, rtype, role='subject', target_etype=None, |
658 action='read', once_is_enough=False): |
609 action='read', strict=False, **kwargs): |
659 super(relation_possible, self).__init__(once_is_enough) |
610 super(relation_possible, self).__init__(**kwargs) |
660 self.rtype = rtype |
611 self.rtype = rtype |
661 self.role = role |
612 self.role = role |
662 self.target_etype = target_etype |
613 self.target_etype = target_etype |
663 self.action = action |
614 self.action = action |
664 |
615 self.strict = strict |
665 def score_class(self, eclass, req): |
616 |
|
617 # hack hack hack |
|
618 def __call__(self, cls, req, **kwargs): |
|
619 if self.strict: |
|
620 return EntitySelector.__call__(self, cls, req, **kwargs) |
|
621 return EClassSelector.__call__(self, cls, req, **kwargs) |
|
622 |
|
623 def score(self, *args): |
|
624 if self.strict: |
|
625 return EntitySelector.score(self, *args) |
|
626 return EClassSelector.score(self, *args) |
|
627 |
|
628 def _get_rschema(self, eclass): |
666 eschema = eclass.e_schema |
629 eschema = eclass.e_schema |
667 try: |
630 try: |
668 if self.role == 'object': |
631 if self.role == 'object': |
669 rschema = eschema.objrels[self.rtype] |
632 return eschema.objrels[self.rtype] |
670 else: |
633 else: |
671 rschema = eschema.subjrels[self.rtype] |
634 return eschema.subjrels[self.rtype] |
672 except KeyError: |
635 except KeyError: |
673 return 0 |
636 return None |
|
637 |
|
638 def score_class(self, eclass, req): |
|
639 rschema = self._get_rschema(eclass) |
|
640 if rschema is None: |
|
641 return 0 # relation not supported |
|
642 eschema = eclass.e_schema |
674 if self.target_etype is not None: |
643 if self.target_etype is not None: |
675 try: |
644 try: |
676 rdef = rschema.role_rdef(eschema, self.target_etype, self.role) |
645 rdef = rschema.role_rdef(eschema, self.target_etype, self.role) |
677 if not rdef.may_have_permission(self.action, req): |
646 if not rdef.may_have_permission(self.action, req): |
678 return 0 |
647 return 0 |
680 return 0 |
649 return 0 |
681 else: |
650 else: |
682 return rschema.may_have_permission(self.action, req, eschema, self.role) |
651 return rschema.may_have_permission(self.action, req, eschema, self.role) |
683 return 1 |
652 return 1 |
684 |
653 |
685 |
|
686 class partial_relation_possible(PartialSelectorMixIn, relation_possible): |
|
687 """partial version of the relation_possible selector |
|
688 |
|
689 The selector will look for class attributes to find its missing |
|
690 information. The list of attributes required on the class |
|
691 for this selector are: |
|
692 |
|
693 - `rtype`: same as `rtype` parameter of the `relation_possible` selector |
|
694 |
|
695 - `role`: this attribute will be passed to the `cubicweb.role` function |
|
696 to determine the role of class in the relation |
|
697 |
|
698 - `etype` (optional): the entity type on the other side of the relation |
|
699 |
|
700 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
701 which must be granted to the logged user, else a 0 score will |
|
702 be returned |
|
703 """ |
|
704 def __init__(self, action='read', once_is_enough=False): |
|
705 super(partial_relation_possible, self).__init__(None, None, None, |
|
706 action, once_is_enough) |
|
707 |
|
708 def complete(self, cls): |
|
709 self.rtype = cls.rtype |
|
710 self.role = role(cls) |
|
711 self.target_etype = getattr(cls, 'etype', None) |
|
712 |
|
713 |
|
714 class may_add_relation(EntitySelector): |
|
715 """accept if the relation can be added to an entity found in the result set |
|
716 by the logged user. |
|
717 |
|
718 See `EntitySelector` documentation for behaviour when row is not specified. |
|
719 |
|
720 :param rtype: a relation type (`basestring`) |
|
721 :param role: the role of the result set entity in the relation. 'subject' or |
|
722 'object', default to 'subject'. |
|
723 """ |
|
724 |
|
725 def __init__(self, rtype, role='subject', target_etype=None, |
|
726 once_is_enough=False): |
|
727 super(may_add_relation, self).__init__(once_is_enough) |
|
728 self.rtype = rtype |
|
729 self.role = role |
|
730 self.target_etype = target_etype |
|
731 |
|
732 def score_entity(self, entity): |
654 def score_entity(self, entity): |
733 rschema = entity._cw.vreg.schema.rschema(self.rtype) |
655 rschema = self._get_rschema(entity) |
|
656 if rschema is None: |
|
657 return 0 # relation not supported |
734 if self.target_etype is not None: |
658 if self.target_etype is not None: |
735 rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role) |
659 rschema = rschema.role_rdef(entity.e_schema, self.target_etype, self.role) |
736 if self.role == 'subject': |
660 if self.role == 'subject': |
737 if not rschema.has_perm(entity._cw, 'add', fromeid=entity.eid): |
661 if not rschema.has_perm(entity._cw, 'add', fromeid=entity.eid): |
738 return 0 |
662 return 0 |
739 elif not rschema.has_perm(entity._cw, 'add', toeid=entity.eid): |
663 elif not rschema.has_perm(entity._cw, 'add', toeid=entity.eid): |
740 return 0 |
664 return 0 |
741 return 1 |
665 return 1 |
742 |
666 |
743 |
667 |
744 class partial_may_add_relation(PartialSelectorMixIn, may_add_relation): |
668 class partial_relation_possible(PartialSelectorMixIn, relation_possible): |
745 """partial version of the may_add_relation selector |
669 """Same as :class:~`cubicweb.selectors.relation_possible`, but will look for |
746 |
670 attributes of the selected class to get information which is otherwise |
747 The selector will look for class attributes to find its missing |
671 expected by the initializer, except for `action` and `strict` which are kept |
748 information. The list of attributes required on the class |
672 as initializer arguments. |
749 for this selector are: |
673 |
750 |
674 This is useful to predefine selector of an abstract class designed to be |
751 - `rtype`: same as `rtype` parameter of the `relation_possible` selector |
675 customized. |
752 |
676 """ |
753 - `role`: this attribute will be passed to the `cubicweb.role` function |
677 def __init__(self, action='read', **kwargs): |
754 to determine the role of class in the relation. |
678 super(partial_relation_possible, self).__init__(None, None, None, |
755 |
679 action, **kwargs) |
756 :param action: a relation schema action (one of 'read', 'add', 'delete') |
|
757 which must be granted to the logged user, else a 0 score will |
|
758 be returned |
|
759 """ |
|
760 def __init__(self, once_is_enough=False): |
|
761 super(partial_may_add_relation, self).__init__(None, once_is_enough=once_is_enough) |
|
762 |
680 |
763 def complete(self, cls): |
681 def complete(self, cls): |
764 self.rtype = cls.rtype |
682 self.rtype = cls.rtype |
765 self.role = role(cls) |
683 self.role = role(cls) |
766 self.target_etype = getattr(cls, 'etype', None) |
684 self.target_etype = getattr(cls, 'etype', None) |
|
685 if self.target_etype is not None: |
|
686 warn('[3.6] please rename etype to target_etype on %s' % cls, |
|
687 DeprecationWarning) |
|
688 else: |
|
689 self.target_etype = getattr(cls, 'target_etype', None) |
767 |
690 |
768 |
691 |
769 class has_related_entities(EntitySelector): |
692 class has_related_entities(EntitySelector): |
770 """accept if entity found in the result set has some linked entities using |
693 """Return 1 if entity support the specified relation and has some linked |
771 the specified relation (optionaly filtered according to the specified target |
694 entities by this relation , optionaly filtered according to the specified |
772 type). Checks first if the relation is possible. |
695 target type. |
773 |
696 |
774 See `EntitySelector` documentation for behaviour when row is not specified. |
697 The relation is specified by the following initializer arguments: |
775 |
698 |
776 :param rtype: a relation type (`basestring`) |
699 * `rtype`, the name of the relation |
777 :param role: the role of the result set entity in the relation. 'subject' or |
700 |
778 'object', default to 'subject'. |
701 * `role`, the role of the entity in the relation, either 'subject' or |
779 :param target_type: if specified, check the relation's end may be of this |
702 'object', default to 'subject'. |
780 target type (`basestring`) |
703 |
781 """ |
704 * `target_etype`, optional name of an entity type that should be found |
782 def __init__(self, rtype, role='subject', target_etype=None, |
705 at the other end of the relation |
783 once_is_enough=False): |
706 |
784 super(has_related_entities, self).__init__(once_is_enough) |
707 See :class:`~cubicweb.selectors.EntitySelector` documentation for entity |
|
708 lookup / score rules according to the input context. |
|
709 """ |
|
710 def __init__(self, rtype, role='subject', target_etype=None, **kwargs): |
|
711 super(has_related_entities, self).__init__(**kwargs) |
785 self.rtype = rtype |
712 self.rtype = rtype |
786 self.role = role |
713 self.role = role |
787 self.target_etype = target_etype |
714 self.target_etype = target_etype |
788 |
715 |
789 def score_entity(self, entity): |
716 def score_entity(self, entity): |
880 return 1 |
807 return 1 |
881 return 0 |
808 return 0 |
882 |
809 |
883 |
810 |
884 class has_add_permission(EClassSelector): |
811 class has_add_permission(EClassSelector): |
885 """accept if logged user has the add permission on entity class found in the |
812 """Return 1 if request's user has the add permission on entity type |
886 result set, and class is not a strict subobject. |
813 specified in the `etype` initializer argument, or according to entity found |
887 |
814 in the input content if not specified. |
888 See `EClassSelector` documentation for behaviour when row is not specified. |
815 |
889 """ |
816 It also check that then entity type is not a strict subobject (e.g. may only |
890 def score(self, cls, req, etype): |
817 be used as a composed of another entity). |
891 eschema = req.vreg.schema.eschema(etype) |
818 |
892 if not (eschema.final or eschema.is_subobject(strict=True)) \ |
819 See :class:`~cubicweb.selectors.EClassSelector` documentation for entity |
893 and eschema.has_perm(req, 'add'): |
820 class lookup / score rules according to the input context when `etype` is |
894 return 1 |
821 not specified. |
895 return 0 |
822 """ |
|
823 def __init__(self, etype=None, **kwargs): |
|
824 super(has_add_permission, self).__init__(**kwargs) |
|
825 self.etype = etype |
|
826 |
|
827 @lltrace |
|
828 def __call__(self, cls, req, **kwargs): |
|
829 if self.etype is None: |
|
830 return super(has_add_permission, self).__call__(cls, req, **kwargs) |
|
831 return self.score(cls, req, self.etype) |
|
832 |
|
833 def score_class(self, eclass, req): |
|
834 eschema = eclass.e_schema |
|
835 if eschema.final or eschema.is_subobject(strict=True) \ |
|
836 or not eschema.has_perm(req, 'add'): |
|
837 return 0 |
|
838 return 1 |
896 |
839 |
897 |
840 |
898 class rql_condition(EntitySelector): |
841 class rql_condition(EntitySelector): |
899 """accept if an arbitrary rql return some results for an eid found in the |
842 """Return non-zero score if arbitrary rql specified in `expression` |
900 result set. Returned score is the number of items returned by the rql |
843 initializer argument return some results for entity found in the input |
|
844 context. Returned score is the number of items returned by the rql |
901 condition. |
845 condition. |
902 |
846 |
903 See `EntitySelector` documentation for behaviour when row is not specified. |
847 `expression` is expected to be a string containing an rql expression, which |
904 |
848 must use 'X' variable to represent the context entity and may use 'U' to |
905 :param expression: basestring containing an rql expression, which should use |
849 represent the request's user. |
906 X variable to represent the context entity and may use U |
850 |
907 to represent the logged user |
851 See :class:`~cubicweb.selectors.EntitySelector` documentation for entity |
908 |
852 lookup / score rules according to the input context. |
909 return the sum of the number of items returned by the rql condition as score |
|
910 or 0 at the first entity scoring to zero. |
|
911 """ |
853 """ |
912 def __init__(self, expression, once_is_enough=False): |
854 def __init__(self, expression, once_is_enough=False): |
913 super(rql_condition, self).__init__(once_is_enough) |
855 super(rql_condition, self).__init__(once_is_enough) |
914 if 'U' in frozenset(split_expression(expression)): |
856 if 'U' in frozenset(split_expression(expression)): |
915 rql = 'Any X WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression |
857 rql = 'Any X WHERE X eid %%(x)s, U eid %%(u)s, %s' % expression |
916 else: |
858 else: |
917 rql = 'Any X WHERE X eid %%(x)s, %s' % expression |
859 rql = 'Any X WHERE X eid %%(x)s, %s' % expression |
918 self.rql = rql |
860 self.rql = rql |
919 |
861 |
|
862 def __repr__(self): |
|
863 return u'<rql_condition "%s" at %x>' % (self.rql, id(self)) |
|
864 |
920 def score(self, req, rset, row, col): |
865 def score(self, req, rset, row, col): |
921 try: |
866 try: |
922 return len(req.execute(self.rql, {'x': rset[row][col], |
867 return len(req.execute(self.rql, {'x': rset[row][col], |
923 'u': req.user.eid}, 'x')) |
868 'u': req.user.eid}, 'x')) |
924 except Unauthorized: |
869 except Unauthorized: |
925 return 0 |
870 return 0 |
926 |
871 |
927 def __repr__(self): |
872 # logged user selectors ######################################################## |
928 return u'<rql_condition "%s" at %x>' % (self.rql, id(self)) |
873 |
929 |
874 @objectify_selector |
930 |
875 @lltrace |
931 class score_entity(EntitySelector): |
876 def authenticated_user(cls, req, **kwargs): |
932 """accept if some arbitrary function return a positive score for an entity |
877 """Return 1 if the user is authenticated (e.g. not the anonymous user). |
933 found in the result set. |
878 |
934 |
879 May only be used on the web side, not on the data repository side. |
935 See `EntitySelector` documentation for behaviour when row is not specified. |
880 """ |
936 |
881 if req.cnx.anonymous_connection: |
937 :param scorefunc: callable expected to take an entity as argument and to |
882 return 0 |
938 return a score >= 0 |
883 return 1 |
939 """ |
884 |
940 def __init__(self, scorefunc, once_is_enough=False): |
885 |
941 super(score_entity, self).__init__(once_is_enough) |
886 # XXX == ~ authenticated_user() |
942 def intscore(*args, **kwargs): |
887 def anonymous_user(): |
943 score = scorefunc(*args, **kwargs) |
888 """Return 1 if the user is not authenticated (e.g. is the anonymous user). |
944 if not score: |
889 |
|
890 May only be used on the web side, not on the data repository side. |
|
891 """ |
|
892 return ~ authenticated_user() |
|
893 |
|
894 |
|
895 class match_user_groups(ExpectedValueSelector): |
|
896 """Return a non-zero score if request's user is in at least one of the |
|
897 groups given as initializer argument. Returned score is the number of groups |
|
898 in which the user is. |
|
899 |
|
900 If the special 'owners' group is given and `rset` is specified in the input |
|
901 context: |
|
902 |
|
903 * if `row` is specified check the entity at the given `row`/`col` (default |
|
904 to 0) is owned by the user |
|
905 |
|
906 * else check all entities in `col` (default to 0) are owned by the user |
|
907 """ |
|
908 |
|
909 @lltrace |
|
910 def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs): |
|
911 user = req.user |
|
912 if user is None: |
|
913 return int('guests' in self.expected) |
|
914 score = user.matching_groups(self.expected) |
|
915 if not score and 'owners' in self.expected and rset: |
|
916 if row is not None: |
|
917 if not user.owns(rset[row][col]): |
|
918 return 0 |
|
919 score = 1 |
|
920 else: |
|
921 score = all(user.owns(r[col]) for r in rset) |
|
922 return score |
|
923 |
|
924 |
|
925 # Web request selectors ######################################################## |
|
926 |
|
927 @objectify_selector |
|
928 @lltrace |
|
929 def primary_view(cls, req, view=None, **kwargs): |
|
930 """Return 1 if: |
|
931 |
|
932 * *no view is specified* in the input context |
|
933 |
|
934 * a view is specified and its `.is_primary()` method return True |
|
935 |
|
936 This selector is usually used by contextual components that only want to |
|
937 appears for the primary view of an entity. |
|
938 """ |
|
939 if view is not None and not view.is_primary(): |
|
940 return 0 |
|
941 return 1 |
|
942 |
|
943 |
|
944 class match_view(ExpectedValueSelector): |
|
945 """Return 1 if a view is specified an as its registry id is in one of the |
|
946 expected view id given to the initializer. |
|
947 """ |
|
948 @lltrace |
|
949 def __call__(self, cls, req, view=None, **kwargs): |
|
950 if view is None or not view.__regid__ in self.expected: |
|
951 return 0 |
|
952 return 1 |
|
953 |
|
954 |
|
955 @objectify_selector |
|
956 @lltrace |
|
957 def match_context_prop(cls, req, context=None, **kwargs): |
|
958 """Return 1 if: |
|
959 |
|
960 * no `context` is specified in input context (take care to confusion, here |
|
961 `context` refers to a string given as an argument to the input context...) |
|
962 |
|
963 * specified `context` is matching the context property value for the |
|
964 appobject using this selector |
|
965 |
|
966 * the appobject's context property value is None |
|
967 |
|
968 This selector is usually used by contextual components that want to appears |
|
969 in a configurable place. |
|
970 """ |
|
971 if context is None: |
|
972 return 0 |
|
973 propval = req.property_value('%s.%s.context' % (cls.__registry__, |
|
974 cls.__regid__)) |
|
975 if not propval: |
|
976 propval = cls.context |
|
977 if propval and context != propval: |
|
978 return 0 |
|
979 return 1 |
|
980 |
|
981 |
|
982 class match_search_state(ExpectedValueSelector): |
|
983 """Return 1 if the current request search state is in one of the expected |
|
984 states given to the initializer. |
|
985 |
|
986 Known search states are either 'normal' or 'linksearch' (eg searching for an |
|
987 object to create a relation with another). |
|
988 |
|
989 This selector is usually used by action that want to appears or not according |
|
990 to the ui search state. |
|
991 """ |
|
992 |
|
993 @lltrace |
|
994 def __call__(self, cls, req, **kwargs): |
|
995 try: |
|
996 if not req.search_state[0] in self.expected: |
945 return 0 |
997 return 0 |
946 if isinstance(score, (int, long)): |
998 except AttributeError: |
|
999 return 1 # class doesn't care about search state, accept it |
|
1000 return 1 |
|
1001 |
|
1002 |
|
1003 class match_form_params(ExpectedValueSelector): |
|
1004 """Return non-zero score if parameter names specified as initializer |
|
1005 arguments are specified in request's form parameters. When multiple |
|
1006 parameters are specified, all of them should be found in req.form. Return a |
|
1007 score corresponding to the number of expected parameters. |
|
1008 """ |
|
1009 |
|
1010 @lltrace |
|
1011 def __call__(self, cls, req, **kwargs): |
|
1012 for param in self.expected: |
|
1013 if not param in req.form: |
|
1014 return 0 |
|
1015 return len(self.expected) |
|
1016 |
|
1017 |
|
1018 class specified_etype_implements(implements): |
|
1019 """Return non-zero score if the entity type specified by an 'etype' key |
|
1020 searched in (by priority) input context kwargs and request form parameters |
|
1021 match a known entity type (case insensitivly), and it's associated entity |
|
1022 class is of one of the type(s) given to the initializer or implements at |
|
1023 least one of the given interfaces. If multiple arguments are given, matching |
|
1024 one of them is enough. |
|
1025 |
|
1026 Entity types should be given as string, the corresponding class will be |
|
1027 fetched from the entity types registry at selection time. |
|
1028 |
|
1029 .. note:: when interface is an entity class, the score will reflect class |
|
1030 proximity so the most specific object will be selected. |
|
1031 |
|
1032 This selector is usually used by views holding entity creation forms (since |
|
1033 we've no result set to work on). |
|
1034 """ |
|
1035 |
|
1036 @lltrace |
|
1037 def __call__(self, cls, req, **kwargs): |
|
1038 try: |
|
1039 etype = kwargs['etype'] |
|
1040 except KeyError: |
|
1041 try: |
|
1042 etype = req.form['etype'] |
|
1043 except KeyError: |
|
1044 return 0 |
|
1045 else: |
|
1046 # only check this is a known type if etype comes from req.form, |
|
1047 # else we want the error to propagate |
|
1048 try: |
|
1049 etype = req.vreg.case_insensitive_etypes[etype.lower()] |
|
1050 req.form['etype'] = etype |
|
1051 except KeyError: |
|
1052 return 0 |
|
1053 score = self.score_class(req.vreg['etypes'].etype_class(etype), req) |
|
1054 if score: |
|
1055 eschema = req.vreg.schema.eschema(etype) |
|
1056 if eschema.has_local_role('add') or eschema.has_perm(req, 'add'): |
947 return score |
1057 return score |
|
1058 return 0 |
|
1059 |
|
1060 |
|
1061 # Other selectors ############################################################## |
|
1062 |
|
1063 |
|
1064 class match_transition(ExpectedValueSelector): |
|
1065 """Return 1 if a `transition` argument is found in the input context which |
|
1066 has a `.name` attribute matching one of the expected names given to the |
|
1067 initializer. |
|
1068 """ |
|
1069 @lltrace |
|
1070 def __call__(self, cls, req, transition=None, **kwargs): |
|
1071 # XXX check this is a transition that apply to the object? |
|
1072 if transition is not None and getattr(transition, 'name', None) in self.expected: |
948 return 1 |
1073 return 1 |
949 self.score_entity = intscore |
1074 return 0 |
|
1075 |
950 |
1076 |
951 ## deprecated stuff ############################################################ |
1077 ## deprecated stuff ############################################################ |
952 |
1078 |
953 entity_implements = class_renamed('entity_implements', implements) |
1079 entity_implements = class_renamed('entity_implements', implements) |
954 |
1080 |