cubicweb/web/views/magicsearch.py
changeset 11057 0b59724cb3f2
parent 10889 310344d6cc6c
child 11767 432f87a63057
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """a query processor to handle quick search shortcuts for cubicweb
       
    19 """
       
    20 
       
    21 __docformat__ = "restructuredtext en"
       
    22 
       
    23 import re
       
    24 from logging import getLogger
       
    25 
       
    26 from six import text_type
       
    27 
       
    28 from yams.interfaces import IVocabularyConstraint
       
    29 
       
    30 from rql import RQLSyntaxError, BadRQLQuery, parse
       
    31 from rql.utils import rqlvar_maker
       
    32 from rql.nodes import Relation
       
    33 
       
    34 from cubicweb import Unauthorized
       
    35 from cubicweb.view import Component
       
    36 from cubicweb.web.views.ajaxcontroller import ajaxfunc
       
    37 
       
    38 LOGGER = getLogger('cubicweb.magicsearch')
       
    39 
       
    40 def _get_approriate_translation(translations_found, eschema):
       
    41     """return the first (should be the only one) possible translation according
       
    42     to the given entity type
       
    43     """
       
    44     # get the list of all attributes / relations for this kind of entity
       
    45     existing_relations = set(eschema.subject_relations())
       
    46     consistent_translations = translations_found & existing_relations
       
    47     if len(consistent_translations) == 0:
       
    48         return None
       
    49     return consistent_translations.pop()
       
    50 
       
    51 
       
    52 def translate_rql_tree(rqlst, translations, schema):
       
    53     """Try to translate each relation in the RQL syntax tree
       
    54 
       
    55     :type rqlst: `rql.stmts.Statement`
       
    56     :param rqlst: the RQL syntax tree
       
    57 
       
    58     :type translations: dict
       
    59     :param translations: the reverted l10n dict
       
    60 
       
    61     :type schema: `cubicweb.schema.Schema`
       
    62     :param schema: the instance's schema
       
    63     """
       
    64     # var_types is used as a map : var_name / var_type
       
    65     vartypes = {}
       
    66     # ambiguous_nodes is used as a map : relation_node / (var_name, available_translations)
       
    67     ambiguous_nodes = {}
       
    68     # For each relation node, check if it's a localized relation name
       
    69     # If it's a localized name, then use the original relation name, else
       
    70     # keep the existing relation name
       
    71     for relation in rqlst.get_nodes(Relation):
       
    72         rtype = relation.r_type
       
    73         lhs, rhs = relation.get_variable_parts()
       
    74         if rtype == 'is':
       
    75             try:
       
    76                 etype = translations[rhs.value]
       
    77                 rhs.value = etype
       
    78             except KeyError:
       
    79                 # If no translation found, leave the entity type as is
       
    80                 etype = rhs.value
       
    81             # Memorize variable's type
       
    82             vartypes[lhs.name] = etype
       
    83         else:
       
    84             try:
       
    85                 translation_set = translations[rtype]
       
    86             except KeyError:
       
    87                 pass # If no translation found, leave the relation type as is
       
    88             else:
       
    89                 # Only one possible translation, no ambiguity
       
    90                 if len(translation_set) == 1:
       
    91                     relation.r_type = next(iter(translations[rtype]))
       
    92                 # More than 1 possible translation => resolve it later
       
    93                 else:
       
    94                     ambiguous_nodes[relation] = (lhs.name, translation_set)
       
    95     if ambiguous_nodes:
       
    96         resolve_ambiguities(vartypes, ambiguous_nodes, schema)
       
    97 
       
    98 
       
    99 def resolve_ambiguities(var_types, ambiguous_nodes, schema):
       
   100     """Tries to resolve remaining ambiguities for translation
       
   101     /!\ An ambiguity is when two different string can be localized with
       
   102         the same string
       
   103     A simple example:
       
   104       - 'name' in a company context will be localized as 'nom' in French
       
   105       - but ... 'surname' will also be localized as 'nom'
       
   106 
       
   107     :type var_types: dict
       
   108     :param var_types: a map : var_name / var_type
       
   109 
       
   110     :type ambiguous_nodes: dict
       
   111     :param ambiguous_nodes: a map : relation_node / (var_name, available_translations)
       
   112 
       
   113     :type schema: `cubicweb.schema.Schema`
       
   114     :param schema: the instance's schema
       
   115     """
       
   116     # Now, try to resolve ambiguous translations
       
   117     for relation, (var_name, translations_found) in ambiguous_nodes.items():
       
   118         try:
       
   119             vartype = var_types[var_name]
       
   120         except KeyError:
       
   121             continue
       
   122         # Get schema for this entity type
       
   123         eschema = schema.eschema(vartype)
       
   124         rtype = _get_approriate_translation(translations_found, eschema)
       
   125         if rtype is None:
       
   126             continue
       
   127         relation.r_type = rtype
       
   128 
       
   129 
       
   130 
       
   131 QUOTED_SRE = re.compile(r'(.*?)(["\'])(.+?)\2')
       
   132 
       
   133 TRANSLATION_MAPS = {}
       
   134 def trmap(config, schema, lang):
       
   135     try:
       
   136         return TRANSLATION_MAPS[lang]
       
   137     except KeyError:
       
   138         assert lang in config.translations, '%s %s' % (lang, config.translations)
       
   139         tr, ctxtr = config.translations[lang]
       
   140         langmap = {}
       
   141         for etype in schema.entities():
       
   142             etype = str(etype)
       
   143             langmap[tr(etype).capitalize()] = etype
       
   144             langmap[etype.capitalize()] = etype
       
   145         for rtype in schema.relations():
       
   146             rtype = str(rtype)
       
   147             langmap.setdefault(tr(rtype).lower(), set()).add(rtype)
       
   148             langmap.setdefault(rtype, set()).add(rtype)
       
   149         TRANSLATION_MAPS[lang] = langmap
       
   150         return langmap
       
   151 
       
   152 
       
   153 class BaseQueryProcessor(Component):
       
   154     __abstract__ = True
       
   155     __regid__ = 'magicsearch_processor'
       
   156     # set something if you want explicit component search facility for the
       
   157     # component
       
   158     name = None
       
   159 
       
   160     def process_query(self, uquery):
       
   161         args = self.preprocess_query(uquery)
       
   162         try:
       
   163             return self._cw.execute(*args)
       
   164         finally:
       
   165             # rollback necessary to avoid leaving the connection in a bad state
       
   166             self._cw.cnx.rollback()
       
   167 
       
   168     def preprocess_query(self, uquery):
       
   169         raise NotImplementedError()
       
   170 
       
   171 
       
   172 
       
   173 
       
   174 class DoNotPreprocess(BaseQueryProcessor):
       
   175     """this one returns the raw query and should be placed in first position
       
   176     of the chain
       
   177     """
       
   178     name = 'rql'
       
   179     priority = 0
       
   180     def preprocess_query(self, uquery):
       
   181         return uquery,
       
   182 
       
   183 
       
   184 class QueryTranslator(BaseQueryProcessor):
       
   185     """ parses through rql and translates into schema language entity names
       
   186     and attributes
       
   187     """
       
   188     priority = 2
       
   189     def preprocess_query(self, uquery):
       
   190         rqlst = parse(uquery, print_errors=False)
       
   191         schema = self._cw.vreg.schema
       
   192         # rql syntax tree will be modified in place if necessary
       
   193         translate_rql_tree(rqlst, trmap(self._cw.vreg.config, schema, self._cw.lang),
       
   194                            schema)
       
   195         return rqlst.as_string(),
       
   196 
       
   197 
       
   198 class QSPreProcessor(BaseQueryProcessor):
       
   199     """Quick search preprocessor
       
   200 
       
   201     preprocessing query in shortcut form to their RQL form
       
   202     """
       
   203     priority = 4
       
   204 
       
   205     def preprocess_query(self, uquery):
       
   206         """try to get rql from a unicode query string"""
       
   207         args = None
       
   208         try:
       
   209             # Process as if there was a quoted part
       
   210             args = self._quoted_words_query(uquery)
       
   211         ## No quoted part
       
   212         except BadRQLQuery:
       
   213             words = uquery.split()
       
   214             if len(words) == 1:
       
   215                 args = self._one_word_query(*words)
       
   216             elif len(words) == 2:
       
   217                 args = self._two_words_query(*words)
       
   218             elif len(words) == 3:
       
   219                 args = self._three_words_query(*words)
       
   220             else:
       
   221                 raise
       
   222         return args
       
   223 
       
   224     def _get_entity_type(self, word):
       
   225         """check if the given word is matching an entity type, return it if
       
   226         it's the case or raise BadRQLQuery if not
       
   227         """
       
   228         etype = word.capitalize()
       
   229         try:
       
   230             return trmap(self._cw.vreg.config, self._cw.vreg.schema, self._cw.lang)[etype]
       
   231         except KeyError:
       
   232             raise BadRQLQuery('%s is not a valid entity name' % etype)
       
   233 
       
   234     def _get_attribute_name(self, word, eschema):
       
   235         """check if the given word is matching an attribute of the given entity type,
       
   236         return it normalized if found or return it untransformed else
       
   237         """
       
   238         """Returns the attributes's name as stored in the DB"""
       
   239         # Need to convert from unicode to string (could be whatever)
       
   240         rtype = word.lower()
       
   241         # Find the entity name as stored in the DB
       
   242         translations = trmap(self._cw.vreg.config, self._cw.vreg.schema, self._cw.lang)
       
   243         try:
       
   244             translations = translations[rtype]
       
   245         except KeyError:
       
   246             raise BadRQLQuery('%s is not a valid attribute for %s entity type'
       
   247                               % (word, eschema))
       
   248         rtype = _get_approriate_translation(translations, eschema)
       
   249         if rtype is None:
       
   250             raise BadRQLQuery('%s is not a valid attribute for %s entity type'
       
   251                               % (word, eschema))
       
   252         return rtype
       
   253 
       
   254     def _one_word_query(self, word):
       
   255         """Specific process for one word query (case (1) of preprocess_rql)
       
   256         """
       
   257         # if this is an integer, then directly go to eid
       
   258         try:
       
   259             eid = int(word)
       
   260             return 'Any X WHERE X eid %(x)s', {'x': eid}, 'x'
       
   261         except ValueError:
       
   262             etype = self._get_entity_type(word)
       
   263             return '%s %s' % (etype, etype[0]),
       
   264 
       
   265     def _complete_rql(self, searchstr, etype, rtype=None, var=None, searchattr=None):
       
   266         searchop = ''
       
   267         if '%' in searchstr:
       
   268             if rtype:
       
   269                 possible_etypes = self._cw.vreg.schema.rschema(rtype).objects(etype)
       
   270             else:
       
   271                 possible_etypes = [self._cw.vreg.schema.eschema(etype)]
       
   272             if searchattr or len(possible_etypes) == 1:
       
   273                 searchattr = searchattr or possible_etypes[0].main_attribute()
       
   274                 searchop = 'LIKE '
       
   275         searchattr = searchattr or 'has_text'
       
   276         if var is None:
       
   277             var = etype[0]
       
   278         return '%s %s %s%%(text)s' % (var, searchattr, searchop)
       
   279 
       
   280     def _two_words_query(self, word1, word2):
       
   281         """Specific process for two words query (case (2) of preprocess_rql)
       
   282         """
       
   283         etype = self._get_entity_type(word1)
       
   284         # this is a valid RQL query : ("Person X", or "Person TMP1")
       
   285         if len(word2) == 1 and word2.isupper():
       
   286             return '%s %s' % (etype, word2),
       
   287         # else, suppose it's a shortcut like : Person Smith
       
   288         restriction = self._complete_rql(word2, etype)
       
   289         if ' has_text ' in restriction:
       
   290             rql = '%s %s ORDERBY FTIRANK(%s) DESC WHERE %s' % (
       
   291                 etype, etype[0], etype[0], restriction)
       
   292         else:
       
   293             rql = '%s %s WHERE %s' % (
       
   294                 etype, etype[0], restriction)
       
   295         return rql, {'text': word2}
       
   296 
       
   297     def _three_words_query(self, word1, word2, word3):
       
   298         """Specific process for three words query (case (3) of preprocess_rql)
       
   299         """
       
   300         etype = self._get_entity_type(word1)
       
   301         eschema = self._cw.vreg.schema.eschema(etype)
       
   302         rtype = self._get_attribute_name(word2, eschema)
       
   303         # expand shortcut if rtype is a non final relation
       
   304         if not self._cw.vreg.schema.rschema(rtype).final:
       
   305             return self._expand_shortcut(etype, rtype, word3)
       
   306         if '%' in word3:
       
   307             searchop = 'LIKE '
       
   308         else:
       
   309             searchop = ''
       
   310         rql = '%s %s WHERE %s' % (etype, etype[0],
       
   311                                   self._complete_rql(word3, etype, searchattr=rtype))
       
   312         return rql, {'text': word3}
       
   313 
       
   314     def _expand_shortcut(self, etype, rtype, searchstr):
       
   315         """Expands shortcut queries on a non final relation to use has_text or
       
   316         the main attribute (according to possible entity type) if '%' is used in the
       
   317         search word
       
   318 
       
   319         Transforms : 'person worksat IBM' into
       
   320         'Personne P WHERE P worksAt C, C has_text "IBM"'
       
   321         """
       
   322         # check out all possilbe entity types for the relation represented
       
   323         # by 'rtype'
       
   324         mainvar = etype[0]
       
   325         searchvar = mainvar  + '1'
       
   326         restriction = self._complete_rql(searchstr, etype, rtype=rtype,
       
   327                                          var=searchvar)
       
   328         if ' has_text ' in restriction:
       
   329             rql =  ('%s %s ORDERBY FTIRANK(%s) DESC '
       
   330                     'WHERE %s %s %s, %s' % (etype, mainvar, searchvar,
       
   331                                             mainvar, rtype, searchvar, # P worksAt C
       
   332                                             restriction))
       
   333         else:
       
   334             rql =  ('%s %s WHERE %s %s %s, %s' % (etype, mainvar,
       
   335                                             mainvar, rtype, searchvar, # P worksAt C
       
   336                                             restriction))
       
   337         return rql, {'text': searchstr}
       
   338 
       
   339 
       
   340     def _quoted_words_query(self, ori_rql):
       
   341         """Specific process when there's a "quoted" part
       
   342         """
       
   343         m = QUOTED_SRE.match(ori_rql)
       
   344         # if there's no quoted part, then no special pre-processing to do
       
   345         if m is None:
       
   346             raise BadRQLQuery("unable to handle request %r" % ori_rql)
       
   347         left_words = m.group(1).split()
       
   348         quoted_part = m.group(3)
       
   349         # Case (1) : Company "My own company"
       
   350         if len(left_words) == 1:
       
   351             try:
       
   352                 word1 = left_words[0]
       
   353                 return self._two_words_query(word1, quoted_part)
       
   354             except BadRQLQuery as error:
       
   355                 raise BadRQLQuery("unable to handle request %r" % ori_rql)
       
   356         # Case (2) : Company name "My own company";
       
   357         elif len(left_words) == 2:
       
   358             word1, word2 = left_words
       
   359             return self._three_words_query(word1, word2, quoted_part)
       
   360             # return ori_rql
       
   361         raise BadRQLQuery("unable to handle request %r" % ori_rql)
       
   362 
       
   363 
       
   364 
       
   365 class FullTextTranslator(BaseQueryProcessor):
       
   366     priority = 10
       
   367     name = 'text'
       
   368 
       
   369     def preprocess_query(self, uquery):
       
   370         """suppose it's a plain text query"""
       
   371         return 'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s', {'text': uquery}
       
   372 
       
   373 
       
   374 
       
   375 class MagicSearchComponent(Component):
       
   376     __regid__  = 'magicsearch'
       
   377     def __init__(self, req, rset=None):
       
   378         super(MagicSearchComponent, self).__init__(req, rset=rset)
       
   379         processors = []
       
   380         self.by_name = {}
       
   381         for processorcls in self._cw.vreg['components']['magicsearch_processor']:
       
   382             # instantiation needed
       
   383             processor = processorcls(self._cw)
       
   384             processors.append(processor)
       
   385             if processor.name is not None:
       
   386                 assert not processor.name in self.by_name
       
   387                 self.by_name[processor.name.lower()] = processor
       
   388         self.processors = sorted(processors, key=lambda x: x.priority)
       
   389 
       
   390     def process_query(self, uquery):
       
   391         assert isinstance(uquery, text_type)
       
   392         try:
       
   393             procname, query = uquery.split(':', 1)
       
   394             proc = self.by_name[procname.strip().lower()]
       
   395             uquery = query.strip()
       
   396         except Exception:
       
   397             # use processor chain
       
   398             unauthorized = None
       
   399             for proc in self.processors:
       
   400                 try:
       
   401                     return proc.process_query(uquery)
       
   402                 # FIXME : we don't want to catch any exception type here !
       
   403                 except (RQLSyntaxError, BadRQLQuery):
       
   404                     pass
       
   405                 except Unauthorized as ex:
       
   406                     unauthorized = ex
       
   407                     continue
       
   408                 except Exception as ex:
       
   409                     LOGGER.debug('%s: %s', ex.__class__.__name__, ex)
       
   410                     continue
       
   411             if unauthorized:
       
   412                 raise unauthorized
       
   413         else:
       
   414             # explicitly specified processor: don't try to catch the exception
       
   415             return proc.process_query(uquery)
       
   416         raise BadRQLQuery(self._cw._('sorry, the server is unable to handle this query'))
       
   417 
       
   418 
       
   419 
       
   420 ## RQL suggestions builder ####################################################
       
   421 class RQLSuggestionsBuilder(Component):
       
   422     """main entry point is `build_suggestions()` which takes
       
   423     an incomplete RQL query and returns a list of suggestions to complete
       
   424     the query.
       
   425 
       
   426     This component is enabled by default and is used to provide autocompletion
       
   427     in the RQL search bar. If you don't want this feature in your application,
       
   428     just unregister it or make it unselectable.
       
   429 
       
   430     .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions
       
   431     .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set
       
   432     .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes
       
   433     .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations
       
   434     .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary
       
   435     """
       
   436     __regid__ = 'rql.suggestions'
       
   437 
       
   438     #: maximum number of results to fetch when suggesting attribute values
       
   439     attr_value_limit = 20
       
   440 
       
   441     def build_suggestions(self, user_rql):
       
   442         """return a list of suggestions to complete `user_rql`
       
   443 
       
   444         :param user_rql: an incomplete RQL query
       
   445         """
       
   446         req = self._cw
       
   447         try:
       
   448             if 'WHERE' not in user_rql: # don't try to complete if there's no restriction
       
   449                 return []
       
   450             variables, restrictions = [part.strip() for part in user_rql.split('WHERE', 1)]
       
   451             if ',' in restrictions:
       
   452                 restrictions, incomplete_part = restrictions.rsplit(',', 1)
       
   453                 user_rql = '%s WHERE %s' % (variables, restrictions)
       
   454             else:
       
   455                 restrictions, incomplete_part = '', restrictions
       
   456                 user_rql = variables
       
   457             select = parse(user_rql, print_errors=False).children[0]
       
   458             req.vreg.rqlhelper.annotate(select)
       
   459             req.vreg.solutions(req, select, {})
       
   460             if restrictions:
       
   461                 return ['%s, %s' % (user_rql, suggestion)
       
   462                         for suggestion in self.rql_build_suggestions(select, incomplete_part)]
       
   463             else:
       
   464                 return ['%s WHERE %s' % (user_rql, suggestion)
       
   465                         for suggestion in self.rql_build_suggestions(select, incomplete_part)]
       
   466         except Exception as exc: # we never want to crash
       
   467             self.debug('failed to build suggestions: %s', exc)
       
   468             return []
       
   469 
       
   470     ## actual completion entry points #########################################
       
   471     def rql_build_suggestions(self, select, incomplete_part):
       
   472         """
       
   473         :param select: the annotated select node (rql syntax tree)
       
   474         :param incomplete_part: the part of the rql query that needs
       
   475                                 to be completed, (e.g. ``X is Pr``, ``X re``)
       
   476         """
       
   477         chunks = incomplete_part.split(None, 2)
       
   478         if not chunks: # nothing to complete
       
   479             return []
       
   480         if len(chunks) == 1: # `incomplete` looks like "MYVAR"
       
   481             return self._complete_rqlvar(select, *chunks)
       
   482         elif len(chunks) == 2: # `incomplete` looks like "MYVAR some_rel"
       
   483             return self._complete_rqlvar_and_rtype(select, *chunks)
       
   484         elif len(chunks) == 3: # `incomplete` looks like "MYVAR some_rel something"
       
   485             return self._complete_relation_object(select, *chunks)
       
   486         else: # would be anything else, hard to decide what to do here
       
   487             return []
       
   488 
       
   489     # _complete_* methods are considered private, at least while the API
       
   490     # isn't stabilized.
       
   491     def _complete_rqlvar(self, select, rql_var):
       
   492         """return suggestions for "variable only" incomplete_part
       
   493 
       
   494         as in :
       
   495 
       
   496         - Any X WHERE X
       
   497         - Any X WHERE X is Project, Y
       
   498         - etc.
       
   499         """
       
   500         return ['%s %s %s' % (rql_var, rtype, dest_var)
       
   501                 for rtype, dest_var in self.possible_relations(select, rql_var)]
       
   502 
       
   503     def _complete_rqlvar_and_rtype(self, select, rql_var, user_rtype):
       
   504         """return suggestions for "variable + rtype" incomplete_part
       
   505 
       
   506         as in :
       
   507 
       
   508         - Any X WHERE X is
       
   509         - Any X WHERE X is Person, X firstn
       
   510         - etc.
       
   511         """
       
   512         # special case `user_type` == 'is', return every possible type.
       
   513         if user_rtype == 'is':
       
   514             return self._complete_is_relation(select, rql_var)
       
   515         else:
       
   516             return ['%s %s %s' % (rql_var, rtype, dest_var)
       
   517                     for rtype, dest_var in self.possible_relations(select, rql_var)
       
   518                     if rtype.startswith(user_rtype)]
       
   519 
       
   520     def _complete_relation_object(self, select, rql_var, user_rtype, user_value):
       
   521         """return suggestions for "variable + rtype + some_incomplete_value"
       
   522 
       
   523         as in :
       
   524 
       
   525         - Any X WHERE X is Per
       
   526         - Any X WHERE X is Person, X firstname "
       
   527         - Any X WHERE X is Person, X firstname "Pa
       
   528         - etc.
       
   529         """
       
   530         # special case `user_type` == 'is', return every possible type.
       
   531         if user_rtype == 'is':
       
   532             return self._complete_is_relation(select, rql_var, user_value)
       
   533         elif user_value:
       
   534             if user_value[0] in ('"', "'"):
       
   535                 # if finished string, don't suggest anything
       
   536                 if len(user_value) > 1 and user_value[-1] == user_value[0]:
       
   537                     return []
       
   538                 user_value = user_value[1:]
       
   539                 return ['%s %s "%s"' % (rql_var, user_rtype, value)
       
   540                         for value in self.vocabulary(select, rql_var,
       
   541                                                      user_rtype, user_value)]
       
   542         return []
       
   543 
       
   544     def _complete_is_relation(self, select, rql_var, prefix=''):
       
   545         """return every possible types for rql_var
       
   546 
       
   547         :param prefix: if specified, will only return entity types starting
       
   548                        with the specified value.
       
   549         """
       
   550         return ['%s is %s' % (rql_var, etype)
       
   551                 for etype in self.possible_etypes(select, rql_var, prefix)]
       
   552 
       
   553     def etypes_suggestion_set(self):
       
   554         """returns the list of possible entity types to suggest
       
   555 
       
   556         The default is to return any non-final entity type available
       
   557         in the schema.
       
   558 
       
   559         Can be overridden for instance if an application decides
       
   560         to restrict this list to a meaningful set of business etypes.
       
   561         """
       
   562         schema = self._cw.vreg.schema
       
   563         return set(eschema.type for eschema in schema.entities() if not eschema.final)
       
   564 
       
   565     def possible_etypes(self, select, rql_var, prefix=''):
       
   566         """return all possible etypes for `rql_var`
       
   567 
       
   568         The returned list will always be a subset of meth:`etypes_suggestion_set`
       
   569 
       
   570         :param select: the annotated select node (rql syntax tree)
       
   571         :param rql_var: the variable name for which we want to know possible types
       
   572         :param prefix: if specified, will only return etypes starting with it
       
   573         """
       
   574         available_etypes = self.etypes_suggestion_set()
       
   575         possible_etypes = set()
       
   576         for sol in select.solutions:
       
   577             if rql_var in sol and sol[rql_var] in available_etypes:
       
   578                 possible_etypes.add(sol[rql_var])
       
   579         if not possible_etypes:
       
   580             # `Any X WHERE X is Person, Y is`
       
   581             # -> won't have a solution, need to give all etypes
       
   582             possible_etypes = available_etypes
       
   583         return sorted(etype for etype in possible_etypes if etype.startswith(prefix))
       
   584 
       
   585     def possible_relations(self, select, rql_var, include_meta=False):
       
   586         """returns a list of couple (rtype, dest_var) for each possible
       
   587         relations with `rql_var` as subject.
       
   588 
       
   589         ``dest_var`` will be picked among availabel variables if types match,
       
   590         otherwise a new one will be created.
       
   591         """
       
   592         schema = self._cw.vreg.schema
       
   593         relations = set()
       
   594         untyped_dest_var = next(rqlvar_maker(defined=select.defined_vars))
       
   595         # for each solution
       
   596         # 1. find each possible relation
       
   597         # 2. for each relation:
       
   598         #    2.1. if the relation is meta, skip it
       
   599         #    2.2. for each possible destination type, pick up possible
       
   600         #         variables for this type or use a new one
       
   601         for sol in select.solutions:
       
   602             etype = sol[rql_var]
       
   603             sol_by_types = {}
       
   604             for varname, var_etype in sol.items():
       
   605                 # don't push subject var to avoid "X relation X" suggestion
       
   606                 if varname != rql_var:
       
   607                     sol_by_types.setdefault(var_etype, []).append(varname)
       
   608             for rschema in schema[etype].subject_relations():
       
   609                 if include_meta or not rschema.meta:
       
   610                     for dest in rschema.objects(etype):
       
   611                         for varname in sol_by_types.get(dest.type, (untyped_dest_var,)):
       
   612                             suggestion = (rschema.type, varname)
       
   613                             if suggestion not in relations:
       
   614                                 relations.add(suggestion)
       
   615         return sorted(relations)
       
   616 
       
   617     def vocabulary(self, select, rql_var, user_rtype, rtype_incomplete_value):
       
   618         """return acceptable vocabulary for `rql_var` + `user_rtype` in `select`
       
   619 
       
   620         Vocabulary is either found from schema (Yams) definition or
       
   621         directly from database.
       
   622         """
       
   623         schema = self._cw.vreg.schema
       
   624         vocab = []
       
   625         for sol in select.solutions:
       
   626             # for each solution :
       
   627             # - If a vocabulary constraint exists on `rql_var+user_rtype`, use it
       
   628             #   to define possible values
       
   629             # - Otherwise, query the database to fetch available values from
       
   630             #   database (limiting results to `self.attr_value_limit`)
       
   631             try:
       
   632                 eschema = schema.eschema(sol[rql_var])
       
   633                 rdef = eschema.rdef(user_rtype)
       
   634             except KeyError: # unknown relation
       
   635                 continue
       
   636             cstr = rdef.constraint_by_interface(IVocabularyConstraint)
       
   637             if cstr is not None:
       
   638                 # a vocabulary is found, use it
       
   639                 vocab += [value for value in cstr.vocabulary()
       
   640                           if value.startswith(rtype_incomplete_value)]
       
   641             elif rdef.final:
       
   642                 # no vocab, query database to find possible value
       
   643                 vocab_rql = 'DISTINCT Any V LIMIT %s WHERE X is %s, X %s V' % (
       
   644                     self.attr_value_limit, eschema.type, user_rtype)
       
   645                 vocab_kwargs = {}
       
   646                 if rtype_incomplete_value:
       
   647                     vocab_rql += ', X %s LIKE %%(value)s' % user_rtype
       
   648                     vocab_kwargs['value'] = u'%s%%' % rtype_incomplete_value
       
   649                 vocab += [value for value, in
       
   650                           self._cw.execute(vocab_rql, vocab_kwargs)]
       
   651         return sorted(set(vocab))
       
   652 
       
   653 
       
   654 
       
   655 @ajaxfunc(output_type='json')
       
   656 def rql_suggest(self):
       
   657     rql_builder = self._cw.vreg['components'].select_or_none('rql.suggestions', self._cw)
       
   658     if rql_builder:
       
   659         return rql_builder.build_suggestions(self._cw.form['term'])
       
   660     return []