# HG changeset patch # User Aurelien Campeas # Date 1278077219 -7200 # Node ID 8a129b3a5aff5d10a44a4d115b8f46e7113c3f1a # Parent c4380d8cfc253b486852997b3a8f455f0f2bfa2b 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 diff -r c4380d8cfc25 -r 8a129b3a5aff doc/book/en/devweb/edition/form.rst --- 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 ---------------------- diff -r c4380d8cfc25 -r 8a129b3a5aff doc/book/en/devweb/views/index.rst --- 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 diff -r c4380d8cfc25 -r 8a129b3a5aff doc/book/en/devweb/views/reledit.rst --- /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='') # 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'')}) + 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. + + + + + + diff -r c4380d8cfc25 -r 8a129b3a5aff web/data/cubicweb.edition.js --- 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) { diff -r c4380d8cfc25 -r 8a129b3a5aff web/data/cubicweb.reledit.js --- /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'); + } +}); diff -r c4380d8cfc25 -r 8a129b3a5aff web/uicfg.py --- 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 diff -r c4380d8cfc25 -r 8a129b3a5aff web/views/basecontrollers.py --- 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) diff -r c4380d8cfc25 -r 8a129b3a5aff web/views/editforms.py --- 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'') - _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'
' - % (divid, divid, divid)) - w(u'
%s
' % (divid, value)) - w(form.render(renderer=renderer)) - w(u'') - w(u'
') - - 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) diff -r c4380d8cfc25 -r 8a129b3a5aff web/views/reledit.py --- /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'') + _addmsg = _('click to add a value') + _deletezone = (u'%(msg)s') + _deletemsg = _('click to delete this value') + _editzone = (u'%(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'
' % + {'id': divid, + 'out': "jQuery('#%s').addClass('hidden')" % divid, + 'over': "jQuery('#%s').removeClass('hidden')" % divid}) + w(u'
' % divid) + w(value) + w(u'
') + w(form.render(renderer=renderer)) + w(u'') + w(u'
') + +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