[box] provide a new generic base box class to edit relation to simple entities, backported from the 'tag' cube
--- 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) {