--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/ajaxcontroller.py Fri Dec 16 12:30:12 2011 +0100
@@ -0,0 +1,452 @@
+# copyright 2003-2011 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-funcs`` 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 :ref:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator:
+
+.. sourcecode:: python
+
+ from cubicweb.selectors 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` 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 functools import partial
+
+from logilab.common.date import strptime
+from logilab.common.deprecation import deprecated
+
+from cubicweb import ObjectNotFound, NoSelectableObject
+from cubicweb.appobject import AppObject
+from cubicweb.selectors import yes
+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')
+ try:
+ func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+ except ObjectNotFound:
+ # function not found in the registry, inspect JSonController for
+ # backward compatibility
+ try:
+ func = getattr(basecontrollers.JSonController, 'js_%s' % fname).im_func
+ func = partial(func, self)
+ except AttributeError:
+ raise RemoteCallFailed('no %s method' % fname)
+ else:
+ self.warning('remote function %s found on JSonController, '
+ 'use AjaxFunction / @ajaxfunc instead', fname)
+ except NoSelectableObject:
+ raise RemoteCallFailed('method %s not available in this context'
+ % fname)
+ # 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, 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, 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, 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((self._cw.document_surrounding_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__
+ return AnAjaxFunc
+
+
+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()
+ req.validate_cache()
+ 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(output_type='json', check_pageid=True)
+def user_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]
+ except KeyError:
+ self.warning('unable to find user callback %s', cbname)
+ return None
+ return cb(self._cw)
+
+
+@ajaxfunc
+def unregister_user_callback(self, cbname):
+ """unregister user callback `cbname`"""
+ self._cw.unregister_callback(self._cw.pageid, cbname)
+
+@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)