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