# HG changeset patch # User Sylvain Thénault # Date 1324572415 -3600 # Node ID 9dc024fea040b42ad13f300a4b620b4e6574153e # Parent f9ebb6d1abc3bc45967aa784d9a6abfa0e1171cb# Parent c33092c4086f7cd3bc5780120f32528a8063ce6a backport stable diff -r c33092c4086f -r 9dc024fea040 __pkginfo__.py --- a/__pkginfo__.py Thu Dec 22 17:44:50 2011 +0100 +++ b/__pkginfo__.py Thu Dec 22 17:46:55 2011 +0100 @@ -52,7 +52,7 @@ 'Twisted': '', # XXX graphviz # server dependencies - 'logilab-database': '>= 1.8.1', + 'logilab-database': '>= 1.8.2', 'pysqlite': '>= 2.5.5', # XXX install pysqlite2 } diff -r c33092c4086f -r 9dc024fea040 debian/control --- a/debian/control Thu Dec 22 17:44:50 2011 +0100 +++ b/debian/control Thu Dec 22 17:46:55 2011 +0100 @@ -35,7 +35,7 @@ Conflicts: cubicweb-multisources Replaces: cubicweb-multisources Provides: cubicweb-multisources -Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.1), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 +Depends: ${misc:Depends}, ${python:Depends}, cubicweb-common (= ${source:Version}), cubicweb-ctl (= ${source:Version}), python-logilab-database (>= 1.8.2), cubicweb-postgresql-support | cubicweb-mysql-support | python-pysqlite2 Recommends: pyro (<< 4.0.0), cubicweb-documentation (= ${source:Version}) Description: server part of the CubicWeb framework CubicWeb is a semantic web application framework. diff -r c33092c4086f -r 9dc024fea040 devtools/testlib.py --- a/devtools/testlib.py Thu Dec 22 17:44:50 2011 +0100 +++ b/devtools/testlib.py Thu Dec 22 17:46:55 2011 +0100 @@ -605,7 +605,7 @@ dump = json.dumps args = [dump(arg) for arg in args] req = self.request(fname=fname, pageid='123', arg=args) - ctrl = self.vreg['controllers'].select('json', req) + ctrl = self.vreg['controllers'].select('ajax', req) return ctrl.publish(), req def app_publish(self, req, path='view'): diff -r c33092c4086f -r 9dc024fea040 doc/book/en/devrepo/datamodel/definition.rst --- a/doc/book/en/devrepo/datamodel/definition.rst Thu Dec 22 17:44:50 2011 +0100 +++ b/doc/book/en/devrepo/datamodel/definition.rst Thu Dec 22 17:46:55 2011 +0100 @@ -186,6 +186,9 @@ * `default`: default value of the attribute. In case of date types, the values which could be used correspond to the RQL keywords `TODAY` and `NOW`. +* `metadata`: Is also accepted as an argument of the attribute contructor. It is + not really an attribute property. see `Metadata`_ for details. + Properties for `String` attributes: * `fulltextindexed`: boolean indicating if the attribute is part of @@ -567,17 +570,41 @@ In any case, identifiers starting with "CW" or "cw" are reserved for internal use by the framework. + .. _Metadata: + + Some attribute using the name of another attribute as prefix are considered + metadata. For example, if an EntityType have both a ``data`` and + ``data_format`` attribute, ``data_format`` is view as the ``format`` metadata + of ``data``. Later the :meth:`cw_attr_metadata` method will allow you to fetch + metadata related to an attribute. There are only three valid metadata names: + ``format``, ``encoding`` and ``name``. + The name of the Python attribute corresponds to the name of the attribute or the relation in *CubicWeb* application. An attribute is defined in the schema as follows:: - attr_name = attr_type(properties) + attr_name = AttrType(*properties, metadata={}) + +where + +* `AttrType`: is one of the type listed in EntityType_, + +* `properties`: is a list of the attribute needs to satisfy (see `Properties`_ + for more details), -where `attr_type` is one of the type listed above and `properties` is -a list of the attribute needs to satisfy (see `Properties`_ -for more details). +* `metadata`: is a dictionary of meta attributes related to ``attr_name``. + Dictionary keys are the name of the meta attribute. Dictionary values + attributes objects (like the content of ``AttrType``). For each entry of the + metadata dictionary a ``_ = `` attribute is + automaticaly added to the EntityType. see `Metadata`_ section for details + about valid key. + + + --- + +While building your schema * it is possible to use the attribute `meta` to flag an entity type as a `meta` (e.g. used to describe/categorize other entities) diff -r c33092c4086f -r 9dc024fea040 doc/book/en/devweb/ajax.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/book/en/devweb/ajax.rst Thu Dec 22 17:46:55 2011 +0100 @@ -0,0 +1,12 @@ +.. _ajax: + +Ajax +---- + +CubicWeb provides a few helpers to facilitate *javascript <-> python* communications. + +You can, for instance, register some python functions that will become +callable from javascript through ajax calls. All the ajax URLs are handled +by the ``AjaxController`` controller. + +.. automodule:: cubicweb.web.views.ajaxcontroller diff -r c33092c4086f -r 9dc024fea040 doc/book/en/devweb/controllers.rst --- a/doc/book/en/devweb/controllers.rst Thu Dec 22 17:44:50 2011 +0100 +++ b/doc/book/en/devweb/controllers.rst Thu Dec 22 17:46:55 2011 +0100 @@ -22,10 +22,6 @@ :exc:`NoSelectableObject` errors that may bubble up to its entry point, in an end-user-friendly way (but other programming errors will slip through) -* the JSon controller (same module) provides services for Ajax calls, - typically using JSON as a serialization format for input, and - sometimes using either JSON or XML for output; - * the JSonpController is a wrapper around the ``ViewController`` that provides jsonp_ services. Padding can be specified with the ``callback`` request parameter. Only *jsonexport* / *ejsonexport* @@ -36,10 +32,6 @@ * the Login/Logout controllers make effective user login or logout requests -.. warning:: - - JsonController will probably be renamed into AjaxController soon since - it has nothing to do with json per se. .. _jsonp: http://en.wikipedia.org/wiki/JSONP @@ -64,6 +56,13 @@ * the MailBugReport controller (web/views/basecontrollers.py) allows to quickly have a `reportbug` feature in one's application +* the :class:`cubicweb.web.views.ajaxcontroller.AjaxController` + (:mod:`cubicweb.web.views.ajaxcontroller`) provides + services for Ajax calls, typically using JSON as a serialization format + for input, and sometimes using either JSON or XML for output. See + :ref:`ajax` chapter for more information. + + Registration ++++++++++++ diff -r c33092c4086f -r 9dc024fea040 doc/book/en/devweb/index.rst --- a/doc/book/en/devweb/index.rst Thu Dec 22 17:44:50 2011 +0100 +++ b/doc/book/en/devweb/index.rst Thu Dec 22 17:46:55 2011 +0100 @@ -12,6 +12,7 @@ request views/index rtags + ajax js css edition/index diff -r c33092c4086f -r 9dc024fea040 doc/book/en/devweb/js.rst --- a/doc/book/en/devweb/js.rst Thu Dec 22 17:44:50 2011 +0100 +++ b/doc/book/en/devweb/js.rst Thu Dec 22 17:46:55 2011 +0100 @@ -72,21 +72,22 @@ A simple example with asyncRemoteExec ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the python side, we have to extend the ``BaseController`` -class. The ``@jsonize`` decorator ensures that the return value of the -method is encoded as JSON data. By construction, the JSonController -inputs everything in JSON format. +On the python side, we have to define an +:class:`cubicweb.web.views.ajaxcontroller.AjaxFunction` object. The +simplest way to do that is to use the +:func:`cubicweb.web.views.ajaxcontroller.ajaxfunc` decorator (for more +details on this, refer to :ref:`ajax`). .. sourcecode: python - from cubicweb.web.views.basecontrollers import JSonController, jsonize + from cubicweb.web.views.ajaxcontroller import ajaxfunc - @monkeypatch(JSonController) - @jsonize + # serialize output to json to get it back easily on the javascript side + @ajaxfunc(output_type='json') def js_say_hello(self, name): return u'hello %s' % name -In the javascript side, we do the asynchronous call. Notice how it +On the javascript side, we do the asynchronous call. Notice how it creates a `deferred` object. Proper treatment of the return value or error handling has to be done through the addCallback and addErrback methods. @@ -131,7 +132,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The server side implementation of `reloadComponent` is the -js_component method of the JSonController. +:func:`cubicweb.web.views.ajaxcontroller.component` *AjaxFunction* appobject. The following function implements a two-steps method to delete a standard bookmark and refresh the UI, while keeping the UI responsive. @@ -166,7 +167,8 @@ * `url` (mandatory) should be a complete url (typically referencing - the JSonController, but this is not strictly mandatory) + the :class:`cubicweb.web.views.ajaxcontroller.AjaxController`, + but this is not strictly mandatory) * `data` (optional) is a dictionary of values given to the controller specified through an `url` argument; some keys may have a @@ -204,25 +206,23 @@ .. sourcecode:: python - from cubicweb import typed_eid - from cubicweb.web.views.basecontrollers import JSonController, xhtmlize + from cubicweb.web.views.ajaxcontroller import ajaxfunc - @monkeypatch(JSonController) - @xhtmlize + @ajaxfunc(output_type='xhtml') def js_frob_status(self, eid, frobname): - entity = self._cw.entity_from_eid(typed_eid(eid)) + entity = self._cw.entity_from_eid(eid) return entity.view('frob', name=frobname) .. sourcecode:: javascript - function update_some_div(divid, eid, frobname) { + function updateSomeDiv(divid, eid, frobname) { var params = {fname:'frob_status', eid: eid, frobname:frobname}; jQuery('#'+divid).loadxhtml(JSON_BASE_URL, params, 'post'); } In this example, the url argument is the base json url of a cube instance (it should contain something like -`http://myinstance/json?`). The actual JSonController method name is +`http://myinstance/ajax?`). The actual AjaxController method name is encoded in the `params` dictionary using the `fname` key. A more real-life example @@ -250,7 +250,7 @@ w(u'') self._cw.add_onload(u""" jQuery('#lazy-%(vid)s').bind('%(event)s', function() { - load_now('#lazy-%(vid)s');});""" + loadNow('#lazy-%(vid)s');});""" % {'event': 'load_%s' % vid, 'vid': vid}) This creates a `div` with a specific event associated to it. @@ -271,7 +271,7 @@ .. sourcecode:: javascript - function load_now(eltsel) { + function loadNow(eltsel) { var lazydiv = jQuery(eltsel); lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl')); } @@ -306,18 +306,77 @@ """trigger an event that will force immediate loading of the view on dom readyness """ - self._cw.add_onload("trigger_load('%s');" % vid) + self._cw.add_onload("triggerLoad('%s');" % vid) The browser-side definition follows. .. sourcecode:: javascript - function trigger_load(divid) { + function triggerLoad(divid) { jQuery('#lazy-' + divd).trigger('load_' + divid); } -.. XXX userCallback / user_callback +python/ajax dynamic callbacks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +CubicWeb provides a way to dynamically register a function and make it +callable from the javascript side. The typical use case for this is a +situation where you have everything at hand to implement an action +(whether it be performing a RQL query or executing a few python +statements) that you'd like to defer to a user click in the web +interface. In other words, generate an HTML ``\n' + STRICT_DOCTYPE + + u'
hello
') + + def test_monkeypatch_jsoncontroller_jsonize(self): + self.assertRaises(RemoteCallFailed, self.remote_call, 'foo') + @monkeypatch(JSonController) + @jsonize + def js_foo(self): + return 12 + res, req = self.remote_call('foo') + self.assertEqual(res, '12') if __name__ == '__main__': unittest_main() diff -r c33092c4086f -r 9dc024fea040 web/views/actions.py --- a/web/views/actions.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/actions.py Thu Dec 22 17:46:55 2011 +0100 @@ -130,7 +130,7 @@ params = self._cw.form.copy() for param in ('vid', '__message') + controller.NAV_FORM_PARAMETERS: params.pop(param, None) - if self._cw.json_request: + if self._cw.ajax_request: path = 'view' if self.cw_rset is not None: params = {'rql': self.cw_rset.printable_rql()} diff -r c33092c4086f -r 9dc024fea040 web/views/ajaxcontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/views/ajaxcontroller.py Thu Dec 22 17:46:55 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 . +# +# (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 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'
') + vtitle = self._cw.form.get('vtitle') + if vtitle: + stream.write(u'

%s

\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'
') + view.render(**kwargs) + extresources = self._cw.html_headers.getvalue(skiphead=True) + if extresources: + stream.write(u'
\n') # XXX use a widget ? + stream.write(extresources) + stream.write(u'
\n') + if divid == 'pageContent': + stream.write(u'
%s
' % 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'')) + 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) diff -r c33092c4086f -r 9dc024fea040 web/views/autoform.py --- a/web/views/autoform.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/autoform.py Thu Dec 22 17:46:55 2011 +0100 @@ -134,10 +134,11 @@ from cubicweb.selectors import ( match_kwargs, match_form_params, non_final_entity, specified_etype_implements) -from cubicweb.utils import json_dumps +from cubicweb.utils import json, json_dumps from cubicweb.web import (stdmsgs, uicfg, eid_param, form as f, formwidgets as fw, formfields as ff) from cubicweb.web.views import forms +from cubicweb.web.views.ajaxcontroller import ajaxfunc _AFS = uicfg.autoform_section _AFFK = uicfg.autoform_field_kwargs @@ -437,6 +438,70 @@ execute(rql, {'x': subj, 'y': obj}) +# ajax edition helpers ######################################################## +@ajaxfunc(output_type='xhtml', check_pageid=True) +def inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx): + view = self._cw.vreg['views'].select('inline-creation', self._cw, + etype=ttype, rtype=rtype, role=role, + peid=peid, petype=petype) + return self._call_view(view, i18nctx=i18nctx) + +@ajaxfunc(output_type='json') +def validate_form(self, action, names, values): + return self.validate_form(action, names, values) + +@ajaxfunc +def cancel_edition(self, errorurl): + """cancelling edition from javascript + + We need to clear associated req's data : + - errorurl + - pending insertions / deletions + """ + self._cw.cancel_edition(errorurl) + +@ajaxfunc(output_type='xhtml') +def reledit_form(self): + req = self._cw + args = dict((x, req.form[x]) + for x in ('formid', 'rtype', 'role', 'reload', 'action')) + rset = req.eid_rset(typed_eid(self._cw.form['eid'])) + try: + args['reload'] = json.loads(args['reload']) + except ValueError: # not true/false, an absolute url + assert args['reload'].startswith('http') + view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype']) + return self._call_view(view, **args) + + +def _add_pending(req, eidfrom, rel, eidto, kind): + key = 'pending_%s' % kind + pendings = req.session.data.setdefault(key, set()) + pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) + +def _remove_pending(req, eidfrom, rel, eidto, kind): + key = 'pending_%s' % kind + pendings = req.session.data[key] + pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) + +@ajaxfunc(output_type='json') +def remove_pending_insert(self, (eidfrom, rel, eidto)): + _remove_pending(self._cw, eidfrom, rel, eidto, 'insert') + +@ajaxfunc(output_type='json') +def add_pending_inserts(self, tripletlist): + for eidfrom, rel, eidto in tripletlist: + _add_pending(self._cw, eidfrom, rel, eidto, 'insert') + +@ajaxfunc(output_type='json') +def remove_pending_delete(self, (eidfrom, rel, eidto)): + _remove_pending(self._cw, eidfrom, rel, eidto, 'delete') + +@ajaxfunc(output_type='json') +def add_pending_delete(self, (eidfrom, rel, eidto)): + _add_pending(self._cw, eidfrom, rel, eidto, 'delete') + + class GenericRelationsWidget(fw.FieldWidget): def render(self, form, field, renderer): diff -r c33092c4086f -r 9dc024fea040 web/views/basecontrollers.py --- a/web/views/basecontrollers.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/basecontrollers.py Thu Dec 22 17:46:55 2011 +0100 @@ -22,20 +22,21 @@ __docformat__ = "restructuredtext en" _ = unicode -from logilab.common.date import strptime +from warnings import warn + from logilab.common.deprecation import deprecated from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError, AuthenticationError, typed_eid) -from cubicweb.utils import UStringIO, json, json_dumps -from cubicweb.uilib import exc_message -from cubicweb.selectors import authenticated_user, anonymous_user, match_form_params -from cubicweb.mail import format_mail -from cubicweb.web import Redirect, RemoteCallFailed, DirectResponse, facet +from cubicweb.utils import json_dumps +from cubicweb.selectors import (authenticated_user, anonymous_user, + match_form_params) +from cubicweb.web import Redirect, RemoteCallFailed from cubicweb.web.controller import Controller -from cubicweb.web.views import vid_from_rset, formrenderers +from cubicweb.web.views import vid_from_rset +@deprecated('jsonize is deprecated, use AjaxFunction appobjects instead') def jsonize(func): """decorator to sets correct content_type and calls `json_dumps` on results @@ -46,6 +47,7 @@ wrapper.__name__ = func.__name__ return wrapper +@deprecated('xhtmlize is deprecated, use AjaxFunction appobjects instead') def xhtmlize(func): """decorator to sets correct content_type and calls `xmlize` on results""" def wrapper(self, *args, **kwargs): @@ -56,6 +58,7 @@ wrapper.__name__ = func.__name__ return wrapper +@deprecated('check_pageid is deprecated, use AjaxFunction appobjects instead') def check_pageid(func): """decorator which checks the given pageid is found in the user's session data @@ -234,7 +237,7 @@ """ % (domid, callback, errback, jsargs, cbargs) def publish(self, rset=None): - self._cw.json_request = True + self._cw.ajax_request = True # XXX unclear why we have a separated controller here vs # js_validate_form on the json controller status, args, entity = _validate_form(self._cw, self._cw.vreg) @@ -242,339 +245,18 @@ self._cw.encoding) return self.response(domid, status, args, entity) -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 JSonController(Controller): __regid__ = 'json' def publish(self, rset=None): - """call js_* methods. Expected form keys: - - :fname: the method name without the js_ prefix - :args: arguments list (json) - - note: it's the responsability of js_* methods to set the correct - response content type - """ - self._cw.json_request = True - try: - fname = self._cw.form['fname'] - func = getattr(self, 'js_%s' % fname) - except KeyError: - raise RemoteCallFailed('no method specified') - except AttributeError: - raise RemoteCallFailed('no %s method' % fname) - # no 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 - - def _rebuild_posted_form(self, 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 _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'
') - vtitle = self._cw.form.get('vtitle') - if vtitle: - stream.write(u'

%s

\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'
') - view.render(**kwargs) - extresources = self._cw.html_headers.getvalue(skiphead=True) - if extresources: - stream.write(u'
\n') # XXX use a widget ? - stream.write(extresources) - stream.write(u'
\n') - if divid == 'pageContent': - stream.write(u'
%s
' % nav_html.getvalue()) - return stream.getvalue() - - @xhtmlize - def js_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: - view = self._cw.vreg['views'].select(vid, req, rset=rset) - except NoSelectableObject: - vid = req.form.get('fallbackvid', 'noresult') - view = self._cw.vreg['views'].select(vid, req, rset=rset) - self.validate_cache(view) - return self._call_view(view, paginate=req.form.pop('paginate', False)) - - @xhtmlize - def js_prop_widget(self, propkey, varname, tabindex=None): - """specific method for CWProperty handling""" - entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw) - entity.eid = varname - entity['pkey'] = propkey - form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity) - form.build_context() - vfield = form.field_by_name('value') - renderer = formrenderers.FormRenderer(self._cw) - return vfield.render(form, renderer, tabindex=tabindex) \ - + renderer.render_help(form, vfield) - - @xhtmlize - def js_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)) - - @xhtmlize - def js_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 - view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, - **optional_kwargs(selectargs)) - return self._call_view(view, **optional_kwargs(renderargs)) - - @check_pageid - @xhtmlize - def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx): - view = self._cw.vreg['views'].select('inline-creation', self._cw, - etype=ttype, rtype=rtype, role=role, - peid=peid, petype=petype) - return self._call_view(view, i18nctx=i18nctx) - - @jsonize - def js_validate_form(self, action, names, values): - return self.validate_form(action, names, values) - - def validate_form(self, action, names, values): - self._cw.form = self._rebuild_posted_form(names, values, action) - return _validate_form(self._cw, self._cw.vreg) - - @xhtmlize - def js_reledit_form(self): - req = self._cw - args = dict((x, req.form[x]) - for x in ('formid', 'rtype', 'role', 'reload', 'action')) - rset = req.eid_rset(typed_eid(self._cw.form['eid'])) - try: - args['reload'] = json.loads(args['reload']) - except ValueError: # not true/false, an absolute url - assert args['reload'].startswith('http') - view = req.vreg['views'].select('reledit', req, rset=rset, rtype=args['rtype']) - return self._call_view(view, **args) - - @jsonize - def js_i18n(self, msgids): - """returns the translation of `msgid`""" - return [self._cw._(msgid) for msgid in msgids] - - @jsonize - def js_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) - - @jsonize - def js_external_resource(self, resource): - """returns the URL of the external resource named `resource`""" - return self._cw.uiprops[resource] - - @check_pageid - @jsonize - def js_user_callback(self, cbname): - page_data = self._cw.session.data.get(self._cw.pageid, {}) - try: - cb = page_data[cbname] - except KeyError: - return None - return cb(self._cw) - - @jsonize - def js_filter_build_rql(self, names, values): - form = self._rebuild_posted_form(names, values) - self._cw.form = form - builder = facet.FilterRQLBuilder(self._cw) - return builder.build_rql() - - @jsonize - def js_filter_select_content(self, facetids, rql, mainvar): - # Union unsupported yet - select = self._cw.vreg.parse(self._cw, rql).children[0] - filtered_variable = facet.get_filtered_variable(select, mainvar) - facet.prepare_select(select, filtered_variable) - update_map = {} - for fid in facetids: - fobj = facet.get_facet(self._cw, fid, select, filtered_variable) - update_map[fid] = fobj.possible_values() - return update_map - - def js_unregister_user_callback(self, cbname): - self._cw.unregister_callback(self._cw.pageid, cbname) - - def js_unload_page_data(self): - self._cw.session.data.pop(self._cw.pageid, None) - - def js_cancel_edition(self, errorurl): - """cancelling edition from javascript - - We need to clear associated req's data : - - errorurl - - pending insertions / deletions - """ - self._cw.cancel_edition(errorurl) - - def js_delete_bookmark(self, beid): - rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s' - self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid}) - - def js_node_clicked(self, treeid, nodeeid): - """add/remove eid in treestate cookie""" - from cubicweb.web.views.treeview import treecookiename - cookies = self._cw.get_cookie() - statename = treecookiename(treeid) - treestate = cookies.get(statename) - if treestate is None: - self._cw.set_cookie(statename, nodeeid) - else: - marked = set(filter(None, treestate.value.split(':'))) - if nodeeid in marked: - marked.remove(nodeeid) - else: - marked.add(nodeeid) - self._cw.set_cookie(statename, ':'.join(marked)) - - @jsonize - @deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead") - def js_set_cookie(self, cookiename, cookievalue): - cookiename, cookievalue = str(cookiename), str(cookievalue) - self._cw.set_cookie(cookiename, cookievalue) - - # relations edition stuff ################################################## - - def _add_pending(self, eidfrom, rel, eidto, kind): - key = 'pending_%s' % kind - pendings = self._cw.session.data.setdefault(key, set()) - pendings.add( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) - - def _remove_pending(self, eidfrom, rel, eidto, kind): - key = 'pending_%s' % kind - pendings = self._cw.session.data[key] - pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) ) - - def js_remove_pending_insert(self, (eidfrom, rel, eidto)): - self._remove_pending(eidfrom, rel, eidto, 'insert') - - def js_add_pending_inserts(self, tripletlist): - for eidfrom, rel, eidto in tripletlist: - self._add_pending(eidfrom, rel, eidto, 'insert') - - def js_remove_pending_delete(self, (eidfrom, rel, eidto)): - self._remove_pending(eidfrom, rel, eidto, 'delete') - - def js_add_pending_delete(self, (eidfrom, rel, eidto)): - self._add_pending(eidfrom, rel, eidto, 'delete') + warn('[3.15] JSONController is deprecated, use AjaxController instead', + DeprecationWarning) + ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli) + return ajax_controller.publish(rset) # XXX move to massmailing - class MailBugReportController(Controller): __regid__ = 'reportbug' __select__ = match_form_params('description') diff -r c33092c4086f -r 9dc024fea040 web/views/baseviews.py --- a/web/views/baseviews.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/baseviews.py Thu Dec 22 17:46:55 2011 +0100 @@ -157,7 +157,7 @@ """:__regid__: *incontext* This view is used whenthe entity should be considered as displayed in its - context. By default it produces the result of `textincontext` wrapped in a + context. By default it produces the result of ``entity.dc_title()`` wrapped in a link leading to the primary view of the entity. """ __regid__ = 'incontext' @@ -165,18 +165,15 @@ def cell_call(self, row, col): entity = self.cw_rset.get_entity(row, col) desc = cut(entity.dc_description(), 50) - self.w(u'
' % ( - xml_escape(entity.absolute_url()), xml_escape(desc))) - self.w(xml_escape(self._cw.view('textincontext', self.cw_rset, - row=row, col=col))) - self.w(u'') - + self.w(u'%s' % ( + xml_escape(entity.absolute_url()), xml_escape(desc), + xml_escape(entity.dc_title()))) class OutOfContextView(EntityView): """:__regid__: *outofcontext* This view is used whenthe entity should be considered as displayed out of - its context. By default it produces the result of `textoutofcontext` wrapped + its context. By default it produces the result of ``entity.dc_long_title()`` wrapped in a link leading to the primary view of the entity. """ __regid__ = 'outofcontext' @@ -184,11 +181,9 @@ def cell_call(self, row, col): entity = self.cw_rset.get_entity(row, col) desc = cut(entity.dc_description(), 50) - self.w(u'' % ( - xml_escape(entity.absolute_url()), xml_escape(desc))) - self.w(xml_escape(self._cw.view('textoutofcontext', self.cw_rset, - row=row, col=col))) - self.w(u'') + self.w(u'%s' % ( + xml_escape(entity.absolute_url()), xml_escape(desc), + xml_escape(entity.dc_long_title()))) class OneLineView(EntityView): @@ -205,9 +200,12 @@ """the one line view for an entity: linked text view """ entity = self.cw_rset.get_entity(row, col) - self.w(u'' % xml_escape(entity.absolute_url())) - self.w(xml_escape(self._cw.view('text', self.cw_rset, row=row, col=col))) - self.w(u'') + desc = cut(entity.dc_description(), 50) + title = cut(entity.dc_title(), + self._cw.property_value('navigation.short-line-size')) + self.w(u'%s' % ( + xml_escape(entity.absolute_url()), xml_escape(desc), + xml_escape(title))) # text views ################################################################### diff -r c33092c4086f -r 9dc024fea040 web/views/bookmark.py --- a/web/views/bookmark.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/bookmark.py Thu Dec 22 17:46:55 2011 +0100 @@ -22,11 +22,12 @@ from logilab.mtconverter import xml_escape -from cubicweb import Unauthorized +from cubicweb import Unauthorized, typed_eid from cubicweb.selectors import is_instance, one_line_rset from cubicweb.web import (action, component, uicfg, htmlwidgets, formwidgets as fw) from cubicweb.web.views import primary +from cubicweb.web.views.ajaxcontroller import ajaxfunc _abaa = uicfg.actionbox_appearsin_addmenu _abaa.tag_subject_of(('*', 'bookmarked_by', '*'), False) @@ -133,3 +134,8 @@ menu.append(self.link(req._('pick existing bookmarks'), url)) self.append(menu) self.render_items(w) + +@ajaxfunc +def delete_bookmark(self, beid): + rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s' + self._cw.execute(rql, {'b': typed_eid(beid), 'u' : self._cw.user.eid}) diff -r c33092c4086f -r 9dc024fea040 web/views/cwproperties.py --- a/web/views/cwproperties.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/cwproperties.py Thu Dec 22 17:46:55 2011 +0100 @@ -35,6 +35,7 @@ from cubicweb.web.formwidgets import (Select, TextInput, Button, SubmitButton, FieldWidget) from cubicweb.web.views import primary, formrenderers, editcontroller +from cubicweb.web.views.ajaxcontroller import ajaxfunc uicfg.primaryview_section.tag_object_of(('*', 'for_user', '*'), 'hidden') @@ -419,6 +420,20 @@ """ return 'view', {} + +@ajaxfunc(output_type='xhtml') +def prop_widget(self, propkey, varname, tabindex=None): + """specific method for CWProperty handling""" + entity = self._cw.vreg['etypes'].etype_class('CWProperty')(self._cw) + entity.eid = varname + entity['pkey'] = propkey + form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity) + form.build_context() + vfield = form.field_by_name('value') + renderer = formrenderers.FormRenderer(self._cw) + return vfield.render(form, renderer, tabindex=tabindex) \ + + renderer.render_help(form, vfield) + _afs = uicfg.autoform_section _afs.tag_subject_of(('*', 'for_user', '*'), 'main', 'hidden') _afs.tag_object_of(('*', 'for_user', '*'), 'main', 'hidden') diff -r c33092c4086f -r 9dc024fea040 web/views/editcontroller.py --- a/web/views/editcontroller.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/editcontroller.py Thu Dec 22 17:46:55 2011 +0100 @@ -161,7 +161,7 @@ neweid = entity.eid except ValidationError, ex: self._to_create[eid] = ex.entity - if self._cw.json_request: # XXX (syt) why? + if self._cw.ajax_request: # XXX (syt) why? ex.entity = eid raise self._to_create[eid] = neweid diff -r c33092c4086f -r 9dc024fea040 web/views/facets.py --- a/web/views/facets.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/facets.py Thu Dec 22 17:46:55 2011 +0100 @@ -32,6 +32,7 @@ from cubicweb.uilib import css_em_num_value from cubicweb.view import AnyRsetView from cubicweb.web import component, facet as facetbase +from cubicweb.web.views.ajaxcontroller import ajaxfunc def facets(req, rset, context, mainvar=None, **kwargs): """return the base rql and a list of widgets for facets applying to the @@ -313,6 +314,28 @@ w(u'') w(u'\n') +# python-ajax remote functions used by facet widgets ######################### + +@ajaxfunc(output_type='json') +def filter_build_rql(self, names, values): + form = self._rebuild_posted_form(names, values) + self._cw.form = form + builder = facetbase.FilterRQLBuilder(self._cw) + return builder.build_rql() + +@ajaxfunc(output_type='json') +def filter_select_content(self, facetids, rql, mainvar): + # Union unsupported yet + select = self._cw.vreg.parse(self._cw, rql).children[0] + filtered_variable = facetbase.get_filtered_variable(select, mainvar) + facetbase.prepare_select(select, filtered_variable) + update_map = {} + for fid in facetids: + fobj = facetbase.get_facet(self._cw, fid, select, filtered_variable) + update_map[fid] = fobj.possible_values() + return update_map + + # facets ###################################################################### diff -r c33092c4086f -r 9dc024fea040 web/views/forms.py --- a/web/views/forms.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/forms.py Thu Dec 22 17:46:55 2011 +0100 @@ -406,7 +406,7 @@ return self.force_session_key # XXX if this is a json request, suppose we should redirect to the # entity primary view - if self._cw.json_request and self.edited_entity.has_eid(): + if self._cw.ajax_request and self.edited_entity.has_eid(): return '%s#%s' % (self.edited_entity.absolute_url(), self.domid) # XXX we should not consider some url parameters that may lead to # different url after a validation error diff -r c33092c4086f -r 9dc024fea040 web/views/treeview.py --- a/web/views/treeview.py Thu Dec 22 17:44:50 2011 +0100 +++ b/web/views/treeview.py Thu Dec 22 17:46:55 2011 +0100 @@ -31,6 +31,7 @@ from cubicweb.view import EntityView from cubicweb.mixins import _done_init from cubicweb.web.views import baseviews +from cubicweb.web.views.ajaxcontroller import ajaxfunc def treecookiename(treeid): return str('%s-treestate' % treeid) @@ -280,3 +281,20 @@ treeid=treeid, initial_load=False, **morekwargs) w(u'') + + +@ajaxfunc +def node_clicked(self, treeid, nodeeid): + """add/remove eid in treestate cookie""" + cookies = self._cw.get_cookie() + statename = treecookiename(treeid) + treestate = cookies.get(statename) + if treestate is None: + self._cw.set_cookie(statename, nodeeid) + else: + marked = set(filter(None, treestate.value.split(':'))) + if nodeeid in marked: + marked.remove(nodeeid) + else: + marked.add(nodeeid) + self._cw.set_cookie(statename, ':'.join(marked))