# HG changeset patch # User Sylvain Thénault # Date 1278608382 -7200 # Node ID 2a273c896a38b5fdfc2239d861c14682cfdb1bc0 # Parent 4154bdc85fe4c057e7dab10b2981e23bb9c4b33a [box] provide a new generic base box class to edit relation to simple entities, backported from the 'tag' cube diff -r 4154bdc85fe4 -r 2a273c896a38 test/unittest_uilib.py --- 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 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() diff -r 4154bdc85fe4 -r 2a273c896a38 uilib.py --- 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')) diff -r 4154bdc85fe4 -r 2a273c896a38 web/box.py --- 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 . -"""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'\n') diff -r 4154bdc85fe4 -r 2a273c896a38 web/data/cubicweb.ajax.box.js --- /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(); + }); + } +} diff -r 4154bdc85fe4 -r 2a273c896a38 web/data/cubicweb.js --- 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) {