[server] eschema_eid needs a connection, not a session
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""a query processor to handle quick search shortcuts for cubicweb"""__docformat__="restructuredtext en"importrefromloggingimportgetLoggerfromyams.interfacesimportIVocabularyConstraintfromrqlimportRQLSyntaxError,BadRQLQuery,parsefromrql.utilsimportrqlvar_makerfromrql.nodesimportRelationfromcubicwebimportUnauthorizedfromcubicweb.viewimportComponentfromcubicweb.web.views.ajaxcontrollerimportajaxfuncLOGGER=getLogger('cubicweb.magicsearch')def_get_approriate_translation(translations_found,eschema):"""return the first (should be the only one) possible translation according to the given entity type """# get the list of all attributes / relations for this kind of entityexisting_relations=set(eschema.subject_relations())consistent_translations=translations_found&existing_relationsiflen(consistent_translations)==0:returnNonereturnconsistent_translations.pop()deftranslate_rql_tree(rqlst,translations,schema):"""Try to translate each relation in the RQL syntax tree :type rqlst: `rql.stmts.Statement` :param rqlst: the RQL syntax tree :type translations: dict :param translations: the reverted l10n dict :type schema: `cubicweb.schema.Schema` :param schema: the instance's schema """# var_types is used as a map : var_name / var_typevartypes={}# ambiguous_nodes is used as a map : relation_node / (var_name, available_translations)ambiguous_nodes={}# For each relation node, check if it's a localized relation name# If it's a localized name, then use the original relation name, else# keep the existing relation nameforrelationinrqlst.get_nodes(Relation):rtype=relation.r_typelhs,rhs=relation.get_variable_parts()ifrtype=='is':try:etype=translations[rhs.value]rhs.value=etypeexceptKeyError:# If no translation found, leave the entity type as isetype=rhs.value# Memorize variable's typevartypes[lhs.name]=etypeelse:try:translation_set=translations[rtype]exceptKeyError:pass# If no translation found, leave the relation type as iselse:# Only one possible translation, no ambiguityiflen(translation_set)==1:relation.r_type=iter(translations[rtype]).next()# More than 1 possible translation => resolve it laterelse:ambiguous_nodes[relation]=(lhs.name,translation_set)ifambiguous_nodes:resolve_ambiguities(vartypes,ambiguous_nodes,schema)defresolve_ambiguities(var_types,ambiguous_nodes,schema):"""Tries to resolve remaining ambiguities for translation /!\ An ambiguity is when two different string can be localized with the same string A simple example: - 'name' in a company context will be localized as 'nom' in French - but ... 'surname' will also be localized as 'nom' :type var_types: dict :param var_types: a map : var_name / var_type :type ambiguous_nodes: dict :param ambiguous_nodes: a map : relation_node / (var_name, available_translations) :type schema: `cubicweb.schema.Schema` :param schema: the instance's schema """# Now, try to resolve ambiguous translationsforrelation,(var_name,translations_found)inambiguous_nodes.items():try:vartype=var_types[var_name]exceptKeyError:continue# Get schema for this entity typeeschema=schema.eschema(vartype)rtype=_get_approriate_translation(translations_found,eschema)ifrtypeisNone:continuerelation.r_type=rtypeQUOTED_SRE=re.compile(r'(.*?)(["\'])(.+?)\2')TRANSLATION_MAPS={}deftrmap(config,schema,lang):try:returnTRANSLATION_MAPS[lang]exceptKeyError:assertlanginconfig.translations,'%s%s'%(lang,config.translations)tr,ctxtr=config.translations[lang]langmap={}foretypeinschema.entities():etype=str(etype)langmap[tr(etype).capitalize()]=etypelangmap[etype.capitalize()]=etypeforrtypeinschema.relations():rtype=str(rtype)langmap.setdefault(tr(rtype).lower(),set()).add(rtype)langmap.setdefault(rtype,set()).add(rtype)TRANSLATION_MAPS[lang]=langmapreturnlangmapclassBaseQueryProcessor(Component):__abstract__=True__regid__='magicsearch_processor'# set something if you want explicit component search facility for the# componentname=Nonedefprocess_query(self,uquery):args=self.preprocess_query(uquery)try:returnself._cw.execute(*args)finally:# rollback necessary to avoid leaving the connection in a bad stateself._cw.cnx.rollback()defpreprocess_query(self,uquery):raiseNotImplementedError()classDoNotPreprocess(BaseQueryProcessor):"""this one returns the raw query and should be placed in first position of the chain """name='rql'priority=0defpreprocess_query(self,uquery):returnuquery,classQueryTranslator(BaseQueryProcessor):""" parses through rql and translates into schema language entity names and attributes """priority=2defpreprocess_query(self,uquery):rqlst=parse(uquery,print_errors=False)schema=self._cw.vreg.schema# rql syntax tree will be modified in place if necessarytranslate_rql_tree(rqlst,trmap(self._cw.vreg.config,schema,self._cw.lang),schema)returnrqlst.as_string(),classQSPreProcessor(BaseQueryProcessor):"""Quick search preprocessor preprocessing query in shortcut form to their RQL form """priority=4defpreprocess_query(self,uquery):"""try to get rql from an unicode query string"""args=Nonetry:# Process as if there was a quoted partargs=self._quoted_words_query(uquery)## No quoted partexceptBadRQLQuery:words=uquery.split()iflen(words)==1:args=self._one_word_query(*words)eliflen(words)==2:args=self._two_words_query(*words)eliflen(words)==3:args=self._three_words_query(*words)else:raisereturnargsdef_get_entity_type(self,word):"""check if the given word is matching an entity type, return it if it's the case or raise BadRQLQuery if not """etype=word.capitalize()try:returntrmap(self._cw.vreg.config,self._cw.vreg.schema,self._cw.lang)[etype]exceptKeyError:raiseBadRQLQuery('%s is not a valid entity name'%etype)def_get_attribute_name(self,word,eschema):"""check if the given word is matching an attribute of the given entity type, return it normalized if found or return it untransformed else """"""Returns the attributes's name as stored in the DB"""# Need to convert from unicode to string (could be whatever)rtype=word.lower()# Find the entity name as stored in the DBtranslations=trmap(self._cw.vreg.config,self._cw.vreg.schema,self._cw.lang)try:translations=translations[rtype]exceptKeyError:raiseBadRQLQuery('%s is not a valid attribute for %s entity type'%(word,eschema))rtype=_get_approriate_translation(translations,eschema)ifrtypeisNone:raiseBadRQLQuery('%s is not a valid attribute for %s entity type'%(word,eschema))returnrtypedef_one_word_query(self,word):"""Specific process for one word query (case (1) of preprocess_rql) """# if this is an integer, then directly go to eidtry:eid=int(word)return'Any X WHERE X eid %(x)s',{'x':eid},'x'exceptValueError:etype=self._get_entity_type(word)return'%s%s'%(etype,etype[0]),def_complete_rql(self,searchstr,etype,rtype=None,var=None,searchattr=None):searchop=''if'%'insearchstr:ifrtype:possible_etypes=self._cw.vreg.schema.rschema(rtype).objects(etype)else:possible_etypes=[self._cw.vreg.schema.eschema(etype)]ifsearchattrorlen(possible_etypes)==1:searchattr=searchattrorpossible_etypes[0].main_attribute()searchop='LIKE 'searchattr=searchattror'has_text'ifvarisNone:var=etype[0]return'%s%s%s%%(text)s'%(var,searchattr,searchop)def_two_words_query(self,word1,word2):"""Specific process for two words query (case (2) of preprocess_rql) """etype=self._get_entity_type(word1)# this is a valid RQL query : ("Person X", or "Person TMP1")iflen(word2)==1andword2.isupper():return'%s%s'%(etype,word2),# else, suppose it's a shortcut like : Person Smithrestriction=self._complete_rql(word2,etype)if' has_text 'inrestriction:rql='%s%s ORDERBY FTIRANK(%s) DESC WHERE %s'%(etype,etype[0],etype[0],restriction)else:rql='%s%s WHERE %s'%(etype,etype[0],restriction)returnrql,{'text':word2}def_three_words_query(self,word1,word2,word3):"""Specific process for three words query (case (3) of preprocess_rql) """etype=self._get_entity_type(word1)eschema=self._cw.vreg.schema.eschema(etype)rtype=self._get_attribute_name(word2,eschema)# expand shortcut if rtype is a non final relationifnotself._cw.vreg.schema.rschema(rtype).final:returnself._expand_shortcut(etype,rtype,word3)if'%'inword3:searchop='LIKE 'else:searchop=''rql='%s%s WHERE %s'%(etype,etype[0],self._complete_rql(word3,etype,searchattr=rtype))returnrql,{'text':word3}def_expand_shortcut(self,etype,rtype,searchstr):"""Expands shortcut queries on a non final relation to use has_text or the main attribute (according to possible entity type) if '%' is used in the search word Transforms : 'person worksat IBM' into 'Personne P WHERE P worksAt C, C has_text "IBM"' """# check out all possilbe entity types for the relation represented# by 'rtype'mainvar=etype[0]searchvar=mainvar+'1'restriction=self._complete_rql(searchstr,etype,rtype=rtype,var=searchvar)if' has_text 'inrestriction:rql=('%s%s ORDERBY FTIRANK(%s) DESC ''WHERE %s%s%s, %s'%(etype,mainvar,searchvar,mainvar,rtype,searchvar,# P worksAt Crestriction))else:rql=('%s%s WHERE %s%s%s, %s'%(etype,mainvar,mainvar,rtype,searchvar,# P worksAt Crestriction))returnrql,{'text':searchstr}def_quoted_words_query(self,ori_rql):"""Specific process when there's a "quoted" part """m=QUOTED_SRE.match(ori_rql)# if there's no quoted part, then no special pre-processing to doifmisNone:raiseBadRQLQuery("unable to handle request %r"%ori_rql)left_words=m.group(1).split()quoted_part=m.group(3)# Case (1) : Company "My own company"iflen(left_words)==1:try:word1=left_words[0]returnself._two_words_query(word1,quoted_part)exceptBadRQLQueryaserror:raiseBadRQLQuery("unable to handle request %r"%ori_rql)# Case (2) : Company name "My own company";eliflen(left_words)==2:word1,word2=left_wordsreturnself._three_words_query(word1,word2,quoted_part)# return ori_rqlraiseBadRQLQuery("unable to handle request %r"%ori_rql)classFullTextTranslator(BaseQueryProcessor):priority=10name='text'defpreprocess_query(self,uquery):"""suppose it's a plain text query"""return'Any X ORDERBY FTIRANK(X) DESC WHERE X has_text %(text)s',{'text':uquery}classMagicSearchComponent(Component):__regid__='magicsearch'def__init__(self,req,rset=None):super(MagicSearchComponent,self).__init__(req,rset=rset)processors=[]self.by_name={}forprocessorclsinself._cw.vreg['components']['magicsearch_processor']:# instantiation neededprocessor=processorcls(self._cw)processors.append(processor)ifprocessor.nameisnotNone:assertnotprocessor.nameinself.by_nameself.by_name[processor.name.lower()]=processorself.processors=sorted(processors,key=lambdax:x.priority)defprocess_query(self,uquery):assertisinstance(uquery,unicode)try:procname,query=uquery.split(':',1)proc=self.by_name[procname.strip().lower()]uquery=query.strip()exceptException:# use processor chainunauthorized=Noneforprocinself.processors:try:returnproc.process_query(uquery)# FIXME : we don't want to catch any exception type here !except(RQLSyntaxError,BadRQLQuery):passexceptUnauthorizedasex:unauthorized=excontinueexceptExceptionasex:LOGGER.debug('%s: %s',ex.__class__.__name__,ex)continueifunauthorized:raiseunauthorizedelse:# explicitly specified processor: don't try to catch the exceptionreturnproc.process_query(uquery)raiseBadRQLQuery(self._cw._('sorry, the server is unable to handle this query'))## RQL suggestions builder ####################################################classRQLSuggestionsBuilder(Component):"""main entry point is `build_suggestions()` which takes an incomplete RQL query and returns a list of suggestions to complete the query. This component is enabled by default and is used to provide autocompletion in the RQL search bar. If you don't want this feature in your application, just unregister it or make it unselectable. .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.build_suggestions .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.etypes_suggestion_set .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_etypes .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.possible_relations .. automethod:: cubicweb.web.views.magicsearch.RQLSuggestionsBuilder.vocabulary """__regid__='rql.suggestions'#: maximum number of results to fetch when suggesting attribute valuesattr_value_limit=20defbuild_suggestions(self,user_rql):"""return a list of suggestions to complete `user_rql` :param user_rql: an incomplete RQL query """req=self._cwtry:if'WHERE'notinuser_rql:# don't try to complete if there's no restrictionreturn[]variables,restrictions=[part.strip()forpartinuser_rql.split('WHERE',1)]if','inrestrictions:restrictions,incomplete_part=restrictions.rsplit(',',1)user_rql='%s WHERE %s'%(variables,restrictions)else:restrictions,incomplete_part='',restrictionsuser_rql=variablesselect=parse(user_rql,print_errors=False).children[0]req.vreg.rqlhelper.annotate(select)req.vreg.solutions(req,select,{})ifrestrictions:return['%s, %s'%(user_rql,suggestion)forsuggestioninself.rql_build_suggestions(select,incomplete_part)]else:return['%s WHERE %s'%(user_rql,suggestion)forsuggestioninself.rql_build_suggestions(select,incomplete_part)]exceptExceptionasexc:# we never want to crashself.debug('failed to build suggestions: %s',exc)return[]## actual completion entry points #########################################defrql_build_suggestions(self,select,incomplete_part):""" :param select: the annotated select node (rql syntax tree) :param incomplete_part: the part of the rql query that needs to be completed, (e.g. ``X is Pr``, ``X re``) """chunks=incomplete_part.split(None,2)ifnotchunks:# nothing to completereturn[]iflen(chunks)==1:# `incomplete` looks like "MYVAR"returnself._complete_rqlvar(select,*chunks)eliflen(chunks)==2:# `incomplete` looks like "MYVAR some_rel"returnself._complete_rqlvar_and_rtype(select,*chunks)eliflen(chunks)==3:# `incomplete` looks like "MYVAR some_rel something"returnself._complete_relation_object(select,*chunks)else:# would be anything else, hard to decide what to do herereturn[]# _complete_* methods are considered private, at least while the API# isn't stabilized.def_complete_rqlvar(self,select,rql_var):"""return suggestions for "variable only" incomplete_part as in : - Any X WHERE X - Any X WHERE X is Project, Y - etc. """return['%s%s%s'%(rql_var,rtype,dest_var)forrtype,dest_varinself.possible_relations(select,rql_var)]def_complete_rqlvar_and_rtype(self,select,rql_var,user_rtype):"""return suggestions for "variable + rtype" incomplete_part as in : - Any X WHERE X is - Any X WHERE X is Person, X firstn - etc. """# special case `user_type` == 'is', return every possible type.ifuser_rtype=='is':returnself._complete_is_relation(select,rql_var)else:return['%s%s%s'%(rql_var,rtype,dest_var)forrtype,dest_varinself.possible_relations(select,rql_var)ifrtype.startswith(user_rtype)]def_complete_relation_object(self,select,rql_var,user_rtype,user_value):"""return suggestions for "variable + rtype + some_incomplete_value" as in : - Any X WHERE X is Per - Any X WHERE X is Person, X firstname " - Any X WHERE X is Person, X firstname "Pa - etc. """# special case `user_type` == 'is', return every possible type.ifuser_rtype=='is':returnself._complete_is_relation(select,rql_var,user_value)elifuser_value:ifuser_value[0]in('"',"'"):# if finished string, don't suggest anythingiflen(user_value)>1anduser_value[-1]==user_value[0]:return[]user_value=user_value[1:]return['%s%s "%s"'%(rql_var,user_rtype,value)forvalueinself.vocabulary(select,rql_var,user_rtype,user_value)]return[]def_complete_is_relation(self,select,rql_var,prefix=''):"""return every possible types for rql_var :param prefix: if specified, will only return entity types starting with the specified value. """return['%s is %s'%(rql_var,etype)foretypeinself.possible_etypes(select,rql_var,prefix)]defetypes_suggestion_set(self):"""returns the list of possible entity types to suggest The default is to return any non-final entity type available in the schema. Can be overridden for instance if an application decides to restrict this list to a meaningful set of business etypes. """schema=self._cw.vreg.schemareturnset(eschema.typeforeschemainschema.entities()ifnoteschema.final)defpossible_etypes(self,select,rql_var,prefix=''):"""return all possible etypes for `rql_var` The returned list will always be a subset of meth:`etypes_suggestion_set` :param select: the annotated select node (rql syntax tree) :param rql_var: the variable name for which we want to know possible types :param prefix: if specified, will only return etypes starting with it """available_etypes=self.etypes_suggestion_set()possible_etypes=set()forsolinselect.solutions:ifrql_varinsolandsol[rql_var]inavailable_etypes:possible_etypes.add(sol[rql_var])ifnotpossible_etypes:# `Any X WHERE X is Person, Y is`# -> won't have a solution, need to give all etypespossible_etypes=available_etypesreturnsorted(etypeforetypeinpossible_etypesifetype.startswith(prefix))defpossible_relations(self,select,rql_var,include_meta=False):"""returns a list of couple (rtype, dest_var) for each possible relations with `rql_var` as subject. ``dest_var`` will be picked among availabel variables if types match, otherwise a new one will be created. """schema=self._cw.vreg.schemarelations=set()untyped_dest_var=rqlvar_maker(defined=select.defined_vars).next()# for each solution# 1. find each possible relation# 2. for each relation:# 2.1. if the relation is meta, skip it# 2.2. for each possible destination type, pick up possible# variables for this type or use a new oneforsolinselect.solutions:etype=sol[rql_var]sol_by_types={}forvarname,var_etypeinsol.items():# don't push subject var to avoid "X relation X" suggestionifvarname!=rql_var:sol_by_types.setdefault(var_etype,[]).append(varname)forrschemainschema[etype].subject_relations():ifinclude_metaornotrschema.meta:fordestinrschema.objects(etype):forvarnameinsol_by_types.get(dest.type,(untyped_dest_var,)):suggestion=(rschema.type,varname)ifsuggestionnotinrelations:relations.add(suggestion)returnsorted(relations)defvocabulary(self,select,rql_var,user_rtype,rtype_incomplete_value):"""return acceptable vocabulary for `rql_var` + `user_rtype` in `select` Vocabulary is either found from schema (Yams) definition or directly from database. """schema=self._cw.vreg.schemavocab=[]forsolinselect.solutions:# for each solution :# - If a vocabulary constraint exists on `rql_var+user_rtype`, use it# to define possible values# - Otherwise, query the database to fetch available values from# database (limiting results to `self.attr_value_limit`)try:eschema=schema.eschema(sol[rql_var])rdef=eschema.rdef(user_rtype)exceptKeyError:# unknown relationcontinuecstr=rdef.constraint_by_interface(IVocabularyConstraint)ifcstrisnotNone:# a vocabulary is found, use itvocab+=[valueforvalueincstr.vocabulary()ifvalue.startswith(rtype_incomplete_value)]elifrdef.final:# no vocab, query database to find possible valuevocab_rql='DISTINCT Any V LIMIT %s WHERE X is %s, X %s V'%(self.attr_value_limit,eschema.type,user_rtype)vocab_kwargs={}ifrtype_incomplete_value:vocab_rql+=', X %s LIKE %%(value)s'%user_rtypevocab_kwargs['value']='%s%%'%rtype_incomplete_valuevocab+=[valueforvalue,inself._cw.execute(vocab_rql,vocab_kwargs)]returnsorted(set(vocab))@ajaxfunc(output_type='json')defrql_suggest(self):rql_builder=self._cw.vreg['components'].select_or_none('rql.suggestions',self._cw)ifrql_builder:returnrql_builder.build_suggestions(self._cw.form['term'])return[]