[box] provide a new generic base box class to edit relation to simple entities, backported from the 'tag' cube
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 08 Jul 2010 18:59:42 +0200
changeset 5949 2a273c896a38
parent 5948 4154bdc85fe4
child 5950 f84dba9b8eca
[box] provide a new generic base box class to edit relation to simple entities, backported from the 'tag' cube
test/unittest_uilib.py
uilib.py
web/box.py
web/data/cubicweb.ajax.box.js
web/data/cubicweb.js
--- a/test/unittest_uilib.py	Thu Jul 08 18:48:44 2010 +0200
+++ b/test/unittest_uilib.py	Thu Jul 08 18:59:42 2010 +0200
@@ -142,6 +142,14 @@
         self.assertEquals(uilib.soup2xhtml('hop </html> hop', 'ascii'),
                           'hop  hop')
 
+    def test_js(self):
+        self.assertEquals(str(uilib.js.pouet(1, "2")),
+                          'pouet(1,"2")')
+        self.assertEquals(str(uilib.js.cw.pouet(1, "2")),
+                          'cw.pouet(1,"2")')
+        self.assertEquals(str(uilib.js.cw.pouet(1, "2").pouet(None)),
+                          'cw.pouet(1,"2").pouet(null)')
+
 if __name__ == '__main__':
     unittest_main()
 
--- a/uilib.py	Thu Jul 08 18:48:44 2010 +0200
+++ b/uilib.py	Thu Jul 08 18:59:42 2010 +0200
@@ -31,6 +31,8 @@
 from logilab.mtconverter import xml_escape, html_unescape
 from logilab.common.date import ustrftime
 
+from cubicweb.utils import json_dumps
+
 
 def rql_for_eid(eid):
     """return the rql query necessary to fetch entity with the given eid.  This
@@ -228,6 +230,52 @@
 
 # HTML generation helper functions ############################################
 
+class _JSId(object):
+    def __init__(self, id, parent=None):
+        self.id = id
+        self.parent = parent
+    def __str__(self):
+        if self.parent:
+            return '%s.%s' % (self.parent, self.id)
+        return '%s' % self.id
+    def __getattr__(self, attr):
+        return _JSId(attr, self)
+    def __call__(self, *args):
+        return _JSCallArgs(args, self)
+
+class _JSCallArgs(_JSId):
+    def __init__(self, args, parent=None):
+        assert isinstance(args, tuple)
+        self.args = args
+        self.parent = parent
+    def __str__(self):
+        args = ','.join(json_dumps(arg) for arg in self.args)
+        if self.parent:
+            return '%s(%s)' % (self.parent, args)
+        return args
+
+class _JS(object):
+    def __getattr__(self, attr):
+        return _JSId(attr)
+
+"""magic object to return strings suitable to call some javascript function with
+the given arguments (which should be correctly typed).
+
+>>> str(js.pouet(1, "2"))
+'pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2"))
+'cw.pouet(1,"2")'
+>>> str(js.cw.pouet(1, "2").pouet(None))
+'cw.pouet(1,"2").pouet(null)')
+"""
+js = _JS()
+
+def domid(string):
+    """return a valid DOM id from a string (should also be usable in jQuery
+    search expression...)
+    """
+    return string.replace('.', '_').replace('-', '_')
+
 HTML4_EMPTY_TAGS = frozenset(('base', 'meta', 'link', 'hr', 'br', 'param',
                               'img', 'area', 'input', 'col'))
 
--- a/web/box.py	Thu Jul 08 18:48:44 2010 +0200
+++ b/web/box.py	Thu Jul 08 18:59:42 2010 +0200
@@ -15,9 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""abstract box classes for CubicWeb web client
+"""abstract box classes for CubicWeb web client"""
 
-"""
 __docformat__ = "restructuredtext en"
 _ = unicode
 
@@ -26,10 +25,11 @@
 from cubicweb import Unauthorized, role as get_role, target as get_target
 from cubicweb.schema import display_name
 from cubicweb.selectors import (no_cnx, one_line_rset,  primary_view,
-                                match_context_prop, partial_has_related_entities)
+                                match_context_prop, partial_relation_possible,
+                                partial_has_related_entities)
 from cubicweb.view import View, ReloadableMixIn
-
-from cubicweb.web import INTERNAL_FIELD_VALUE
+from cubicweb.uilib import domid, js
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
                                       RawBoxItem, BoxSeparator)
 from cubicweb.web.action import UnregisteredAction
@@ -241,3 +241,92 @@
                     entities.append(entity)
         return entities
 
+
+class AjaxEditRelationBoxTemplate(EntityBoxTemplate):
+    __select__ = EntityBoxTemplate.__select__ & partial_relation_possible()
+
+    # view used to display related entties
+    item_vid = 'incontext'
+    # values separator when multiple values are allowed
+    separator = ','
+    # msgid of the message to display when some new relation has been added/removed
+    added_msg = None
+    removed_msg = None
+
+    # class attributes below *must* be set in concret classes (additionaly to
+    # rtype / role [/ target_etype]. They should correspond to js_* methods on
+    # the json controller
+
+    # function(eid)
+    # -> expected to return a list of values to display as input selector
+    #    vocabulary
+    fname_vocabulary = None
+
+    # function(eid, value)
+    # -> handle the selector's input (eg create necessary entities and/or
+    # relations). If the relation is multiple, you'll get a list of value, else
+    # a single string value.
+    fname_validate = None
+
+    # function(eid, linked entity eid)
+    # -> remove the relation
+    fname_remove = None
+
+    def cell_call(self, row, col, **kwargs):
+        req = self._cw
+        entity = self.cw_rset.get_entity(row, col)
+        related = entity.related(self.rtype, self.role)
+        rdef = entity.e_schema.rdef(self.rtype, self.role, self.target_etype)
+        if self.role == 'subject':
+            mayadd = rdef.has_perm(req, 'add', fromeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', fromeid=entity.eid)
+        else:
+            mayadd = rdef.has_perm(req, 'add', toeid=entity.eid)
+            maydel = rdef.has_perm(req, 'delete', toeid=entity.eid)
+        if not (related or mayadd):
+            return
+        if mayadd or maydel:
+            req.add_js(('cubicweb.ajax.js', 'cubicweb.ajax.box.js'))
+        _ = req._
+        w = self.w
+        divid = domid(self.__regid__) + unicode(entity.eid)
+        w(u'<div class="sideBox" id="%s%s">' % (domid(self.__regid__), entity.eid))
+        w(u'<div class="sideBoxTitle"><span>%s</span></div>' %
+               rdef.rtype.display_name(req, self.role))
+        w(u'<div class="sideBox"><div class="sideBoxBody">')
+        if related:
+            w(u'<table>')
+            for rentity in related.entities():
+                # for each related entity, provide a link to remove the relation
+                subview = rentity.view(self.item_vid)
+                if maydel:
+                    jscall = js.ajaxBoxRemoveLinkedEntity(
+                        self.__regid__, entity.eid, rentity.eid,
+                        self.fname_remove,
+                        self.removed_msg and _(self.removed_msg))
+                    w(u'<tr><td>[<a href="javascript: %s">-</a>]</td>'
+                      '<td class="tagged">%s</td></tr>' % (xml_escape(jscall),
+                                                           subview))
+                else:
+                    w(u'<tr><td class="tagged">%s</td></tr>' % (subview))
+            w(u'</table>')
+        else:
+            w(_('no related entity'))
+        if mayadd:
+            req.add_js('jquery.autocomplete.js')
+            req.add_css('jquery.autocomplete.css')
+            multiple = rdef.role_cardinality(self.role) in '*+'
+            w(u'<table><tr><td>')
+            jscall = js.ajaxBoxShowSelector(
+                self.__regid__, entity.eid, self.fname_vocabulary,
+                self.fname_validate, self.added_msg and _(self.added_msg),
+                _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
+                multiple and self.separator)
+            w('<a class="button sglink" href="javascript: %s">%s</a>' % (
+                xml_escape(jscall),
+                multiple and _('add_relation') or _('update_relation')))
+            w(u'</td><td>')
+            w(u'<div id="%sHolder"></div>' % divid)
+            w(u'</td></tr></table>')
+        w(u'</div>\n')
+        w(u'</div></div>\n')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.ajax.box.js	Thu Jul 08 18:59:42 2010 +0200
@@ -0,0 +1,81 @@
+/**
+ * Functions for ajax boxes.
+ *
+ *  :organization: Logilab
+ *  :copyright: 2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
+ */
+
+function ajaxBoxValidateSelectorInput(boxid, eid, separator, fname, msg) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var value = $('#' + holderid + 'Input').val();
+    if (separator) {
+	value = $.map(value.split(separator), jQuery.trim);
+    }
+    var d = loadRemote('json', ajaxFuncArgs(fname, null, eid, value));
+    d.addCallback(function() {
+	    $('#' + holderid).empty();
+	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+	    if (msg) {
+		document.location.hash = '#header';
+		updateMessage(msg);
+	    }
+	});
+}
+
+function ajaxBoxRemoveLinkedEntity(boxid, eid, relatedeid, delfname, msg) {
+    var d = loadRemote('json', ajaxFuncArgs(delfname, null, eid, relatedeid));
+    d.addCallback(function() {
+	    var formparams = ajaxFuncArgs('render', null, 'boxes', boxid, eid);
+	    $('#' + cw.utils.domid(boxid) + eid).loadxhtml('json', formparams);
+	    if (msg) {
+		document.location.hash = '#header';
+		updateMessage(msg);
+	    }
+    });
+}
+
+function ajaxBoxShowSelector(boxid, eid,
+			     unrelfname,
+			     addfname, msg,
+			     oklabel, cancellabel,
+			     separator) {
+    var holderid = cw.utils.domid(boxid) + eid + 'Holder';
+    var holder = $('#' + holderid);
+    if (holder.children().length) {
+	holder.empty();
+    }
+    else {
+	var inputid = holderid + 'Input';
+	var deferred = loadRemote('json', ajaxFuncArgs(unrelfname, null, eid));
+	deferred.addCallback(function (unrelated) {
+	    var input = INPUT({'type': 'text', 'id': inputid, 'size': 20});
+	    holder.append(input).show();
+	    $input = $(input);
+	    $input.keypress(function (event) {
+		if (event.keyCode == KEYS.KEY_ENTER) {
+		    // XXX not very user friendly: we should test that the suggestions
+		    //     aren't visible anymore
+		    ajaxBoxValidateSelectorInput(boxid, eid, separator, addfname, msg);
+		}
+	    });
+	    var buttons = DIV({'class' : "sgformbuttons"},
+			      A({'href' : "javascript: noop();",
+				 'onclick' : cw.utils.strFuncCall('ajaxBoxValidateSelectorInput',
+								  boxid, eid, separator, addfname, msg)},
+				  oklabel),
+			      ' / ',
+			      A({'href' : "javascript: noop();",
+				 'onclick' : '$("#' + holderid + '").empty()'},
+				  cancellabel));
+	    holder.append(buttons);
+	    $input.autocomplete(unrelated, {
+		multiple: separator,
+		max: 15
+	    });
+	    $input.focus();
+	});
+    }
+}
--- a/web/data/cubicweb.js	Thu Jul 08 18:48:44 2010 +0200
+++ b/web/data/cubicweb.js	Thu Jul 08 18:59:42 2010 +0200
@@ -296,9 +296,38 @@
             result.push(lst[i]);
         }
         return result;
+    },
+
+    /**
+     * .. function:: domid(string)
+     *
+     * return a valid DOM id from a string (should also be usable in jQuery
+     * search expression...). This is the javascript implementation of
+     * :func:`cubicweb.uilib.domid`.
+     */
+    domid: function (string) {
+	var newstring = string.replace(".", "_").replace("-", "_");
+	while (newstring != string) {
+	    string = newstring;
+	    newstring = newstring.replace(".", "_").replace("-", "_");
+	}
+	return newstring; // XXX
+    },
+
+    /**
+     * .. function:: strFuncCall(fname, *args)
+     *
+     * return a string suitable to call the `fname` javascript function with the
+     * given arguments (which should be correctly typed).. This is providing
+     * javascript implementation equivalent to :func:`cubicweb.uilib.js`.
+     */
+    strFuncCall: function(fname /* ...*/) {
+	    return (fname + '(' +
+		    $.map(cw.utils.sliceList(arguments, 1), jQuery.toJSON).join(',')
+		    + ')'
+		    );
     }
 
-
 });
 
 String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) {