author | Julien Cristau <julien.cristau@logilab.fr> |
Fri, 10 Jul 2015 18:18:58 +0200 | |
changeset 10536 | 887c6eef8077 |
parent 10215 | 96085e379066 |
child 10662 | 10942ed172de |
permissions | -rw-r--r-- |
# copyright 2003-2013 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 is functions that can be called from the javascript world. To register a new remote function, either decorate your function with 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:`~cubicweb.web.views.ajaxcontroller.AjaxFunction` and implement 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" from warnings import warn from functools import partial from logilab.common.date import strptime from logilab.common.registry import yes from logilab.common.deprecation import deprecated from cubicweb import ObjectNotFound, NoSelectableObject from cubicweb.appobject import AppObject from cubicweb.utils import json, json_dumps, UStringIO from cubicweb.uilib import exc_message from cubicweb.web import RemoteCallFailed, DirectResponse from cubicweb.web.controller import Controller from cubicweb.web.views import vid_from_rset from cubicweb.web.views import basecontrollers def optional_kwargs(extraargs): if extraargs is None: return {} # we receive unicode keys which is not supported by the **syntax return dict((str(key), value) for key, value in extraargs.iteritems()) class AjaxController(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' def publish(self, rset=None): self._cw.ajax_request = True try: fname = self._cw.form['fname'] except KeyError: raise RemoteCallFailed('no method specified') # 1/ check first for old-style (JSonController) ajax func for bw compat try: func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func func = partial(func, self) except AttributeError: # 2/ check for new-style (AjaxController) ajax func try: func = self._cw.vreg['ajax-func'].select(fname, self._cw) except ObjectNotFound: raise RemoteCallFailed('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 argument args = self._cw.form.get('arg', ()) if not isinstance(args, (list, tuple)): args = (args,) try: args = [json.loads(arg) for arg in args] except ValueError as exc: self.exception('error while decoding json arguments for ' 'js_%s: %s (err: %s)', fname, args, exc) raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) try: result = func(*args) except (RemoteCallFailed, DirectResponse): raise except Exception as exc: self.exception('an exception occurred while calling js_%s(%s): %s', fname, args, exc) raise RemoteCallFailed(exc_message(exc, self._cw.encoding)) if result is None: return '' # get unicode on @htmlize methods, encoded string on @jsonize methods elif isinstance(result, unicode): return result.encode(self._cw.encoding) return result class AjaxFunction(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__ = True check_pageid = False output_type = None @staticmethod def _rebuild_posted_form(names, values, action=None): form = {} for name, value in zip(names, values): # remove possible __action_xxx inputs if name.startswith('__action'): if action is None: # strip '__action_' to get the actual action name action = name[9:] continue # form.setdefault(name, []).append(value) if name in form: curvalue = form[name] if isinstance(curvalue, list): curvalue.append(value) else: form[name] = [curvalue, value] else: form[name] = value # simulate click on __action_%s button to help the controller if action: form['__action_%s' % action] = u'whatever' return form def validate_form(self, action, names, values): self._cw.form = self._rebuild_posted_form(names, values, action) return basecontrollers._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() if rql.startswith('rql:'): rql = rql[4:] if rocheck: self._cw.ensure_ro_rql(rql) try: return self._cw.execute(rql, args) except Exception as ex: self.exception("error in _exec(rql=%s): %s", rql, ex) return None return None def _call_view(self, view, paginate=False, **kwargs): divid = self._cw.form.get('divid') # we need to call pagination before with the stream set try: stream = view.set_stream() except AttributeError: stream = UStringIO() kwargs['w'] = stream.write assert not paginate if divid == 'pageContent': # ensure divid isn't reused by the view (e.g. table view) del self._cw.form['divid'] # mimick main template behaviour stream.write(u'<div id="pageContent">') vtitle = self._cw.form.get('vtitle') if vtitle: stream.write(u'<h1 class="vtitle">%s</h1>\n' % vtitle) paginate = True nav_html = UStringIO() if paginate and not view.handle_pagination: view.paginate(w=nav_html.write) stream.write(nav_html.getvalue()) if divid == 'pageContent': stream.write(u'<div id="contentmain">') view.render(**kwargs) extresources = self._cw.html_headers.getvalue(skiphead=True) if extresources: stream.write(u'<div class="ajaxHtmlHead">\n') # XXX use a widget? stream.write(extresources) stream.write(u'</div>\n') if divid == 'pageContent': stream.write(u'</div>%s</div>' % nav_html.getvalue()) return stream.getvalue() def _ajaxfunc_factory(implementation, selector=yes(), _output_type=None, _check_pageid=False, regid=None): """converts a standard python function into an AjaxFunction appobject""" class AnAjaxFunc(AjaxFunction): __regid__ = regid or implementation.__name__ __select__ = selector output_type = _output_type check_pageid = _check_pageid def serialize(self, content): if self.output_type is None: return content elif self.output_type == 'xhtml': self._cw.set_content_type(self._cw.html_content_type()) return ''.join((u'<div>', content.strip(), u'</div>')) elif self.output_type == 'json': self._cw.set_content_type('application/json') return json_dumps(content) raise RemoteCallFailed('no serializer found for output type %s' % self.output_type) def __call__(self, *args, **kwargs): if self.check_pageid: data = self._cw.session.data.get(self._cw.pageid) if data is None: raise RemoteCallFailed(self._cw._('pageid-not-found')) return self.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__ = AnAjaxFunc return implementation def ajaxfunc(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')) if implementation is None: 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') def view(self): # XXX try to use the page-content template req = self._cw rql = req.form.get('rql') if rql: rset = self._exec(rql) elif 'eid' in req.form: rset = self._cw.eid_rset(req.form['eid']) else: rset = None vid = req.form.get('vid') or vid_from_rset(req, rset, self._cw.vreg.schema) try: viewobj = self._cw.vreg['views'].select(vid, req, rset=rset) except NoSelectableObject: vid = req.form.get('fallbackvid', 'noresult') viewobj = self._cw.vreg['views'].select(vid, req, rset=rset) viewobj.set_http_cache_headers() if req.is_client_cache_valid(): return '' return self._call_view(viewobj, paginate=req.form.pop('paginate', False)) @ajaxfunc(output_type='xhtml') def component(self, compid, rql, registry='components', extraargs=None): if rql: 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') return self._call_view(comp, **optional_kwargs(extraargs)) @ajaxfunc(output_type='xhtml') def render(self, registry, oid, eid=None, selectargs=None, renderargs=None): if eid is not None: rset = self._cw.eid_rset(eid) # XXX set row=0 elif self._cw.form.get('rql'): rset = self._cw.execute(self._cw.form['rql']) else: rset = None viewobj = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **optional_kwargs(selectargs)) return self._call_view(viewobj, **optional_kwargs(renderargs)) @ajaxfunc(output_type='json') def i18n(self, msgids): """returns the translation of `msgid`""" return [self._cw._(msgid) for msgid in msgids] @ajaxfunc(output_type='json') def format_date(self, strdate): """returns the formatted date for `msgid`""" date = strptime(strdate, '%Y-%m-%d %H:%M:%S') return self._cw.format_date(date) @ajaxfunc(output_type='json') def external_resource(self, resource): """returns the URL of the external resource named `resource`""" return self._cw.uiprops[resource] @ajaxfunc def unload_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") def set_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) @ajaxfunc def delete_relation(self, rtype, subjeid, objeid): rql = 'DELETE S %s O WHERE S eid %%(s)s, O eid %%(o)s' % rtype self._cw.execute(rql, {'s': subjeid, 'o': objeid}) @ajaxfunc def add_relation(self, rtype, subjeid, objeid): rql = 'SET S %s O WHERE S eid %%(s)s, O eid %%(o)s' % rtype self._cw.execute(rql, {'s': subjeid, 'o': objeid})