reledit refactoring
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Fri, 02 Jul 2010 15:26:59 +0200
changeset 5869 8a129b3a5aff
parent 5868 c4380d8cfc25
child 5870 d3ec7c4bb373
reledit refactoring * js handling rewritten to exploit all form capabilities (such as file upload ...) * attribute-like composite relations (one ttype, cardinality in '?1' from composite side) are more cutely handled, with a 'add'/'delete' additional actions/icons * a reledit_ctrl rtag to finely control: reloading, edition and default values * a proper chapter in the documentation (book) * many bugfixes
doc/book/en/devweb/edition/form.rst
doc/book/en/devweb/views/index.rst
doc/book/en/devweb/views/reledit.rst
web/data/cubicweb.edition.js
web/data/cubicweb.reledit.js
web/uicfg.py
web/views/basecontrollers.py
web/views/editforms.py
web/views/reledit.py
--- a/doc/book/en/devweb/edition/form.rst	Fri Jul 02 14:47:44 2010 +0200
+++ b/doc/book/en/devweb/edition/form.rst	Fri Jul 02 15:26:59 2010 +0200
@@ -1,3 +1,5 @@
+.. _webform:
+
 HTML form construction
 ----------------------
 
--- a/doc/book/en/devweb/views/index.rst	Fri Jul 02 14:47:44 2010 +0200
+++ b/doc/book/en/devweb/views/index.rst	Fri Jul 02 15:26:59 2010 +0200
@@ -12,6 +12,7 @@
    views
    basetemplates
    primary
+   reledit
    baseviews
    startup
    boxes
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/devweb/views/reledit.rst	Fri Jul 02 15:26:59 2010 +0200
@@ -0,0 +1,113 @@
+.. _reledit:
+
+The "Click and Edit" (also `reledit`) View
+------------------------------------------
+
+The principal way to update data through the Web UI is through the
+`modify` action on entities, which brings a full form. This is
+described in the :ref:`webform` chapter.
+
+There is however another way to perform piecewise edition of entities
+and relations, using a specific `reledit` (for *relation edition*)
+view from the :mod:`cubicweb.web.views.reledit` module.
+
+This is typically applied from the default Primary View (see
+:ref:`primary_view`) on the attributes and relation section. It makes
+small editions more convenient.
+
+Of course, this can be used customely in any other view. Here come
+some explanation about its capabilities and instructions on the way to
+use it.
+
+Using `reledit`
+***************
+
+Let's start again with a simple example:
+
+.. sourcecode:: python
+
+   class Company(EntityType):
+        name = String(required=True, unique=True)
+        boss = SubjectRelation('Person', cardinality='1*')
+        status = SubjectRelation('File', cardinality='?*', composite='subject')
+
+In some view code we might want to show these attributes/relations and
+allow the user to edit each of them in turn without having to leave
+the current page. We would write code as below:
+
+.. sourcecode:: python
+
+   company.view('reledit', rtype='name', default_value='<name>') # editable name attribute
+   company.view('reledit', rtype='boss') # editable boss relation
+   company.view('reledit', rtype='status') # editable attribute-like relation
+
+If one wanted to edit the company from a boss's point of view, one
+would have to indicate the proper relation's role. By default the role
+is `subject`.
+
+.. sourcecode:: python
+
+   person.view('reledit', rtype='boss', role='object')
+
+Each of these will provide with a different editing widget. The `name`
+attribute will obviously get a text input field. The `boss` relation
+will be edited through a selection box, allowing to pick another
+`Person` as boss. The `status` relation, given that it defines Company
+as a composite entity with one file inside, will provide additional actions
+
+* to `add` a `File` when there is one
+* to `delete` the `File` (if the cardinality allows it)
+
+Moreover, editing the relation or using the `add` action leads to an
+embedded edition/creation form allowing edition of the target entity
+(which is `File` in our example) instead of merely allowing to choose
+amongst existing files.
+
+The `reledit_ctrl` rtag
+***********************
+
+The behaviour of reledited attributes/relations can be finely
+controlled using the reledit_ctrl rtag, defined in
+:mod:`cubicweb.web.uicfg`.
+
+This rtag provides three control variables:
+
+* ``default_value``
+* ``reload``, to specificy if edition of the relation entails a full page
+  reload, which defaults to False
+* ``noedit``, to explicitly inhibit edition
+
+Let's see how to use these controls.
+
+.. sourcecode:: python
+
+    from logilab.mtconverter import xml_escape
+    from cubicweb.web.uicfg import reledit_ctrl
+    reledit_ctrl.tag_attribute(('Company', 'name'),
+                               {'reload': lambda x:x.eid,
+                                'default_value': xml_escape(u'<logilab tastes better>')})
+    reledit_ctrl.tag_object_of(('*', 'boss', 'Person'), {'noedit': True})
+
+The `default_value` needs to be an xml escaped unicode string.
+
+The `noedit` attribute is convenient to programmatically disable some
+relation edition on views that apply it systematically (the prime
+example being the primary view). Here we use it to forbid changing the
+`boss` relation from a `Person` side (as it could have unwanted
+effects).
+
+Finally, the `reload` key accepts either a boolean, an eid or an
+unicode string representing an url. If an eid is provided, it will be
+internally transformed into an url. The eid/url case helps when one
+needs to reload and the current url is inappropriate. A common case is
+edition of a key attribute, which is part of the current url. If one
+user changed the Company's name from `lozilab` to `logilab`, reloading
+on http://myapp/company/lozilab would fail. Providing the entity's
+eid, then, forces to reload on something like http://myapp/company/42,
+which always work.
+
+
+
+
+
+
--- a/web/data/cubicweb.edition.js	Fri Jul 02 14:47:44 2010 +0200
+++ b/web/data/cubicweb.edition.js	Fri Jul 02 15:26:59 2010 +0200
@@ -19,6 +19,7 @@
  * * `varname`, the name of the variable as used in the original creation form
  * * `tabindex`, the tabindex that should be set on the widget
  */
+
 function setPropValueWidget(varname, tabindex) {
     var key = firstSelected(document.getElementById('pkey:' + varname));
     if (key) {
@@ -514,7 +515,7 @@
 function postForm(bname, bvalue, formid) {
     var form = cw.getNode(formid);
     if (bname) {
-        var child = form.append(INPUT({
+        var child = form.appendChild(INPUT({
             type: 'hidden',
             name: bname,
             value: bvalue
@@ -526,7 +527,6 @@
     }
     if (bname) {
         jQuery(child).remove();
-        /* cleanup */
     }
 }
 
@@ -597,6 +597,9 @@
 }
 
 
+
+// ======================= DEPRECATED FUNCTIONS ========================= //
+// (mostly reledit related)
 /**
  * .. function:: inlineValidateRelationFormOptions(rtype, eid, divid, options)
  *
@@ -617,47 +620,73 @@
  *
  *     * `lzone`, html fragment (string) for a clic-zone triggering actual edition
  */
-function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
-    try {
-        var form = cw.getNode(divid + '-form');
-        var relname = rtype + ':' + eid;
-        var newtarget = jQuery('[name=' + relname + ']').val();
-        var zipped = cw.utils.formContents(form);
-        var args = ajaxFuncArgs('validate_form', null, 'apply', zipped[0], zipped[1]);
-        var d = loadRemote(JSON_BASE_URL, args, 'POST')
-    } catch(ex) {
-        return false;
+
+
+showInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function showInlineEditionForm(eid, rtype, divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
     }
-    d.addCallback(function(result, req) {
-        execFormValidationResponse(rtype, eid, divid, options, result);
-    });
-    return false;
-}
+);
+
+hideInlineEdit = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function hideInlineEdit(eid, rtype, divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
+    }
+);
+
 
-function execFormValidationResponse(rtype, eid, divid, options, result) {
-    options = $.extend({onsuccess: noop,
-                        onfailure: noop
-                       }, options);
-    if (handleFormValidationResponse(divid + '-form', options.onsucess , options.onfailure, result)) {
-        if (options.reload) {
-            document.location.reload();
-        } else {
-            var args = {
-                fname: 'reledit_form',
-                rtype: rtype,
-                role: options.role,
-                eid: eid,
-                divid: divid,
-                reload: options.reload,
-                vid: options.vid,
-                default_value: options.default_value,
-                landing_zone: options.lzone
-            };
-            jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+inlineValidateRelationFormOptions = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
+        try {
+            var form = cw.getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = cw.utils.formContents(form);
+            var args = ajaxFuncArgs('validate_form', null, 'apply', zipped[0], zipped[1]);
+            var d = loadRemote(JSON_BASE_URL, args, 'POST');
+        } catch(ex) {
+            return false;
         }
-    }
+        d.addCallback(function(result, req) {
+            execFormValidationResponse(rtype, eid, divid, options, result);
+        });
+        return false;
+    });
 
-}
+execFormValidationResponse = cw.utils.deprecatedFunction(
+    '[3.9] this is now unused by reledit (see cw.reledit.js)',
+    function execFormValidationResponse(rtype, eid, divid, options, result) {
+        options = $.extend({onsuccess: noop,
+                            onfailure: noop
+                           }, options);
+        if (handleFormValidationResponse(divid + '-form', options.onsucess , options.onfailure, result)) {
+            if (options.reload) {
+                document.location.reload();
+            } else {
+                var args = {
+                    fname: 'reledit_form',
+                    rtype: rtype,
+                    role: options.role,
+                    eid: eid,
+                    divid: divid,
+                    reload: options.reload,
+                    vid: options.vid,
+                    default_value: options.default_value,
+                    landing_zone: options.lzone
+                };
+                jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+            }
+        }
+});
 
 
 /**
@@ -665,7 +694,9 @@
  *
  * inline edition
  */
-function loadInlineEditionFormOptions(eid, rtype, divid, options) {
+loadInlineEditionFormOptions = cw.utils.deprecatedFunction(
+  '[3.9] this is now unused by reledit (see cw.reledit.js) ',
+  function loadInlineEditionFormOptions(eid, rtype, divid, options) {
     var args = {
         fname: 'reledit_form',
         rtype: rtype,
@@ -681,23 +712,9 @@
         }
     };
     jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-}
-function showInlineEditionForm(eid, rtype, divid) {
-    jQuery('#' + divid).hide();
-    jQuery('#' + divid + '-value').hide();
-    jQuery('#' + divid + '-form').show();
-}
-
-function hideInlineEdit(eid, rtype, divid) {
-    jQuery('#appMsg').hide();
-    jQuery('div.errorMessage').remove();
-    jQuery('#' + divid).show();
-    jQuery('#' + divid + '-value').show();
-    jQuery('#' + divid + '-form').hide();
-}
+});
 
 
-// ======================= DEPRECATED FUNCTIONS ========================= //
 inlineValidateRelationForm = cw.utils.deprecatedFunction(
     '[3.9] inlineValidateRelationForm() function is deprecated, use inlineValidateRelationFormOptions instead',
     function(rtype, role, eid, divid, reload, vid, default_value, lzone, onsucess, onfailure) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.reledit.js	Fri Jul 02 15:26:59 2010 +0200
@@ -0,0 +1,73 @@
+cw.reledit = new Namespace('cw.reledit');
+
+
+jQuery.extend(cw.reledit, {
+
+    /* Unhides the part of reledit div containing the form
+     * hides other parts
+     */
+    showInlineEditionForm: function (divid) {
+        jQuery('#' + divid).hide();
+        jQuery('#' + divid + '-value').hide();
+        jQuery('#' + divid + '-form').show();
+      },
+
+    /* Hides and removes edition parts, incl. messages
+     * show initial widget state
+     */
+    cleanupAfterCancel: function (divid) {
+        jQuery('#appMsg').hide();
+        jQuery('div.errorMessage').remove();
+        jQuery('#' + divid).show();
+        jQuery('#' + divid + '-value').show();
+        jQuery('#' + divid + '-form').hide();
+    },
+
+    /* callback used on form validation success
+     * refreshes the whole page or just the edited reledit zone
+     * @param results: [status, ...]
+     * @param formid: the dom id of the reledit form
+     * @param cbargs: ...
+     */
+     onSuccess: function (results, formid, cbargs) {
+        var params = {fname: 'reledit_form'};
+        jQuery('#' + formid + ' input:hidden').each(function (elt) {
+            var name = jQuery(this).attr('name');
+            if (name && name.startswith('__reledit|')) {
+                params[name.split('|')[1]] = this.value;
+            }
+        });
+        var reload = cw.evalJSON(params.reload);
+        if (reload || (params.formid == 'deleteconf')) {
+            if (typeof reload == 'string') {
+                /* Sometimes we want to reload but the reledit thing
+                 * updated a key attribute which was a component of the
+                 * url
+                 */
+                document.location.href = reload;
+                return;
+            }
+            else {
+                document.location.reload();
+                return;
+            }
+        }
+        jQuery('#'+params.divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, params, 'post');
+    },
+
+    /* called by reledit forms to submit changes
+     * @param formid : the dom id of the form used
+     * @param rtype : the attribute being edited
+     * @param eid : the eid of the entity being edited
+     * @param reload: boolean to reload page if true (when changing URL dependant data)
+     * @param default_value : value if the field is empty
+     */
+    loadInlineEditionForm: function(formid, eid, rtype, role, divid, reload, vid, default_value) {
+        var args = {fname: 'reledit_form', rtype: rtype, role: role,
+                    pageid: pageid,
+    	            eid: eid, divid: divid, formid: formid,
+    		    reload: reload, vid: vid, default_value: default_value,
+    		    callback: function () {cw.reledit.showInlineEditionForm(divid);}};
+       jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+    }
+});
--- a/web/uicfg.py	Fri Jul 02 14:47:44 2010 +0200
+++ b/web/uicfg.py	Fri Jul 02 15:26:59 2010 +0200
@@ -389,6 +389,32 @@
 # permissions checking is by-passed and supposed to be ok
 autoform_permissions_overrides = RelationTagsSet('autoform_permissions_overrides')
 
+class _ReleditTags(RelationTagsDict):
+    _keys = frozenset('reload default_value noedit'.split())
+
+    def tag_subject_of(self, key, *args, **kwargs):
+        subj, rtype, obj = key
+        if obj != '*':
+            self.warning('using explict target type in display_ctrl.tag_subject_of() '
+                         'has no effect, use (%s, %s, "*") instead of (%s, %s, %s)',
+                         subj, rtype, subj, rtype, obj)
+        super(_ReleditTags, self).tag_subject_of(key, *args, **kwargs)
+
+    def tag_object_of(self, key, *args, **kwargs):
+        subj, rtype, obj = key
+        if subj != '*':
+            self.warning('using explict subject type in display_ctrl.tag_object_of() '
+                         'has no effect, use ("*", %s, %s) instead of (%s, %s, %s)',
+                         rtype, obj, subj, rtype, obj)
+        super(_ReleditTags, self).tag_object_of(key, *args, **kwargs)
+
+    def tag_relation(self, key, tag):
+        for tagkey in tag.iterkeys():
+            assert tagkey in self._keys
+        return super(_ReleditTags, self).tag_relation(key, tag)
+
+reledit_ctrl = _ReleditTags('reledit')
+
 # boxes.EditBox configuration #################################################
 
 # 'link' / 'create' relation tags, used to control the "add entity" submenu
--- a/web/views/basecontrollers.py	Fri Jul 02 14:47:44 2010 +0200
+++ b/web/views/basecontrollers.py	Fri Jul 02 15:26:59 2010 +0200
@@ -457,12 +457,12 @@
     def js_reledit_form(self):
         req = self._cw
         args = dict((x, req.form[x])
-                    for x in frozenset(('rtype', 'role', 'reload', 'landing_zone')))
-        entity = req.entity_from_eid(typed_eid(req.form['eid']))
-        # note: default is reserved in js land
-        args['default'] = req.form['default_value']
-        args['reload'] = json.loads(args['reload'])
-        rset = req.eid_rset(typed_eid(req.form['eid']))
+                    for x in ('formid', 'rtype', 'role', 'reload', 'default_value'))
+        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('doreledit', req, rset=rset, rtype=args['rtype'])
         return self._call_view(view, **args)
 
--- a/web/views/editforms.py	Fri Jul 02 14:47:44 2010 +0200
+++ b/web/views/editforms.py	Fri Jul 02 15:26:59 2010 +0200
@@ -26,6 +26,7 @@
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cached
+from logilab.common.deprecation import class_moved
 
 from cubicweb import tags
 from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
@@ -35,7 +36,7 @@
 from cubicweb.web import uicfg, stdmsgs, eid_param, dumps, \
      formfields as ff, formwidgets as fw
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
-from cubicweb.web.views import forms
+from cubicweb.web.views import forms, reledit
 
 _pvdc = uicfg.primaryview_display_ctrl
 
@@ -260,215 +261,5 @@
 
 # click and edit handling ('reledit') ##########################################
 
-class DummyForm(object):
-    __slots__ = ('event_args',)
-    def form_render(self, **_args):
-        return u''
-    def render(self, **_args):
-        return u''
-    def append_field(self, *args):
-        pass
-    def field_by_name(self, rtype, role, eschema=None):
-        return None
-
-
-class ClickAndEditFormView(FormViewMixIn, EntityView):
-    """form used to permit ajax edition of a relation or attribute of an entity
-    in a view, if logged user have the permission to edit it.
-
-    (double-click on the field to see an appropriate edition widget).
-    """
-    __regid__ = 'doreledit'
-    __select__ = non_final_entity() & match_kwargs('rtype')
-    # FIXME editableField class could be toggleable from userprefs
-
-    _onclick = u"showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
-    _onsubmit = ("return inlineValidateRelationFormOptions('%(rtype)s', '%(eid)s', "
-                 "'%(divid)s', %(options)s);")
-    _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
-    _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
-                           'alt="%(msg)s"/>')
-    _landingzonemsg = _('click to edit this field')
-    # default relation vids according to cardinality
-    _one_rvid = 'incontext'
-    _many_rvid = 'csv'
-
-
-    def cell_call(self, row, col, rtype=None, role='subject',
-                  reload=False,      # controls reloading the whole page after change
-                  rvid=None,         # vid to be applied to other side of rtype (non final relations only)
-                  default=None,      # default value
-                  landing_zone=None  # prepend value with a separate html element to click onto
-                                     # (esp. needed when values are links)
-                  ):
-        """display field to edit entity's `rtype` relation on click"""
-        assert rtype
-        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
-        self._cw.add_js('cubicweb.edition.js')
-        self._cw.add_css('cubicweb.form.css')
-        if default is None:
-            default = xml_escape(self._cw._('<%s not specified>')
-                                 % display_name(self._cw, rtype, role))
-        schema = self._cw.vreg.schema
-        entity = self.cw_rset.get_entity(row, col)
-        rschema = schema.rschema(rtype)
-        lzone = self._build_landing_zone(landing_zone)
-        # compute value, checking perms, build form
-        if rschema.final:
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone)
-            if not self.should_edit_attribute(entity, rschema, form):
-                self.w(entity.printable_value(rtype))
-                return
-            value = entity.printable_value(rtype) or default
-        else:
-            rvid = self._compute_best_vid(entity.e_schema, rschema, role)
-            rset = entity.related(rtype, role)
-            if rset:
-                value = self._cw.view(rvid, rset)
-            else:
-                value = default
-            if not self.should_edit_relation(entity, rschema, role, rvid):
-                if rset:
-                    self.w(value)
-                return
-            # XXX do we really have to give lzone twice?
-            form = self._build_form(entity, rtype, role, 'base', default, reload, lzone,
-                                    dict(vid=rvid, lzone=lzone))
-        field = form.field_by_name(rtype, role, entity.e_schema)
-        form.append_field(field)
-        self.relation_form(lzone, value, form,
-                           self._build_renderer(entity, rtype, role))
-
-    def should_edit_attribute(self, entity, rschema, form):
-        if not entity.cw_has_perm('update'):
-            return False
-        rdef = entity.e_schema.rdef(rschema)
-        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
-            return False
-        try:
-            form.field_by_name(str(rschema), 'subject', entity.e_schema)
-        except FieldNotFound:
-            return False
-        return True
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        if ((role == 'subject' and not rschema.has_perm(self._cw, 'add',
-                                                        fromeid=entity.eid))
-            or
-            (role == 'object' and not rschema.has_perm(self._cw, 'add',
-                                                       toeid=entity.eid))):
-            return False
-        return True
-
-    def relation_form(self, lzone, value, form, renderer):
-        """xxx-reledit div (class=field)
-              +-xxx div (class="editableField")
-              |   +-landing zone
-              +-xxx-value div
-              +-xxx-form div
-        """
-        w = self.w
-        divid = form.event_args['divid']
-        w(u'<div id="%s-reledit" class="field" '
-          u'onmouseout="jQuery(\'#%s\').addClass(\'hidden\')" '
-          u'onmouseover="jQuery(\'#%s\').removeClass(\'hidden\')">'
-          % (divid, divid, divid))
-        w(u'<div id="%s-value" class="editableFieldValue">%s</div>' % (divid, value))
-        w(form.render(renderer=renderer))
-        w(u'<div id="%s" class="editableField hidden" onclick="%s" title="%s">' % (
-                divid, xml_escape(self._onclick % form.event_args),
-                self._cw._(self._landingzonemsg)))
-        w(lzone)
-        w(u'</div>')
-        w(u'</div>')
-
-    def _compute_best_vid(self, eschema, rschema, role):
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        if dispctrl.get('rvid'):
-            return dispctrl['rvid']
-        if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
-            return self._many_rvid
-        return self._one_rvid
-
-    def _build_landing_zone(self, lzone):
-        return lzone or self._defaultlandingzone % {
-            'msg': xml_escape(self._cw._(self._landingzonemsg))}
-
-    def _build_renderer(self, entity, rtype, role):
-        return self._cw.vreg['formrenderers'].select(
-            'base', self._cw, entity=entity, display_label=False,
-            display_help=False, table_class='',
-            button_bar_class='buttonbar', display_progress_div=False)
-
-    def _build_args(self, entity, rtype, role, formid, default, reload, lzone,
-                    extradata=None):
-        divid = '%s-%s-%s' % (rtype, role, entity.eid)
-        options = {'reload' : reload, 'default_value' : default,
-                   'role' : role, 'vid' : '',
-                   'lzone' : lzone}
-        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype,
-                      'options' : dumps(options)}
-        if extradata:
-            event_args.update(extradata)
-        return divid, event_args
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                             reload, lzone, extradata)
-        onsubmit = self._onsubmit % event_args
-        cancelclick = self._cancelclick % (entity.eid, rtype, divid)
-        form = self._cw.vreg['forms'].select(
-            formid, self._cw, entity=entity, domid='%s-form' % divid,
-            cssstyle='display: none', onsubmit=onsubmit, action='#',
-            form_buttons=[fw.SubmitButton(),
-                          fw.Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)],
-            **formargs)
-        form.event_args = event_args
-        return form
-
-
-class AutoClickAndEditFormView(ClickAndEditFormView):
-    """same as ClickAndEditFormView but checking if the view *should* be applied
-    by checking uicfg configuration and composite relation property.
-    """
-    __regid__ = 'reledit'
-    _onclick = (u"loadInlineEditionFormOptions(%(eid)s, '%(rtype)s', "
-                "'%(divid)s', %(options)s);")
-
-    def should_edit_attribute(self, entity, rschema, form):
-        rdef = entity.e_schema.rdef(rschema)
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, 'subject', rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_attribute(
-            entity, rschema, form)
-
-    def should_edit_relation(self, entity, rschema, role, rvid):
-        eschema = entity.e_schema
-        dispctrl = _pvdc.etype_get(eschema, rschema, role)
-        vid = dispctrl.get('vid', 'reledit')
-        if vid != 'reledit': # reledit explicitly disabled
-            return False
-        rdef = eschema.rdef(rschema, role)
-        if rdef.composite == role:
-            return False
-        afs = uicfg.autoform_section.etype_get(
-            entity.__regid__, rschema, role, rdef.object)
-        if 'main_hidden' in afs:
-            return False
-        return super(AutoClickAndEditFormView, self).should_edit_relation(
-            entity, rschema, role, rvid)
-
-    def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
-                  extradata=None, **formargs):
-        _divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                              reload, lzone, extradata)
-        form = DummyForm()
-        form.event_args = event_args
-        return form
-
-    def _build_renderer(self, entity, rtype, role):
-        pass
-
+ClickAndEditFormView = class_moved(reledit.ClickAndEditFormView)
+AutoClickAndEditFormView = class_moved(reledit.AutoClickAndEditFormView)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/reledit.py	Fri Jul 02 15:26:59 2010 +0200
@@ -0,0 +1,338 @@
+import copy
+
+from logilab.mtconverter import xml_escape
+
+from cubicweb import neg_role
+from cubicweb.schema import display_name
+from cubicweb.utils import json
+from cubicweb.selectors import non_final_entity, match_kwargs
+from cubicweb.view import EntityView
+from cubicweb.web import uicfg, stdmsgs
+from cubicweb.web.form import FormViewMixIn, FieldNotFound
+from cubicweb.web.formwidgets import Button, SubmitButton
+
+class DummyForm(object):
+    __slots__ = ('event_args',)
+    def form_render(self, **_args):
+        return u''
+    def render(self, *_args, **_kwargs):
+        return u''
+    def append_field(self, *args):
+        pass
+    def field_by_name(self, rtype, role, eschema=None):
+        return None
+
+class ClickAndEditFormView(FormViewMixIn, EntityView):
+    __regid__ = 'doreledit'
+    __select__ = non_final_entity() & match_kwargs('rtype')
+
+    # ui side continuations
+    _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', "
+                "'%(divid)s', %(reload)s, '%(vid)s', '%(default_value)s');")
+    _cancelclick = "cw.reledit.cleanupAfterCancel('%s')"
+
+    # ui side actions/buttons
+    _addzone = (u'<img title="%(msg)s" src="data/plus.png" '
+                u'alt="%(msg)s"/>')
+    _addmsg = _('click to add a value')
+    _deletezone = (u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>')
+    _deletemsg = _('click to delete this value')
+    _editzone = (u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>')
+    _editzonemsg = _('click to edit this field')
+
+    # default relation vids according to cardinality
+    _one_rvid = 'incontext'
+    _many_rvid = 'csv'
+
+    def cell_call(self, row, col, rtype=None, role='subject',
+                  reload=False, # controls reloading the whole page after change
+                                # boolean, eid (to redirect), or
+                                # function taking the subject entity & returning a boolean or an eid
+                  rvid=None,    # vid to be applied to other side of rtype (non final relations only)
+                  default_value=None,
+                  formid=None
+                  ):
+        """display field to edit entity's `rtype` relation on click"""
+        assert rtype
+        assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
+        if self.__regid__ == 'doreledit':
+            assert formid
+        self._cw.add_js('cubicweb.reledit.js')
+        if formid:
+            self._cw.add_js('cubicweb.edition.js')
+        self._cw.add_css('cubicweb.form.css')
+        entity = self.cw_rset.get_entity(row, col)
+        rschema = self._cw.vreg.schema[rtype]
+        reload = self._compute_reload(entity, rschema, role, reload)
+        default_value = self._compute_default_value(entity, rschema, role, default_value)
+        # compute value, checking perms, build & display form
+        divid = self._build_divid(rtype, role, entity.eid)
+        if rschema.final:
+            value = entity.printable_value(rtype)
+            form, renderer = self._build_form(entity, rtype, role, divid, 'base',
+                                              default_value, reload)
+            if not self._should_edit_attribute(entity, rschema, form):
+                self.w(value)
+                return
+            value = value or default_value
+            field = form.field_by_name(rtype, role, entity.e_schema)
+            form.append_field(field)
+            self.view_form(divid, value, form, renderer)
+        else:
+            rvid = self._compute_best_vid(entity.e_schema, rschema, role)
+            related_rset = entity.related(rtype, role)
+            if related_rset:
+                value = self._cw.view(rvid, related_rset)
+            else:
+                value = default_value
+            ttypes = self._compute_ttypes(rschema, role)
+
+            if not self._should_edit_relation(entity, rschema, role):
+                self.w(value)
+                return
+            # this is for attribute-like composites (1 target type, 1 related entity at most)
+            edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
+            delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
+            add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
+            # compute formid
+            if len(ttypes) > 1: # redundant safety belt
+                formid = 'base'
+            else:
+                afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0])
+                # is there an afs spec that says we should edit
+                # the rschema as an attribute ?
+                if afs and 'main_attributes' in afs:
+                    formid = 'base'
+
+            form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value,
+                                              reload, dict(vid=rvid),
+                                              edit_related, add_related and ttypes[0])
+            self.view_form(divid, value, form, renderer, edit_related,
+                           delete_related, add_related)
+
+    def _compute_best_vid(self, eschema, rschema, role):
+        if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
+            return self._many_rvid
+        return self._one_rvid
+
+    def _compute_ttypes(self, rschema, role):
+        dual_role = neg_role(role)
+        return getattr(rschema, '%ss' % dual_role)()
+
+    def _compute_reload(self, entity, rschema, role, reload):
+        rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
+        ctrl_reload = rule.get('reload', reload)
+        if callable(ctrl_reload):
+            ctrl_reload = ctrl_reload(entity)
+        if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
+            ctrl_reload = self._cw.build_url(ctrl_reload)
+        return ctrl_reload
+
+    def _compute_default_value(self, entity, rschema, role, default_value):
+        etype = entity.e_schema.type
+        rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*')
+        ctrl_default = rule.get('default_value', default_value)
+        if ctrl_default:
+            return ctrl_default
+        if default_value is None:
+            return xml_escape(self._cw._('<%s not specified>') %
+                              display_name(self._cw, rschema.type, role))
+        return default_value
+
+    def _is_composite(self, eschema, rschema, role):
+        return eschema.rdef(rschema, role).composite == role
+
+    def _may_add_related(self, rset, entity, rschema, role, ttypes):
+        """ ok for attribute-like composite entities """
+        if self._is_composite(entity.e_schema, rschema, role):
+            if len(ttypes) > 1: # wrong cardinality: do not handle
+                return False
+            ttype = ttypes[0]
+            if rset:
+                eschema = entity.e_schema
+                if role == 'subject':
+                    source, target = eschema.type, ttype
+                else:
+                    source, target = ttype, eschema.type
+                card = rschema.rdef(source, target).role_cardinality(role)
+                if card in '1?':
+                    return False
+            if rschema.has_perm(self._cw, 'add', toetype=ttype):
+                return True
+        return False
+
+    def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
+        """ controls the edition of the related entity """
+        if len(ttypes) > 1:
+            return False
+        if not self._is_composite(entity.e_schema, rschema, role):
+            return False
+        if len(related_rset.rows) != 1:
+            return False
+        return related_rset.get_entity(0, 0).cw_has_perm('update')
+
+    def _may_delete_related(self, related_rset, entity, rschema, role):
+        # we assume may_edit_related
+        assert len(related_rset.rows) == 1
+        kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+        if not rschema.has_perm(self._cw, 'delete', **kwargs):
+            return False
+        for related_entity in related_rset.entities():
+            if not related_entity.cw_has_perm('delete'):
+                return False
+        return True
+
+    def _build_edit_zone(self):
+        return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
+
+    def _build_delete_zone(self):
+        return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))}
+
+    def _build_add_zone(self):
+        return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))}
+
+    def _build_divid(self, rtype, role, entity_eid):
+        """ builds an id for the root div of a reledit widget """
+        return '%s-%s-%s' % (rtype, role, entity_eid)
+
+    def _build_args(self, entity, rtype, role, formid, default_value, reload,
+                    extradata=None):
+        divid = self._build_divid(rtype, role, entity.eid)
+        event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid,
+                      'reload' : json.dumps(reload), 'default_value' : default_value,
+                      'role' : role, 'vid' : u''}
+        if extradata:
+            event_args.update(extradata)
+        return event_args
+
+    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+                    extradata=None, edit_related=False, add_related=False, **formargs):
+        event_args = self._build_args(entity, rtype, role, formid, default_value,
+                                      reload, extradata)
+        cancelclick = self._cancelclick % divid
+        if edit_related:
+            display_fields = None
+            display_label = True
+            related_entity = entity.related(rtype, role).get_entity(0, 0)
+            self._cw.form['eid'] = related_entity.eid
+        elif add_related:
+            display_fields = None
+            display_label = True
+            _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
+            _new_entity.eid = self._cw.varmaker.next()
+            related_entity = _new_entity
+            self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
+        else: # base case: edition/attribute relation
+            display_fields = [(rtype, role)]
+            display_label = False
+            related_entity = entity
+        form = self._cw.vreg['forms'].select(
+            formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid,
+            display_fields=display_fields, formtype='inlined',
+            action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'),
+            cwtarget='eformframe', cssstyle='display: none',
+            **formargs)
+        # pass reledit arguments
+        for pname, pvalue in event_args.iteritems():
+            form.add_hidden('__reledit|' + pname, pvalue)
+        # handle buttons
+        if form.form_buttons: # edition, delete
+            form_buttons = []
+            for button in form.form_buttons:
+                if not button.label.endswith('apply'):
+                    if button.label.endswith('cancel'):
+                        button = copy.deepcopy(button)
+                        button.cwaction = None
+                        button.onclick = cancelclick
+                    form_buttons.append(button)
+            form.form_buttons = form_buttons
+        else: # base
+            form.form_buttons = [SubmitButton(),
+                                 Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
+        form.event_args = event_args
+        renderer = self._cw.vreg['formrenderers'].select(
+            'base', self._cw, entity=related_entity, display_label=display_label,
+            display_help=False, table_class='',
+            button_bar_class='buttonbar', display_progress_div=False)
+        return form, renderer
+
+    def _should_edit_attribute(self, entity, rschema, form):
+        # examine rtags
+        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False)
+        if noedit:
+            return False
+        rdef = entity.e_schema.rdef(rschema)
+        afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object)
+        if 'main_hidden' in  afs:
+            return False
+        # check permissions
+        if not entity.cw_has_perm('update'):
+            return False
+        rdef = entity.e_schema.rdef(rschema)
+        if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
+            return False
+        # XXX ?
+        try:
+            form.field_by_name(str(rschema), 'subject', entity.e_schema)
+        except FieldNotFound:
+            return False
+        return True
+
+    def _should_edit_relation(self, entity, rschema, role):
+        # examine rtags
+        rtype = rschema.type
+        noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False)
+        if noedit:
+            return False
+        rdef = entity.e_schema.rdef(rschema, role)
+        afs = uicfg.autoform_section.etype_get(
+            entity.__regid__, rschema, role, rdef.object)
+        if 'main_hidden' in afs:
+            return False
+        perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
+        return rschema.has_perm(self._cw, 'add', **perm_args)
+
+    def view_form(self, divid, value, form=None, renderer=None,
+                  edit_related=False, delete_related=False, add_related=False):
+        w = self.w
+        w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' %
+          {'id': divid,
+           'out': "jQuery('#%s').addClass('hidden')" % divid,
+           'over': "jQuery('#%s').removeClass('hidden')" % divid})
+        w(u'<div id="%s-value" class="editableFieldValue">' % divid)
+        w(value)
+        w(u'</div>')
+        w(form.render(renderer=renderer))
+        w(u'<div id="%s" class="editableField hidden">' % divid)
+        args = form.event_args.copy()
+        if not add_related: # excludes edition
+            args['formid'] = 'edition'
+            w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
+            w(self._build_edit_zone())
+            w(u'</div>')
+        else:
+            args['formid'] = 'edition'
+            w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
+            w(self._build_add_zone())
+            w(u'</div>')
+        if delete_related:
+            args['formid'] = 'deleteconf'
+            w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
+              (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
+            w(self._build_delete_zone())
+            w(u'</div>')
+        w(u'</div>')
+        w(u'</div>')
+
+class AutoClickAndEditFormView(ClickAndEditFormView):
+    __regid__ = 'reledit'
+
+    def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
+                  extradata=None, edit_related=False, add_related=False, **formargs):
+        event_args = self._build_args(entity, rtype, role, 'base', default_value,
+                                      reload, extradata)
+        form = DummyForm()
+        form.event_args = event_args
+        return form, None