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
--- 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