406 raise unauthorized |
410 raise unauthorized |
407 else: |
411 else: |
408 # explicitly specified processor: don't try to catch the exception |
412 # explicitly specified processor: don't try to catch the exception |
409 return proc.process_query(uquery) |
413 return proc.process_query(uquery) |
410 raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query')) |
414 raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query')) |
|
415 |
|
416 |
|
417 |
|
418 ## RQL suggestions builder #################################################### |
|
419 class RQLSuggestionsBuilder(Component): |
|
420 """main entry point is `build_suggestions()` which takes |
|
421 an incomplete RQL query and returns a list of suggestions to complete |
|
422 the query. |
|
423 |
|
424 This component is enabled by default and is used to provide autocompletion |
|
425 in the RQL search bar. If you don't want this feature in your application, |
|
426 just unregister it or make it unselectable. |
|
427 |
|
428 .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions |
|
429 .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set |
|
430 .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes |
|
431 .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations |
|
432 .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary |
|
433 """ |
|
434 __regid__ = 'rql.suggestions' |
|
435 |
|
436 #: maximum number of results to fetch when suggesting attribute values |
|
437 attr_value_limit = 20 |
|
438 |
|
439 def build_suggestions(self, user_rql): |
|
440 """return a list of suggestions to complete `user_rql` |
|
441 |
|
442 :param user_rql: an incomplete RQL query |
|
443 """ |
|
444 req = self._cw |
|
445 try: |
|
446 if 'WHERE' not in user_rql: # don't try to complete if there's no restriction |
|
447 return [] |
|
448 variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)] |
|
449 if ',' in restrictions: |
|
450 restrictions, incomplete_part = restrictions.rsplit(',', 1) |
|
451 user_rql = '%s WHERE %s' % (variables, restrictions) |
|
452 else: |
|
453 restrictions, incomplete_part = '', restrictions |
|
454 user_rql = variables |
|
455 select = parse(user_rql).children[0] |
|
456 req.vreg.rqlhelper.annotate(select) |
|
457 req.vreg.solutions(req, select, {}) |
|
458 if restrictions: |
|
459 return ['%s, %s' % (user_rql, suggestion) |
|
460 for suggestion in self.rql_build_suggestions(select, incomplete_part)] |
|
461 else: |
|
462 return ['%s WHERE %s' % (user_rql, suggestion) |
|
463 for suggestion in self.rql_build_suggestions(select, incomplete_part)] |
|
464 except Exception, exc: # we never want to crash |
|
465 self.debug('failed to build suggestions: %s', exc) |
|
466 return [] |
|
467 |
|
468 ## actual completion entry points ######################################### |
|
469 def rql_build_suggestions(self, select, incomplete_part): |
|
470 """ |
|
471 :param select: the annotated select node (rql syntax tree) |
|
472 :param incomplete_part: the part of the rql query that needs |
|
473 to be completed, (e.g. ``X is Pr``, ``X re``) |
|
474 """ |
|
475 chunks = incomplete_part.split(None, 2) |
|
476 if not chunks: # nothing to complete |
|
477 return [] |
|
478 if len(chunks) == 1: # `incomplete` looks like "MYVAR" |
|
479 return self._complete_rqlvar(select, *chunks) |
|
480 elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel" |
|
481 return self._complete_rqlvar_and_rtype(select, *chunks) |
|
482 elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something" |
|
483 return self._complete_relation_object(select, *chunks) |
|
484 else: # would be anything else, hard to decide what to do here |
|
485 return [] |
|
486 |
|
487 # _complete_* methods are considered private, at least while the API |
|
488 # isn't stabilized. |
|
489 def _complete_rqlvar(self, select, rql_var): |
|
490 """return suggestions for "variable only" incomplete_part |
|
491 |
|
492 as in : |
|
493 |
|
494 - Any X WHERE X |
|
495 - Any X WHERE X is Project, Y |
|
496 - etc. |
|
497 """ |
|
498 return ['%s %s %s' % (rql_var, rtype, dest_var) |
|
499 for rtype, dest_var in self.possible_relations(select, rql_var)] |
|
500 |
|
501 def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype): |
|
502 """return suggestions for "variable + rtype" incomplete_part |
|
503 |
|
504 as in : |
|
505 |
|
506 - Any X WHERE X is |
|
507 - Any X WHERE X is Person, X firstn |
|
508 - etc. |
|
509 """ |
|
510 # special case `user_type` == 'is', return every possible type. |
|
511 if user_rtype == 'is': |
|
512 return self._complete_is_relation(select, rql_var) |
|
513 else: |
|
514 return ['%s %s %s' % (rql_var, rtype, dest_var) |
|
515 for rtype, dest_var in self.possible_relations(select, rql_var) |
|
516 if rtype.startswith(user_rtype)] |
|
517 |
|
518 def _complete_relation_object(self, select, rql_var, user_rtype, user_value): |
|
519 """return suggestions for "variable + rtype + some_incomplete_value" |
|
520 |
|
521 as in : |
|
522 |
|
523 - Any X WHERE X is Per |
|
524 - Any X WHERE X is Person, X firstname " |
|
525 - Any X WHERE X is Person, X firstname "Pa |
|
526 - etc. |
|
527 """ |
|
528 # special case `user_type` == 'is', return every possible type. |
|
529 if user_rtype == 'is': |
|
530 return self._complete_is_relation(select, rql_var, user_value) |
|
531 elif user_value: |
|
532 if user_value[0] in ('"', "'"): |
|
533 # if finished string, don't suggest anything |
|
534 if len(user_value) > 1 and user_value[-1] == user_value[0]: |
|
535 return [] |
|
536 user_value = user_value[1:] |
|
537 return ['%s %s "%s"' % (rql_var, user_rtype, value) |
|
538 for value in self.vocabulary(select, rql_var, |
|
539 user_rtype, user_value)] |
|
540 return [] |
|
541 |
|
542 def _complete_is_relation(self, select, rql_var, prefix=''): |
|
543 """return every possible types for rql_var |
|
544 |
|
545 :param prefix: if specified, will only return entity types starting |
|
546 with the specified value. |
|
547 """ |
|
548 return ['%s is %s' % (rql_var, etype) |
|
549 for etype in self.possible_etypes(select, rql_var, prefix)] |
|
550 |
|
551 def etypes_suggestion_set(self): |
|
552 """returns the list of possible entity types to suggest |
|
553 |
|
554 The default is to return any non-final entity type available |
|
555 in the schema. |
|
556 |
|
557 Can be overridden for instance if an application decides |
|
558 to restrict this list to a meaningful set of business etypes. |
|
559 """ |
|
560 schema = self._cw.vreg.schema |
|
561 return set(eschema.type for eschema in schema.entities() if not eschema.final) |
|
562 |
|
563 def possible_etypes(self, select, rql_var, prefix=''): |
|
564 """return all possible etypes for `rql_var` |
|
565 |
|
566 The returned list will always be a subset of meth:`etypes_suggestion_set` |
|
567 |
|
568 :param select: the annotated select node (rql syntax tree) |
|
569 :param rql_var: the variable name for which we want to know possible types |
|
570 :param prefix: if specified, will only return etypes starting with it |
|
571 """ |
|
572 available_etypes = self.etypes_suggestion_set() |
|
573 possible_etypes = set() |
|
574 for sol in select.solutions: |
|
575 if rql_var in sol and sol[rql_var] in available_etypes: |
|
576 possible_etypes.add(sol[rql_var]) |
|
577 if not possible_etypes: |
|
578 # `Any X WHERE X is Person, Y is` |
|
579 # -> won't have a solution, need to give all etypes |
|
580 possible_etypes = available_etypes |
|
581 return sorted(etype for etype in possible_etypes if etype.startswith(prefix)) |
|
582 |
|
583 def possible_relations(self, select, rql_var, include_meta=False): |
|
584 """returns a list of couple (rtype, dest_var) for each possible |
|
585 relations with `rql_var` as subject. |
|
586 |
|
587 ``dest_var`` will be picked among availabel variables if types match, |
|
588 otherwise a new one will be created. |
|
589 """ |
|
590 schema = self._cw.vreg.schema |
|
591 relations = set() |
|
592 untyped_dest_var = rqlvar_maker(defined=select.defined_vars).next() |
|
593 # for each solution |
|
594 # 1. find each possible relation |
|
595 # 2. for each relation: |
|
596 # 2.1. if the relation is meta, skip it |
|
597 # 2.2. for each possible destination type, pick up possible |
|
598 # variables for this type or use a new one |
|
599 for sol in select.solutions: |
|
600 etype = sol[rql_var] |
|
601 sol_by_types = {} |
|
602 for varname, var_etype in sol.items(): |
|
603 # don't push subject var to avoid "X relation X" suggestion |
|
604 if varname != rql_var: |
|
605 sol_by_types.setdefault(var_etype, []).append(varname) |
|
606 for rschema in schema[etype].subject_relations(): |
|
607 if include_meta or not rschema.meta: |
|
608 for dest in rschema.objects(etype): |
|
609 for varname in sol_by_types.get(dest.type, (untyped_dest_var,)): |
|
610 suggestion = (rschema.type, varname) |
|
611 if suggestion not in relations: |
|
612 relations.add(suggestion) |
|
613 return sorted(relations) |
|
614 |
|
615 def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value): |
|
616 """return acceptable vocabulary for `rql_var` + `user_rtype` in `select` |
|
617 |
|
618 Vocabulary is either found from schema (Yams) definition or |
|
619 directly from database. |
|
620 """ |
|
621 schema = self._cw.vreg.schema |
|
622 vocab = [] |
|
623 for sol in select.solutions: |
|
624 # for each solution : |
|
625 # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it |
|
626 # to define possible values |
|
627 # - Otherwise, query the database to fetch available values from |
|
628 # database (limiting results to `self.attr_value_limit`) |
|
629 try: |
|
630 eschema = schema.eschema(sol[rql_var]) |
|
631 rdef = eschema.rdef(user_rtype) |
|
632 except KeyError: # unknown relation |
|
633 continue |
|
634 cstr = rdef.constraint_by_interface(IVocabularyConstraint) |
|
635 if cstr is not None: |
|
636 # a vocabulary is found, use it |
|
637 vocab += [value for value in cstr.vocabulary() |
|
638 if value.startswith(rtype_incomplete_value)] |
|
639 elif rdef.final: |
|
640 # no vocab, query database to find possible value |
|
641 vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % ( |
|
642 self.attr_value_limit, eschema.type, user_rtype) |
|
643 vocab_kwargs = {} |
|
644 if rtype_incomplete_value: |
|
645 vocab_rql += ', X %s LIKE %%(value)s' % user_rtype |
|
646 vocab_kwargs['value'] = '%s%%' % rtype_incomplete_value |
|
647 vocab += [value for value, in |
|
648 self._cw.execute(vocab_rql, vocab_kwargs)] |
|
649 return sorted(set(vocab)) |
|
650 |
|
651 |
|
652 |
|
653 @ajaxfunc(output_type='json') |
|
654 def rql_suggest(self): |
|
655 rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw) |
|
656 if rql_builder: |
|
657 return rql_builder.build_suggestions(self._cw.form['term']) |
|
658 return [] |