javascript + json refactoring tls-sprint
authorsylvain.thenault@logilab.fr
Wed, 22 Apr 2009 16:50:46 +0200
branchtls-sprint
changeset 1419 7ff24154351d
parent 1417 06af20e663f2
child 1420 25c13e5b12bd
javascript + json refactoring
web/data/cubicweb.ajax.js
web/data/cubicweb.bookmarks.js
web/data/cubicweb.calendar.js
web/data/cubicweb.compat.js
web/data/cubicweb.edition.js
web/data/cubicweb.formfilter.js
web/data/cubicweb.htmlhelpers.js
web/data/cubicweb.preferences.js
web/data/cubicweb.tabs.js
web/data/cubicweb.widgets.js
web/views/basecontrollers.py
--- a/web/data/cubicweb.ajax.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.ajax.js	Wed Apr 22 16:50:46 2009 +0200
@@ -1,6 +1,6 @@
 /*
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
@@ -37,6 +37,7 @@
     if (typeof roundedCornersOnLoad != 'undefined') {
 	roundedCornersOnLoad();
     }
+    jQuery(CubicWeb).trigger('ajax-loaded');
 }
 
 // cubicweb loadxhtml plugin to make jquery handle xhtml response
@@ -109,22 +110,6 @@
 
 //============= base AJAX functions to make remote calls =====================//
 
-
-/*
- * This function will call **synchronously** a remote method on the cubicweb server
- * @param fname: the function name to call (as exposed by the JSONController)
- * @param args: the list of arguments to pass the function
- */
-function remote_exec(fname) {
-    setProgressCursor();
-    var props = {'mode' : "remote", 'fname' : fname, 'pageid' : pageid,
-     		 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    var result  = jQuery.ajax({url: JSON_BASE_URL, data: props, async: false}).responseText;
-    result = evalJSON(result);
-    resetCursor();
-    return result;
-}
-
 function remoteCallFailed(err, req) {
     if (req.status == 500) {
 	updateMessage(err);
@@ -137,88 +122,66 @@
  * This function is the equivalent of MochiKit's loadJSONDoc but
  * uses POST instead of GET
  */
-function loadJSONDocUsingPOST(url, queryargs, mode) {
-    mode = mode || 'remote';
+function loadJSONDocUsingPOST(url, data) {
     setProgressCursor();
-    var dataType = (mode == 'remote') ? "json":null;
-    var deferred = loadJSON(url, queryargs, 'POST', dataType);
+    var deferred = loadJSON(url, data, 'POST');
     deferred = deferred.addErrback(remoteCallFailed);
-//     if (mode == 'remote') {
-// 	deferred = deferred.addCallbacks(evalJSONRequest);
-//     }
     deferred = deferred.addCallback(resetCursor);
     return deferred;
 }
 
 
-function _buildRemoteArgs(fname) {
-    return  {'mode' : "remote", 'fname' : fname, 'pageid' : pageid,
-     	     'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-}
-
 /*
- * This function will call **asynchronously** a remote method on the cubicweb server
- * This function is a low level one. You should use `async_remote_exec` or
- * `async_rawremote_exec` instead.
+ * This function will call **synchronously** a remote method on the cubicweb server
+ * @param fname: the function name to call (as exposed by the JSONController)
+ *
+ * additional arguments will be directly passed to the specified function
  *
- * @param fname: the function name to call (as exposed by the JSONController)
- * @param funcargs: the function's arguments
- * @param mode: rawremote or remote
+ * It looks at http headers to guess the response type.
  */
-function _async_exec(fname, funcargs, mode) {
-    var props = {'mode' : mode, 'fname' : fname, 'pageid' : pageid};
-    var args = map(urlEncode, map(jQuery.toJSON, funcargs));
-    args.unshift(''); // this is to be able to use join() directly
-    var queryargs = as_url(props) + args.join('&arg=');
-    return loadJSONDocUsingPOST(JSON_BASE_URL, queryargs, mode);
+function remoteExec(fname /* ... */) {
+    setProgressCursor();
+    var props = {'fname' : fname, 'pageid' : pageid,
+     		 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
+    var result  = jQuery.ajax({url: JSON_BASE_URL, data: props, async: false}).responseText;
+    if (result) {
+	result = evalJSON(result);
+    }
+    resetCursor();
+    return result;
 }
 
 /*
- * This function will call **asynchronously** a remote method on the cubicweb server
+ * This function will call **asynchronously** a remote method on the json
+ * controller of the cubicweb http server
+ *
  * @param fname: the function name to call (as exposed by the JSONController)
+ *
  * additional arguments will be directly passed to the specified function
- * Expected response type is Json.
- */
-function async_remote_exec(fname /* ... */) {
-    return _async_exec(fname, sliceList(arguments, 1), 'remote');
-}
-
-/*
- * This version of _async_exec doesn't expect a json response.
+ *
  * It looks at http headers to guess the response type.
  */
-function async_rawremote_exec(fname /* ... */) {
-    return _async_exec(fname, sliceList(arguments, 1), 'rawremote');
+function asyncRemoteExec(fname /* ... */) {
+    var props = {'fname' : fname, 'pageid' : pageid,
+     		 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
+    return loadJSONDocUsingPOST(JSON_BASE_URL, props);
 }
 
-/*
- * This function will call **asynchronously** a remote method on the cubicweb server
- * @param fname: the function name to call (as exposed by the JSONController)
- * @param varargs: the list of arguments to pass to the function
- * This is an alternative form of `async_remote_exec` provided for convenience
- */
-function async_remote_exec_varargs(fname, varargs) {
-    return _async_exec(fname, varargs, 'remote');
-}
 
 /* emulation of gettext's _ shortcut
  */
 function _(message) {
-    return remote_exec('i18n', [message])[0];
-}
-
-function rqlexec(rql) {
-    return async_remote_exec('rql', rql);
+    return remoteExec('i18n', [message])[0];
 }
 
 function userCallback(cbname) {
-    async_remote_exec('user_callback', cbname);
+    asyncRemoteExec('user_callback', cbname);
 }
 
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    remote_exec('unload_page_data');
+    remoteExec('unload_page_data');
 }
 
 function openHash() {
@@ -236,7 +199,7 @@
     nodeid = nodeid || (compid + 'Component');
     extraargs = extraargs || {};
     var node = getNode(nodeid);
-    var d = async_rawremote_exec('component', compid, rql, registry, extraargs);
+    var d = asyncRemoteExec('component', compid, rql, registry, extraargs);
     d.addCallback(function(result, req) {
 	var domnode = getDomFromResponse(result);
 	if (node) {
@@ -259,7 +222,7 @@
 }
 
 function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
-    var d = async_remote_exec('user_callback', cbname);
+    var d = asyncRemoteExec('user_callback', cbname);
     d.addCallback(function() {
 	reloadComponent(compid, rql, registry, nodeid);
 	if (msg) { updateMessage(msg); }
@@ -273,7 +236,7 @@
 }
 
 function userCallbackThenReloadPage(cbname, msg) {
-    var d = async_remote_exec('user_callback', cbname);
+    var d = asyncRemoteExec('user_callback', cbname);
     d.addCallback(function() {
 	window.location.reload();
 	if (msg) { updateMessage(msg); }
@@ -291,7 +254,7 @@
  * while the page was generated.
  */
 function unregisterUserCallback(cbname) {
-    var d = async_remote_exec('unregister_user_callback', cbname);
+    var d = asyncRemoteExec('unregister_user_callback', cbname);
     d.addCallback(function() {resetCursor();});
     d.addErrback(function(xxx) {
 	updateMessage(_("an error occured"));
@@ -322,11 +285,11 @@
 	props['pageid'] = pageid;
 	if (vid) { props['vid'] = vid; }
 	if (extraparams) { jQuery.extend(props, extraparams); }
-	// FIXME we need to do as_url(props) manually instead of
+	// FIXME we need to do asURL(props) manually instead of
 	// passing `props` directly to loadxml because replacePageChunk
 	// is sometimes called (abusively) with some extra parameters in `vid`
 	var mode = swap?'swap':'replace';
-	var url = JSON_BASE_URL + as_url(props);
+	var url = JSON_BASE_URL + asURL(props);
 	jQuery(node).loadxhtml(url, params, 'get', mode);
     } else {
 	log('Node', nodeId, 'not found');
@@ -357,6 +320,22 @@
 jQuery(document).ready(buildWysiwygEditors);
 
 
+/*
+ * takes a list of DOM nodes and removes all empty text nodes
+ */
+function stripEmptyTextNodes(nodelist) {
+    var stripped = [];
+    for (var i=0; i < nodelist.length; i++) {
+	var node = nodelist[i];
+	if (isTextNode(node) && !node.textContent.strip()) {
+	    continue;
+	} else {
+	    stripped.push(node);
+	}
+    }
+    return stripped;
+}
+
 /* convenience function that returns a DOM node based on req's result. */
 function getDomFromResponse(response) {
     if (typeof(response) == 'string') {
@@ -368,6 +347,7 @@
 	// no child (error cases) => return the whole document
 	return doc.cloneNode(true);
     }
+    children = stripEmptyTextNodes(children);
     if (children.length == 1) {
 	// only one child => return it
 	return children[0].cloneNode(true);
--- a/web/data/cubicweb.bookmarks.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.bookmarks.js	Wed Apr 22 16:50:46 2009 +0200
@@ -1,7 +1,7 @@
 CubicWeb.require('ajax.js');
 
 function removeBookmark(beid) {
-    d = async_remote_exec('delete_bookmark', beid);
+    d = asyncRemoteExec('delete_bookmark', beid);
     d.addCallback(function(boxcontent) {
 	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
   	document.location.hash = '#header';
--- a/web/data/cubicweb.calendar.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.calendar.js	Wed Apr 22 16:50:46 2009 +0200
@@ -235,7 +235,7 @@
     //      the only way understood by both IE and Mozilla. Otherwise,
     //      IE accepts innerText and mozilla accepts textContent
     var selectedDate = new Date(cal.year, cal.month, cell.innerHTML, 12);
-    var xxx = remote_exec("format_date", toISOTimestamp(selectedDate));
+    var xxx = remoteExec("format_date", toISOTimestamp(selectedDate));
     input.value = xxx;
     cal.hide();
 }
--- a/web/data/cubicweb.compat.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.compat.js	Wed Apr 22 16:50:46 2009 +0200
@@ -365,13 +365,12 @@
 };
 
 
-function loadJSON(url, data, type, dataType) {
+function loadJSON(url, data, type) {
     var d = new Deferred();
     jQuery.ajax({
 	url: url,
 	type: type,
 	data: data,
-	dataType: dataType,
 
 	beforeSend: function(xhr) {
 	    d.req = xhr;
--- a/web/data/cubicweb.edition.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.edition.js	Wed Apr 22 16:50:46 2009 +0200
@@ -1,6 +1,6 @@
 /*
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
@@ -22,7 +22,8 @@
 function setPropValueWidget(varname, tabindex) {
     var key = firstSelected(document.getElementById('pkey:'+varname));
     if (key) {
-	var args = _buildRemoteArgs('prop_widget', key, varname, tabindex);
+	var args = {fname: 'prop_widget', pageid: pageid,
+     		    arg: map(jQuery.toJSON, [key, varname, tabindex])};
 	jqNode('div:value:'+varname).loadxhtml(JSON_BASE_URL, args, 'post');
     }
 }
@@ -51,40 +52,25 @@
     });
 }
 
+
 function showMatchingSelect(selectedValue, eid) {
     if (selectedValue) {
 	divId = 'div' + selectedValue + '_' + eid;
 	var divNode = jQuery('#' + divId);
 	if (!divNode.length) {
 	    var args = {vid: 'unrelateddivs', relation: selectedValue,
-			rql: rql_for_eid(eid), pageid: pageid,
-			'__notemplate': 1};
-	    jQuery.get(JSON_BASE_URL, args, function(response) {
-		// append generated HTML to the cell
-		jQuery('#unrelatedDivs_' + eid).append(getDomFromResponse(response));
-		_showMatchingSelect(eid, jQuery('#' + divId));
-	    });
-	    // deferred = doXHR(JSON_BASE_URL + queryString(args));
-	    // deferred.addCallback(_buildAndShowMatchingSelect, eid, divId);
+			rql: rql_for_eid(eid), '__notemplate': 1,
+			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId))}};
+	    jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
 	} else {
 	    _showMatchingSelect(eid, divNode);
 	}
-    }
-    else {
+    } else {
 	_showMatchingSelect(eid, null);
     }
 }
 
 
-
-// @param divStr a HTML string returned by the server
-// function _buildAndShowMatchingSelect(eid, divId, req) {
-//     var tdNode = jQuery('#unrelatedDivs_' + eid);
-//     // append generated HTML to the cell
-//     tdNode.appendChild(getDomFromRequest(req));
-//     _showMatchingSelect(eid, jQuery('#' + divId));
-// }
-
 // @param divNode is a jQuery selection
 function _showMatchingSelect(eid, divNode) {
     // hide all divs, and then show the matching one
@@ -154,7 +140,7 @@
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
     var oid = optionNode.id.substring(2); // option id is prefixed by "id"
-    remote_exec('add_pending_insert', oid.split(':'));
+    remoteExec('add_pending_inserts', [oid.split(':')]);
     var selectNode = optionNode.parentNode;
     // remove option node
     selectNode.removeChild(optionNode);
@@ -186,7 +172,7 @@
 	   options[options.length] = OPTION({'id' : elementId, 'value' : node_id}, entityView);
 	}
     }
-    remote_exec('remove_pending_insert', elementId.split(':'));
+    remoteExec('remove_pending_insert', elementId.split(':'));
 }
 
 // this function builds a Handle to cancel pending insertion
@@ -198,7 +184,7 @@
 
 // @param nodeId eid_from:r_type:eid_to
 function addPendingDelete(nodeId, eid) {
-    var d = async_remote_exec('add_pending_delete', nodeId.split(':'));
+    var d = asyncRemoteExec('add_pending_delete', nodeId.split(':'));
     d.addCallback(function () {
 	// and strike entity view
 	jqNode('span' + nodeId).addClass('pendingDelete');
@@ -209,7 +195,7 @@
 
 // @param nodeId eid_from:r_type:eid_to
 function cancelPendingDelete(nodeId, eid) {
-    var d = async_remote_exec('remove_pending_delete', nodeId.split(':'));
+    var d = asyncRemoteExec('remove_pending_delete', nodeId.split(':'));
     d.addCallback(function () {
 	// reset link's CSS class
 	jqNode('span' + nodeId).removeClass('pendingDelete');
@@ -232,11 +218,11 @@
 function selectForAssociation(tripletIdsString, originalEid) {
     var tripletlist = map(function (x) { return x.split(':'); },
 			  tripletIdsString.split('-'));
-    var d = async_remote_exec('add_pending_inserts', tripletlist);
+    var d = asyncRemoteExec('add_pending_inserts', tripletlist);
     d.addCallback(function () {
 	var args = {vid: 'edition', __mode: 'normal',
 		    rql: rql_for_eid(originalEid)};
-	document.location = 'view?' + as_url(args);
+	document.location = 'view?' + asURL(args);
     });
 
 }
@@ -246,13 +232,9 @@
     jQuery('#inline' + rtype + 'slot span.icounter').each(function (i) {
 	this.innerHTML = i+1;
     });
-    // var divnode = jQuery('#inline' + rtype + 'slot');
-    // var iforms = getElementsByTagAndClassName('span', 'icounter', divnode);
-    // for (var i=0; i<iforms.length; i++) {
-    //   iforms[i].innerHTML = i+1;
-    // }
 }
 
+
 /*
  * makes an AJAX request to get an inline-creation view's content
  * @param peid : the parent entity eid
@@ -260,21 +242,17 @@
  * @param rtype : the relation type between both entities
  */
 function addInlineCreationForm(peid, ttype, rtype, role) {
-    var d = async_rawremote_exec('inline_creation_form', peid, ttype, rtype, role);
+    var d = asyncRemoteExec('inline_creation_form', peid, ttype, rtype, role);
     d.addCallback(function (response) {
 	var linknode = getNode('add' + rtype + ':' + peid + 'link');
         var dom = getDomFromResponse(response);
 	var form = jQuery(dom);
 	form.css('display', 'none');
 	form.insertBefore(linknode.parentNode).slideDown('fast');
-	// setStyle(form, {display: 'none'});
-	// insertSiblingNodesBefore(linknode.parentNode, form);
 	updateInlinedEntitiesCounters(rtype);
-	// slideDown(form, {'duration':0.6});
 	reorderTabindex();
 	form.trigger('inlinedform-added');
         postAjaxLoad(dom);
-	// MochiKit.Signal.signal(CubicWeb, 'inlinedform-added', form);
     });
     d.addErrback(function (xxx) {
 	log('xxx =', xxx);
@@ -300,15 +278,13 @@
  */
 function removeInlinedEntity(peid, rtype, eid) {
     var nodeid = ['rel', peid, rtype, eid].join('-');
-    var divid = ['div', peid, rtype, eid].join('-');
-    var noticeid = ['notice', peid, rtype, eid].join('-');
     var node = jqNode(nodeid);
     if (node && node.length) {
 	node.remove();
+	var divid = ['div', peid, rtype, eid].join('-');
 	jqNode(divid).fadeTo('fast', 0.5);
-	// setOpacity(divid, 0.4);
+	var noticeid = ['notice', peid, rtype, eid].join('-');
 	jqNode(noticeid).fadeIn('fast');
-	// appear(jQuery('#' + noticeid), {'duration': 0.5});
     }
 }
 
@@ -321,11 +297,8 @@
 	node = INPUT({type: 'hidden', id: nodeid,
 		      name: rtype+':'+peid, value: eid});
 	jqNode(['fs', peid, rtype, eid].join('-')).append(node);
-	// appendChildNodes(fs, node);
 	jqNode(divid).fadeTo('fast', 1);
-	// setOpacity(divid, 1);
 	jqNode(noticeid).hide();
-	// jQuery('#' + noticeid).hide();
     }
 }
 
@@ -433,8 +406,8 @@
 	var target = form.attr('cubicweb:target');
 	if (target) {
 	    form.attr('target', target);
-	    /* do not use display: none because some browser ignore iframe
-             *     with no display */
+	    /* do not use display: none because some browsers ignore iframe
+             * with no display */
 	    form.append(IFRAME({name: target, id: target,
 				src: 'javascript: void(0)',
 				width: '0px', height: '0px'}));
@@ -444,10 +417,6 @@
 
 $(document).ready(setFormsTarget);
 
-function _sendForm(formid, action) {
-    var zipped = formContents(formid);
-    return async_remote_exec('validate_form', action, zipped[0], zipped[1]);
-}
 
 /*
  * called on traditionnal form submission : the idea is to try
@@ -457,7 +426,8 @@
  */
 function validateForm(formid, action, onsuccess) {
     try {
-	var d = _sendForm(formid, action);
+	var zipped = formContents(formid);
+	var d = asyncRemoteExec('validate_form', action, zipped[0], zipped[1]);
     } catch (ex) {
 	log('got exception', ex);
 	return false;
@@ -465,11 +435,11 @@
     function _callback(result, req) {
 	handleFormValidationResponse(formid, onsuccess, result);
     }
-    // d.addCallback(handleFormValidationResponse, formid, onsuccess);
     d.addCallback(_callback);
     return false;
 }
 
+
 /*
  * called by live-edit forms to submit changes
  * @param formid : the dom id of the form used
@@ -489,7 +459,7 @@
 	    }
 	}
 	var zipped = formContents(form);
-	var d = async_remote_exec('edit_field', 'apply', zipped[0], zipped[1], rtype, eid);
+	var d = asyncRemoteExec('edit_field', 'apply', zipped[0], zipped[1], rtype, eid);
     } catch (ex) {
 	log('got exception', ex);
 	return false;
--- a/web/data/cubicweb.formfilter.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.formfilter.js	Wed Apr 22 16:50:46 2009 +0200
@@ -43,7 +43,7 @@
     var zipped = facetFormContent(form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = async_remote_exec('filter_build_rql', zipped[0], zipped[1]);
+    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
     d.addCallback(function(result) {
 	var rql = result[0];
 	var $bkLink = jQuery('#facetBkLink');
@@ -80,7 +80,7 @@
 		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
 	    }
 	}
-	var d = async_remote_exec('filter_select_content', toupdate, rql);
+	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
 	d.addCallback(function(updateMap) {
 	    for (facetId in updateMap) {
 		var values = updateMap[facetId];
--- a/web/data/cubicweb.htmlhelpers.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Wed Apr 22 16:50:46 2009 +0200
@@ -111,13 +111,13 @@
 }
 
 /* builds an url from an object (used as a dictionnary)
- * Notable difference with MochiKit's queryString: as_url does not
+ * Notable difference with MochiKit's queryString: asURL does not
  * *url_quote* each value found in the dictionnary
  *
- * >>> as_url({'rql' : "RQL", 'x': [1, 2], 'itemvid' : "oneline"})
+ * >>> asURL({'rql' : "RQL", 'x': [1, 2], 'itemvid' : "oneline"})
  * rql=RQL&vid=list&itemvid=oneline&x=1&x=2
  */
-function as_url(props) {
+function asURL(props) {
     var chunks = [];
     for(key in props) {
 	var value = props[key];
--- a/web/data/cubicweb.preferences.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.preferences.js	Wed Apr 22 16:50:46 2009 +0200
@@ -5,6 +5,6 @@
  */
 function toggle_and_remember_visibility(elemId, cookiename) {
     jqNode(elemId).toggleClass('hidden');
-    async_remote_exec('set_cookie', cookiename,
+    asyncRemoteExec('set_cookie', cookiename,
                       jQuery('#' + elemId).attr('class'));
 }
--- a/web/data/cubicweb.tabs.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.tabs.js	Wed Apr 22 16:50:46 2009 +0200
@@ -1,6 +1,6 @@
 function set_tab(tabname, cookiename) {
     // set appropriate cookie
-    async_remote_exec('set_cookie', cookiename, tabname);
+    asyncRemoteExec('set_cookie', cookiename, tabname);
     // trigger show + tabname event
     trigger_load(tabname);
 }
--- a/web/data/cubicweb.widgets.js	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/data/cubicweb.widgets.js	Wed Apr 22 16:50:46 2009 +0200
@@ -277,7 +277,7 @@
 	  this.eid_to = name[1];
           this.etype_to = wdgnode.getAttribute('cubicweb:etype_to');
           this.etype_from = wdgnode.getAttribute('cubicweb:etype_from');
-     	  var d = async_remote_exec('add_and_link_new_entity', this.etype_to, this.rel, this.eid_to, this.etype_from, 'new_val');
+     	  var d = asyncRemoteExec('add_and_link_new_entity', this.etype_to, this.rel, this.eid_to, this.etype_from, 'new_val');
           d.addCallback(function (eid) {
           jQuery(wdgnode).find("option[selected]").removeAttr("selected");
           var new_option = OPTION({'value':eid, 'selected':'selected'}, value=new_val);
--- a/web/views/basecontrollers.py	Tue Apr 21 19:20:56 2009 +0200
+++ b/web/views/basecontrollers.py	Wed Apr 22 16:50:46 2009 +0200
@@ -18,7 +18,7 @@
 from cubicweb import NoSelectableObject, ValidationError, ObjectNotFound, typed_eid
 from cubicweb.utils import strptime
 from cubicweb.selectors import yes, match_user_groups
-from cubicweb.view import STRICT_DOCTYPE, CW_XHTML_EXTENSIONS
+from cubicweb.view import STRICT_DOCTYPE
 from cubicweb.common.mail import format_mail
 from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed
 from cubicweb.web.formrenderers import FormRenderer
@@ -30,8 +30,42 @@
     HAS_SEARCH_RESTRICTION = True
 except ImportError: # gae
     HAS_SEARCH_RESTRICTION = False
-    
-    
+
+
+def xhtml_wrap(source):
+    head = u'<?xml version="1.0"?>\n' + STRICT_DOCTYPE
+    return head + u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">%s</div>' % source.strip()
+
+def jsonize(func):
+    """decorator to sets correct content_type and calls `simplejson.dumps` on
+    results
+    """
+    def wrapper(self, *args, **kwargs):
+        self.req.set_content_type('application/json')
+        result = func(self, *args, **kwargs)
+        return simplejson.dumps(result)
+    return wrapper
+
+def xhtmlize(func):
+    """decorator to sets correct content_type and calls `xmlize` on results"""
+    def wrapper(self, *args, **kwargs):
+        self.req.set_content_type(self.req.html_content_type())
+        result = func(self, *args, **kwargs)
+        return xhtml_wrap(result)
+    return wrapper
+
+def check_pageid(func):
+    """decorator which checks the given pageid is found in the
+    user's session data
+    """
+    def wrapper(self, *args, **kwargs):
+        data = self.req.get_session_data(self.req.pageid)
+        if data is None:
+            raise RemoteCallFailed(self.req._('pageid-not-found'))
+        return func(self, *args, **kwargs)
+    return wrapper
+
+
 class LoginController(Controller):
     id = 'login'
 
@@ -44,10 +78,10 @@
             # Cookie authentication
             return self.appli.need_login_content(self.req)
 
-    
+
 class LogoutController(Controller):
     id = 'logout'
-    
+
     def publish(self, rset=None):
         """logout from the application"""
         return self.appli.session_handler.logout(self.req)
@@ -60,7 +94,7 @@
     """
     id = 'view'
     template = 'main-template'
-    
+
     def publish(self, rset=None):
         """publish a request, returning an encoded string"""
         view, rset = self._select_view_and_rset(rset)
@@ -131,7 +165,7 @@
             else:
                 rql = 'SET Y %s X WHERE X eid %%(x)s, Y eid %%(y)s' % rtype
             for teid in eids:
-                req.execute(rql, {'x': eid, 'y': typed_eid(teid)}, ('x', 'y')) 
+                req.execute(rql, {'x': eid, 'y': typed_eid(teid)}, ('x', 'y'))
 
 
 class FormValidatorController(Controller):
@@ -178,56 +212,65 @@
         except AttributeError:
             eid = err.entity
         return (False, (eid, err.errors))
-        
-def xmlize(source):
-    head = u'<?xml version="1.0"?>\n' + STRICT_DOCTYPE % CW_XHTML_EXTENSIONS
-    return head + u'<div xmlns="http://www.w3.org/1999/xhtml" xmlns:cubicweb="http://www.logilab.org/2008/cubicweb">%s</div>' % source.strip()
 
-def jsonize(func):
-    """sets correct content_type and calls `simplejson.dumps` on results
-    """
-    def wrapper(self, *args, **kwargs):
-        self.req.set_content_type('application/json')
-        result = func(self, *args, **kwargs)
-        return simplejson.dumps(result)
-    return wrapper
-
-
-def check_pageid(func):
-    """decorator which checks the given pageid is found in the
-    user's session data
-    """
-    def wrapper(self, *args, **kwargs):
-        data = self.req.get_session_data(self.req.pageid)
-        if data is None:
-            raise RemoteCallFailed(self.req._('pageid-not-found'))
-        return func(self, *args, **kwargs)
-    return wrapper
-    
 
 class JSonController(Controller):
     id = 'json'
-    template = 'main'
 
     def publish(self, rset=None):
-        mode = self.req.form.get('mode', 'html')
+        """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.req.pageid = self.req.form.get('pageid')
+        fname = self.req.form['fname']
+        try:
+            func = getattr(self, 'js_%s' % fname)
+        except AttributeError:
+            raise RemoteCallFailed('no %s method' % fname)
+        # no <arg> attribute means the callback takes no argument
+        args = self.req.form.get('arg', ())
+        if not isinstance(args, (list, tuple)):
+            args = (args,)
+        args = [simplejson.loads(arg) for arg in args]
         try:
-            func = getattr(self, '%s_exec' % mode)
-        except AttributeError, ex:
-            self.error('json controller got an unknown mode %r', mode)
-            self.error('\t%s', ex)
-            result = u''
-        else:
-            try:
-                result = func(rset)
-            except RemoteCallFailed:
-                raise
-            except Exception, ex:
-                self.exception('an exception occured on json request(rset=%s): %s',
-                               rset, ex)
-                raise RemoteCallFailed(repr(ex))
-        return result.encode(self.req.encoding)
+            result = func(*args)
+        except RemoteCallFailed:
+            raise
+        except Exception, ex:
+            self.exception('an exception occured while calling js_%s(%s): %s',
+                           fname, args, ex)
+            raise RemoteCallFailed(repr(ex))
+        if result is None:
+            return ''
+        # get unicode on @htmlize methods, encoded string on @jsonize methods
+        elif isinstance(result, unicode):
+            return result.encode(self.req.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'):
+                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, eidkey=None, rocheck=True):
         """json mode: execute RQL and return resultset as json"""
@@ -240,31 +283,15 @@
             return None
         return None
 
-    @jsonize
-    def json_exec(self, rset=None):
-        """json mode: execute RQL and return resultset as json"""
-        rql = self.req.form.get('rql')
-        if rset is None and rql:
-            rset = self._exec(rql)
-        return rset and rset.rows or []
-
-    def _set_content_type(self, vobj, data):
-        """sets req's content type according to vobj's content type
-        (and xmlize data if needed)
-        """
-        content_type = vobj.content_type
-        if content_type == 'application/xhtml+xml':
-            self.req.set_content_type(content_type)
-            return xmlize(data)
-        return data
-    
-    def html_exec(self, rset=None):
+    @xhtmlize
+    def js_view(self):
         # XXX try to use the page-content template
         req = self.req
         rql = req.form.get('rql')
-        if rset is None and rql:
+        if rql:
             rset = self._exec(rql)
-        
+        else:
+            rset = None
         vid = req.form.get('vid') or vid_from_rset(req, rset, self.schema)
         try:
             view = self.vreg.select_view(vid, req, rset)
@@ -292,46 +319,46 @@
             stream.write(u'</div>\n')
         if req.form.get('paginate') and divid == 'pageContent':
             stream.write(u'</div></div>')
-        source = stream.getvalue()
-        return self._set_content_type(view, source)
+        return stream.getvalue()
 
-    def rawremote_exec(self, rset=None):
-        """like remote_exec but doesn't change content type"""
-        # no <arg> attribute means the callback takes no argument
-        args = self.req.form.get('arg', ())
-        if not isinstance(args, (list, tuple)):
-            args = (args,)
-        fname = self.req.form['fname']
-        args = [simplejson.loads(arg) for arg in args]
-        try:
-            func = getattr(self, 'js_%s' % fname)
-        except AttributeError:
-            self.exception('rawremote_exec fname=%s', fname)
-            return u""
-        return func(*args)
+    @xhtmlize
+    def js_prop_widget(self, propkey, varname, tabindex=None):
+        """specific method for CWProperty handling"""
+        entity = self.vreg.etype_class('CWProperty')(self.req, None, None)
+        entity.eid = varname
+        entity['pkey'] = propkey
+        form = self.vreg.select_object('forms', 'edition', self.req, None,
+                                       entity=entity)
+        form.form_build_context()
+        vfield = form.field_by_name('value')
+        renderer = FormRenderer()
+        return vfield.render(form, renderer, tabindex=tabindex) \
+               + renderer.render_help(form, vfield)
 
-    remote_exec = jsonize(rawremote_exec)
-        
-    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'):
-                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
-    
+    @xhtmlize
+    def js_component(self, compid, rql, registry='components', extraargs=None):
+        if rql:
+            rset = self._exec(rql)
+        else:
+            rset = None
+        comp = self.vreg.select_object(registry, compid, self.req, rset)
+        if extraargs is None:
+            extraargs = {}
+        else: # we receive unicode keys which is not supported by the **syntax
+            extraargs = dict((str(key), value)
+                             for key, value in extraargs.items())
+        extraargs = extraargs or {}
+        return comp.dispatch(**extraargs)
+
+    @check_pageid
+    @xhtmlize
+    def js_inline_creation_form(self, peid, ttype, rtype, role):
+        view = self.vreg.select_view('inline-creation', self.req, None,
+                                     etype=ttype, peid=peid, rtype=rtype,
+                                     role=role)
+        return view.dispatch(etype=ttype, peid=peid, rtype=rtype, role=role)
+
+    @jsonize
     def js_validate_form(self, action, names, values):
         # XXX this method (and correspoding js calls) should use the new
         #     `RemoteCallFailed` mechansim
@@ -359,6 +386,7 @@
             return (False, self.req._(str(err)))
         return (False, '???')
 
+    @jsonize
     def js_edit_field(self, action, names, values, rtype, eid):
         success, args = self.js_validate_form(action, names, values)
         if success:
@@ -368,52 +396,29 @@
             return (success, args, entity.printable_value(rtype))
         else:
             return (success, args, None)
-            
-    def js_rql(self, rql):
-        rset = self._exec(rql)
-        return rset and rset.rows or []
-    
+
+#     def js_rql(self, rql):
+#         rset = self._exec(rql)
+#         return rset and rset.rows or []
+
+    @jsonize
     def js_i18n(self, msgids):
         """returns the translation of `msgid`"""
         return [self.req._(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.format_date(date)
 
+    @jsonize
     def js_external_resource(self, resource):
         """returns the URL of the external resource named `resource`"""
         return self.req.external_resource(resource)
 
-    def js_prop_widget(self, propkey, varname, tabindex=None):
-        """specific method for CWProperty handling"""
-        entity = self.vreg.etype_class('CWProperty')(self.req, None, None)
-        entity.eid = varname
-        entity['pkey'] = propkey
-        form = self.vreg.select_object('forms', 'edition', self.req, None,
-                                       entity=entity)
-        form.form_build_context()
-        vfield = form.field_by_name('value')
-        renderer = FormRenderer()
-        return vfield.render(form, renderer, tabindex=tabindex) \
-                   + renderer.render_help(form, vfield)
-
-    def js_component(self, compid, rql, registry='components', extraargs=None):
-        if rql:
-            rset = self._exec(rql)
-        else:
-            rset = None
-        comp = self.vreg.select_object(registry, compid, self.req, rset)
-        if extraargs is None:
-            extraargs = {}
-        else: # we receive unicode keys which is not supported by the **syntax
-            extraargs = dict((str(key), value)
-                             for key, value in extraargs.items())
-        extraargs = extraargs or {}
-        return self._set_content_type(comp, comp.dispatch(**extraargs))
-
     @check_pageid
+    @jsonize
     def js_user_callback(self, cbname):
         page_data = self.req.get_session_data(self.req.pageid, {})
         try:
@@ -421,53 +426,16 @@
         except KeyError:
             return None
         return cb(self.req)
-    
-    def js_unregister_user_callback(self, cbname):
-        self.req.unregister_callback(self.req.pageid, cbname)
-
-    def js_unload_page_data(self):
-        self.req.del_session_data(self.req.pageid)
-        
-    def js_cancel_edition(self, errorurl):
-        """cancelling edition from javascript
-
-        We need to clear associated req's data :
-          - errorurl
-          - pending insertions / deletions
-        """
-        self.req.cancel_edition(errorurl)
-    
-    @check_pageid
-    def js_inline_creation_form(self, peid, ttype, rtype, role):
-        view = self.vreg.select_view('inline-creation', self.req, None,
-                                     etype=ttype, peid=peid, rtype=rtype,
-                                     role=role)
-        source = view.dispatch(etype=ttype, peid=peid, rtype=rtype, role=role)
-        return self._set_content_type(view, source)
-
-    def js_remove_pending_insert(self, (eidfrom, rel, eidto)):
-        self._remove_pending(eidfrom, rel, eidto, 'insert')
-        
-    def js_add_pending_insert(self, (eidfrom, rel, eidto)):
-        self._add_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')
 
     if HAS_SEARCH_RESTRICTION:
+        @jsonize
         def js_filter_build_rql(self, names, values):
             form = self._rebuild_posted_form(names, values)
             self.req.form = form
             builder = FilterRQLBuilder(self.req)
             return builder.build_rql()
 
+        @jsonize
         def js_filter_select_content(self, facetids, rql):
             rqlst = self.vreg.parse(self.req, rql) # XXX Union unsupported yet
             mainvar = prepare_facets_rqlst(rqlst)[0]
@@ -477,13 +445,33 @@
                 update_map[facetid] = facet.possible_values()
             return update_map
 
+    def js_unregister_user_callback(self, cbname):
+        self.req.unregister_callback(self.req.pageid, cbname)
+
+    def js_unload_page_data(self):
+        self.req.del_session_data(self.req.pageid)
+
+    def js_cancel_edition(self, errorurl):
+        """cancelling edition from javascript
+
+        We need to clear associated req's data :
+          - errorurl
+          - pending insertions / deletions
+        """
+        self.req.cancel_edition(errorurl)
+
     def js_delete_bookmark(self, beid):
-        try:
-            rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
-            self.req.execute(rql, {'b': typed_eid(beid), 'u' : self.req.user.eid})
-        except Exception, ex:
-            self.exception(unicode(ex))
-            return self.req._('Problem occured')
+        rql = 'DELETE B bookmarked_by U WHERE B eid %(b)s, U eid %(u)s'
+        self.req.execute(rql, {'b': typed_eid(beid), 'u' : self.req.user.eid})
+
+    def js_set_cookie(self, cookiename, cookievalue):
+        # XXX we should consider jQuery.Cookie
+        cookiename, cookievalue = str(cookiename), str(cookievalue)
+        cookies = self.req.get_cookie()
+        cookies[cookiename] = cookievalue
+        self.req.set_cookie(cookies, cookiename)
+
+    # relations edition stuff ##################################################
 
     def _add_pending(self, eidfrom, rel, eidto, kind):
         key = 'pending_%s' % kind
@@ -492,7 +480,7 @@
         self.req.set_session_data(key, pendings)
 
     def _remove_pending(self, eidfrom, rel, eidto, kind):
-        key = 'pending_%s' % kind        
+        key = 'pending_%s' % kind
         try:
             pendings = self.req.get_session_data(key)
             pendings.remove( (typed_eid(eidfrom), rel, typed_eid(eidto)) )
@@ -501,6 +489,21 @@
         else:
             self.req.set_session_data(key, pendings)
 
+    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')
+
+    # XXX specific code. Kill me and my AddComboBox friend
+    @jsonize
     def js_add_and_link_new_entity(self, etype_to, rel, eid_to, etype_from, value_from):
         # create a new entity
         eid_from = self.req.execute('INSERT %s T : T name "%s"' % ( etype_from, value_from ))[0][0]
@@ -508,12 +511,6 @@
         rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from}
         return eid_from
 
-    def js_set_cookie(self, cookiename, cookievalue):
-        # XXX we should consider jQuery.Cookie
-        cookiename, cookievalue = str(cookiename), str(cookievalue)
-        cookies = self.req.get_cookie()
-        cookies[cookiename] = cookievalue
-        self.req.set_cookie(cookies, cookiename)
 
 class SendMailController(Controller):
     id = 'sendmail'
@@ -549,7 +546,7 @@
         msg = format_mail({'email' : self.req.user.get_email(),
                            'name' : self.req.user.dc_title(),},
                           [recipient], body, subject)
-        self.smtp.sendmail(helo_addr, [recipient], msg.as_string())    
+        self.smtp.sendmail(helo_addr, [recipient], msg.as_string())
 
     def publish(self, rset=None):
         # XXX this allow anybody with access to an cubicweb application to use it as a mail relay
@@ -572,4 +569,4 @@
         self.sendmail(self.config['submit-mail'], _('%s error report') % self.config.appid, body)
         url = self.build_url(__message=self.req._('bug report sent'))
         raise Redirect(url)
-    
+