# 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/>.## (disable pylint msg for client obj access to protected member as in obj._cw)# pylint: disable=W0212"""The ``ajaxcontroller`` module defines the :class:`AjaxController`controller and the ``ajax-func`` cubicweb registry... autoclass:: cubicweb.web.views.ajaxcontroller.AjaxController :members:``ajax-funcs`` registry hosts exposed remote functions, that isfunctions that can be called from the javascript world.To register a new remote function, either decorate your functionwith the :func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:.. sourcecode:: python from cubicweb.predicates import mactch_user_groups from cubicweb.web.views.ajaxcontroller import ajaxfunc @ajaxfunc(output_type='json', selector=match_user_groups('managers')) def list_users(self): return [u for (u,) in self._cw.execute('Any L WHERE U login L')]or inherit from :class:`cubicwbe.web.views.ajaxcontroller.AjaxFunction` andimplement the ``__call__`` method:.. sourcecode:: python from cubicweb.web.views.ajaxcontroller import AjaxFunction class ListUser(AjaxFunction): __regid__ = 'list_users' # __regid__ is the name of the exposed function __select__ = match_user_groups('managers') output_type = 'json' def __call__(self): return [u for (u, ) in self._cw.execute('Any L WHERE U login L')].. autoclass:: cubicweb.web.views.ajaxcontroller.AjaxFunction :members:.. autofunction:: cubicweb.web.views.ajaxcontroller.ajaxfunc"""__docformat__="restructuredtext en"fromwarningsimportwarnfromfunctoolsimportpartialfromlogilab.common.dateimportstrptimefromlogilab.common.registryimportyesfromlogilab.common.deprecationimportdeprecatedfromcubicwebimportObjectNotFound,NoSelectableObjectfromcubicweb.appobjectimportAppObjectfromcubicweb.utilsimportjson,json_dumps,UStringIOfromcubicweb.uilibimportexc_messagefromcubicweb.webimportRemoteCallFailed,DirectResponsefromcubicweb.web.controllerimportControllerfromcubicweb.web.viewsimportvid_from_rsetfromcubicweb.web.viewsimportbasecontrollersdefoptional_kwargs(extraargs):ifextraargsisNone:return{}# we receive unicode keys which is not supported by the **syntaxreturndict((str(key),value)forkey,valueinextraargs.iteritems())classAjaxController(Controller):"""AjaxController handles ajax remote calls from javascript The following javascript function call: .. sourcecode:: javascript var d = asyncRemoteExec('foo', 12, "hello"); d.addCallback(function(result) { alert('server response is: ' + result); }); will generate an ajax HTTP GET on the following url:: BASE_URL/ajax?fname=foo&arg=12&arg="hello" The AjaxController controller will therefore be selected to handle those URLs and will itself select the :class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` matching the *fname* parameter. """__regid__='ajax'defpublish(self,rset=None):self._cw.ajax_request=Truetry:fname=self._cw.form['fname']exceptKeyError:raiseRemoteCallFailed('no method specified')# 1/ check first for old-style (JSonController) ajax func for bw compattry:func=getattr(basecontrollers.JSonController,'js_%s'%fname).im_funcfunc=partial(func,self)exceptAttributeError:# 2/ check for new-style (AjaxController) ajax functry:func=self._cw.vreg['ajax-func'].select(fname,self._cw)exceptObjectNotFound:raiseRemoteCallFailed('no %s method'%fname)else:warn('[3.15] remote function %s found on JSonController, ''use AjaxFunction / @ajaxfunc instead'%fname,DeprecationWarning,stacklevel=2)# no <arg> attribute means the callback takes no argumentargs=self._cw.form.get('arg',())ifnotisinstance(args,(list,tuple)):args=(args,)try:args=[json.loads(arg)forarginargs]exceptValueError,exc:self.exception('error while decoding json arguments for ''js_%s: %s (err: %s)',fname,args,exc)raiseRemoteCallFailed(exc_message(exc,self._cw.encoding))try:result=func(*args)except(RemoteCallFailed,DirectResponse):raiseexceptException,exc:self.exception('an exception occurred while calling js_%s(%s): %s',fname,args,exc)raiseRemoteCallFailed(exc_message(exc,self._cw.encoding))ifresultisNone:return''# get unicode on @htmlize methods, encoded string on @jsonize methodselifisinstance(result,unicode):returnresult.encode(self._cw.encoding)returnresultclassAjaxFunction(AppObject):""" Attributes on this base class are: :attr: `check_pageid`: make sure the pageid received is valid before proceeding :attr: `output_type`: - *None*: no processing, no change on content-type - *json*: serialize with `json_dumps` and set *application/json* content-type - *xhtml*: wrap result in an XML node and forces HTML / XHTML content-type (use ``_cw.html_content_type()``) """__registry__='ajax-func'__select__=yes()__abstract__=Truecheck_pageid=Falseoutput_type=None@staticmethoddef_rebuild_posted_form(names,values,action=None):form={}forname,valueinzip(names,values):# remove possible __action_xxx inputsifname.startswith('__action'):ifactionisNone:# strip '__action_' to get the actual action nameaction=name[9:]continue# form.setdefault(name, []).append(value)ifnameinform:curvalue=form[name]ifisinstance(curvalue,list):curvalue.append(value)else:form[name]=[curvalue,value]else:form[name]=value# simulate click on __action_%s button to help the controllerifaction:form['__action_%s'%action]=u'whatever'returnformdefvalidate_form(self,action,names,values):self._cw.form=self._rebuild_posted_form(names,values,action)returnbasecontrollers._validate_form(self._cw,self._cw.vreg)def_exec(self,rql,args=None,rocheck=True):"""json mode: execute RQL and return resultset as json"""rql=rql.strip()ifrql.startswith('rql:'):rql=rql[4:]ifrocheck:self._cw.ensure_ro_rql(rql)try:returnself._cw.execute(rql,args)exceptException,ex:self.exception("error in _exec(rql=%s): %s",rql,ex)returnNonereturnNonedef_call_view(self,view,paginate=False,**kwargs):divid=self._cw.form.get('divid')# we need to call pagination before with the stream settry:stream=view.set_stream()exceptAttributeError:stream=UStringIO()kwargs['w']=stream.writeassertnotpaginateifdivid=='pageContent':# ensure divid isn't reused by the view (e.g. table view)delself._cw.form['divid']# mimick main template behaviourstream.write(u'<div id="pageContent">')vtitle=self._cw.form.get('vtitle')ifvtitle:stream.write(u'<h1 class="vtitle">%s</h1>\n'%vtitle)paginate=Truenav_html=UStringIO()ifpaginateandnotview.handle_pagination:view.paginate(w=nav_html.write)stream.write(nav_html.getvalue())ifdivid=='pageContent':stream.write(u'<div id="contentmain">')view.render(**kwargs)extresources=self._cw.html_headers.getvalue(skiphead=True)ifextresources:stream.write(u'<div class="ajaxHtmlHead">\n')# XXX use a widget ?stream.write(extresources)stream.write(u'</div>\n')ifdivid=='pageContent':stream.write(u'</div>%s</div>'%nav_html.getvalue())returnstream.getvalue()def_ajaxfunc_factory(implementation,selector=yes(),_output_type=None,_check_pageid=False,regid=None):"""converts a standard python function into an AjaxFunction appobject"""classAnAjaxFunc(AjaxFunction):__regid__=regidorimplementation.__name____select__=selectoroutput_type=_output_typecheck_pageid=_check_pageiddefserialize(self,content):ifself.output_typeisNone:returncontentelifself.output_type=='xhtml':self._cw.set_content_type(self._cw.html_content_type())return''.join((self._cw.document_surrounding_div(),content.strip(),u'</div>'))elifself.output_type=='json':self._cw.set_content_type('application/json')returnjson_dumps(content)raiseRemoteCallFailed('no serializer found for output type %s'%self.output_type)def__call__(self,*args,**kwargs):ifself.check_pageid:data=self._cw.session.data.get(self._cw.pageid)ifdataisNone:raiseRemoteCallFailed(self._cw._('pageid-not-found'))returnself.serialize(implementation(self,*args,**kwargs))AnAjaxFunc.__name__=implementation.__name__# make sure __module__ refers to the original module otherwise# vreg.register(obj) will ignore ``obj``.AnAjaxFunc.__module__=implementation.__module__# relate the ``implementation`` object to its wrapper appobject# will be used by e.g.:# import base_module# @ajaxfunc# def foo(self):# return 42# assert foo(object) == 42# vreg.register_and_replace(foo, base_module.older_foo)implementation.__appobject__=AnAjaxFuncreturnimplementationdefajaxfunc(implementation=None,selector=yes(),output_type=None,check_pageid=False,regid=None):"""promote a standard function to an ``AjaxFunction`` appobject. All parameters are optional: :param selector: a custom selector object if needed, default is ``yes()`` :param output_type: either None, 'json' or 'xhtml' to customize output content-type. Default is None :param check_pageid: whether the function requires a valid `pageid` or not to proceed. Default is False. :param regid: a custom __regid__ for the created ``AjaxFunction`` object. Default is to keep the wrapped function name. ``ajaxfunc`` can be used both as a standalone decorator: .. sourcecode:: python @ajaxfunc def my_function(self): return 42 or as a parametrizable decorator: .. sourcecode:: python @ajaxfunc(output_type='json') def my_function(self): return 42 """# if used as a parametrized decorator (e.g. @ajaxfunc(output_type='json'))ifimplementationisNone:def_decorator(func):return_ajaxfunc_factory(func,selector=selector,_output_type=output_type,_check_pageid=check_pageid,regid=regid)return_decorator# else, used as a standalone decorator (i.e. @ajaxfunc)return_ajaxfunc_factory(implementation,selector=selector,_output_type=output_type,_check_pageid=check_pageid,regid=regid)################################################################################ Cubicweb remote functions for : ## - appobject rendering ## - user / page session data management ################################################################################@ajaxfunc(output_type='xhtml')defview(self):# XXX try to use the page-content templatereq=self._cwrql=req.form.get('rql')ifrql:rset=self._exec(rql)elif'eid'inreq.form:rset=self._cw.eid_rset(req.form['eid'])else:rset=Nonevid=req.form.get('vid')orvid_from_rset(req,rset,self._cw.vreg.schema)try:viewobj=self._cw.vreg['views'].select(vid,req,rset=rset)exceptNoSelectableObject:vid=req.form.get('fallbackvid','noresult')viewobj=self._cw.vreg['views'].select(vid,req,rset=rset)viewobj.set_http_cache_headers()req.validate_cache()returnself._call_view(viewobj,paginate=req.form.pop('paginate',False))@ajaxfunc(output_type='xhtml')defcomponent(self,compid,rql,registry='components',extraargs=None):ifrql:rset=self._exec(rql)else:rset=None# XXX while it sounds good, addition of the try/except below cause pb:# when filtering using facets return an empty rset, the edition box# isn't anymore selectable, as expected. The pb is that with the# try/except below, we see a "an error occurred" message in the ui, while# we don't see it without it. Proper fix would probably be to deal with# this by allowing facet handling code to tell to js_component that such# error is expected and should'nt be reported.#try:comp=self._cw.vreg[registry].select(compid,self._cw,rset=rset,**optional_kwargs(extraargs))#except NoSelectableObject:# raise RemoteCallFailed('unselectable')returnself._call_view(comp,**optional_kwargs(extraargs))@ajaxfunc(output_type='xhtml')defrender(self,registry,oid,eid=None,selectargs=None,renderargs=None):ifeidisnotNone:rset=self._cw.eid_rset(eid)# XXX set row=0elifself._cw.form.get('rql'):rset=self._cw.execute(self._cw.form['rql'])else:rset=Noneviewobj=self._cw.vreg[registry].select(oid,self._cw,rset=rset,**optional_kwargs(selectargs))returnself._call_view(viewobj,**optional_kwargs(renderargs))@ajaxfunc(output_type='json')defi18n(self,msgids):"""returns the translation of `msgid`"""return[self._cw._(msgid)formsgidinmsgids]@ajaxfunc(output_type='json')defformat_date(self,strdate):"""returns the formatted date for `msgid`"""date=strptime(strdate,'%Y-%m-%d %H:%M:%S')returnself._cw.format_date(date)@ajaxfunc(output_type='json')defexternal_resource(self,resource):"""returns the URL of the external resource named `resource`"""returnself._cw.uiprops[resource]@ajaxfunc(output_type='json',check_pageid=True)defuser_callback(self,cbname):"""execute the previously registered user callback `cbname`. If matching callback is not found, return None """page_data=self._cw.session.data.get(self._cw.pageid,{})try:cb=page_data[cbname]exceptKeyError:self.warning('unable to find user callback %s',cbname)returnNonereturncb(self._cw)@ajaxfuncdefunregister_user_callback(self,cbname):"""unregister user callback `cbname`"""self._cw.unregister_callback(self._cw.pageid,cbname)@ajaxfuncdefunload_page_data(self):"""remove user's session data associated to current pageid"""self._cw.session.data.pop(self._cw.pageid,None)@ajaxfunc(output_type='json')@deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")defset_cookie(self,cookiename,cookievalue):"""generates the Set-Cookie HTTP reponse header corresponding to `cookiename` / `cookievalue`. """cookiename,cookievalue=str(cookiename),str(cookievalue)self._cw.set_cookie(cookiename,cookievalue)