diff -r 058bb3dc685f -r 0b59724cb3f2 web/views/reledit.py --- a/web/views/reledit.py Mon Jan 04 18:40:30 2016 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,404 +0,0 @@ -# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. -# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr -# -# This file is part of CubicWeb. -# -# CubicWeb is free software: you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation, either version 2.1 of the License, or (at your option) -# any later version. -# -# CubicWeb is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License along -# with CubicWeb. If not, see . -"""edit entity attributes/relations from any view, without going to the entity -form -""" - -__docformat__ = "restructuredtext en" -from cubicweb import _ - -import copy -from warnings import warn - -from logilab.mtconverter import xml_escape -from logilab.common.deprecation import deprecated, class_renamed -from logilab.common.decorators import cached - -from cubicweb import neg_role -from cubicweb.schema import display_name -from cubicweb.utils import json, json_dumps -from cubicweb.predicates import non_final_entity, match_kwargs -from cubicweb.view import EntityView -from cubicweb.web import stdmsgs -from cubicweb.web.views import uicfg -from cubicweb.web.form import FieldNotFound -from cubicweb.web.formwidgets import Button, SubmitButton -from cubicweb.web.views.ajaxcontroller import ajaxfunc - -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 add_hidden(self, *args): - pass - -class AutoClickAndEditFormView(EntityView): - __regid__ = 'reledit' - __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', '%(action)s');") - _cancelclick = "cw.reledit.cleanupAfterCancel('%s')" - - # ui side actions/buttons - _addzone = u'%(msg)s' - _addmsg = _('click to add a value') - _addlogo = 'plus.png' - _deletezone = u'%(msg)s' - _deletemsg = _('click to delete this value') - _deletelogo = 'cancel.png' - _editzone = u'%(msg)s' - _editzonemsg = _('click to edit this field') - _editlogo = 'pen_icon.png' - - # renderer - _form_renderer_id = 'base' - - def entity_call(self, entity, 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='base', - action=None - ): - """display field to edit entity's `rtype` relation on click""" - assert rtype - self._cw.add_css('cubicweb.form.css') - self._cw.add_js(('cubicweb.reledit.js', 'cubicweb.edition.js', 'cubicweb.ajax.js')) - self.entity = entity - rschema = self._cw.vreg.schema[rtype] - rctrl = self._cw.vreg['uicfg'].select('reledit', self._cw, entity=entity) - self._rules = rctrl.etype_get(self.entity.e_schema.type, rschema.type, role, '*') - reload = self._compute_reload(rschema, role, reload) - divid = self._build_divid(rtype, role, self.entity.eid) - if rschema.final: - self._handle_attribute(rschema, role, divid, reload, action) - else: - if self._is_composite(): - self._handle_composite(rschema, role, divid, reload, formid, action) - else: - self._handle_relation(rschema, role, divid, reload, formid, action) - - def _handle_attribute(self, rschema, role, divid, reload, action): - rvid = self._rules.get('rvid', None) - if rvid is not None: - value = self._cw.view(rvid, entity=self.entity, - rtype=rschema.type, role=role) - else: - value = self.entity.printable_value(rschema.type) - if not self._should_edit_attribute(rschema): - self.w(value) - return - form, renderer = self._build_form(self.entity, rschema, role, divid, - 'base', reload, action) - value = value or self._compute_default_value(rschema, role) - self.view_form(divid, value, form, renderer) - - def _compute_formid_value(self, rschema, role, rvid, formid): - related_rset = self.entity.related(rschema.type, role) - if related_rset: - value = self._cw.view(rvid, related_rset) - else: - value = self._compute_default_value(rschema, role) - if not self._should_edit_relation(rschema, role): - return None, value - return formid, value - - def _handle_relation(self, rschema, role, divid, reload, formid, action): - rvid = self._rules.get('rvid', 'autolimited') - formid, value = self._compute_formid_value(rschema, role, rvid, formid) - if formid is None: - return self.w(value) - form, renderer = self._build_form(self.entity, rschema, role, divid, formid, - reload, action, dict(vid=rvid)) - self.view_form(divid, value, form, renderer) - - def _handle_composite(self, rschema, role, divid, reload, formid, action): - # this is for attribute-like composites (1 target type, 1 related entity at most, for now) - entity = self.entity - related_rset = entity.related(rschema.type, role) - add_related = self._may_add_related(related_rset, rschema, role) - edit_related = self._may_edit_related_entity(related_rset, rschema, role) - delete_related = edit_related and self._may_delete_related(related_rset, rschema, role) - rvid = self._rules.get('rvid', 'autolimited') - formid, value = self._compute_formid_value(rschema, role, rvid, formid) - if formid is None or not (edit_related or add_related): - # till we learn to handle cases where not (edit_related or add_related) - self.w(value) - return - form, renderer = self._build_form(entity, rschema, role, divid, formid, - reload, action, dict(vid=rvid)) - self.view_form(divid, value, form, renderer, - edit_related, add_related, delete_related) - - @cached - def _compute_ttypes(self, rschema, role): - dual_role = neg_role(role) - return getattr(rschema, '%ss' % dual_role)() - - def _compute_reload(self, rschema, role, reload): - ctrl_reload = self._rules.get('reload', reload) - if callable(ctrl_reload): - ctrl_reload = ctrl_reload(self.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, rschema, role): - default = self._rules.get('novalue_label') - if default is None: - if self._rules.get('novalue_include_rtype'): - default = self._cw._('<%s not specified>') % display_name( - self._cw, rschema.type, role) - else: - default = self._cw._('') - else: - default = self._cw._(default) - return xml_escape(default) - - def _is_composite(self): - return self._rules.get('edit_target') == 'related' - - def _may_add_related(self, related_rset, rschema, role): - """ ok for attribute-like composite entities """ - ttypes = self._compute_ttypes(rschema, role) - if len(ttypes) > 1: # many etypes: learn how to do it - return False - rdef = rschema.role_rdef(self.entity.e_schema, ttypes[0], role) - card = rdef.role_cardinality(role) - if related_rset or card not in '?1': - return False - if role == 'subject': - kwargs = {'fromeid': self.entity.eid} - else: - kwargs = {'toeid': self.entity.eid} - return rdef.has_perm(self._cw, 'add', **kwargs) - - def _may_edit_related_entity(self, related_rset, rschema, role): - """ controls the edition of the related entity """ - ttypes = self._compute_ttypes(rschema, role) - if len(ttypes) > 1 or len(related_rset.rows) != 1: - return False - if self.entity.e_schema.rdef(rschema, role).role_cardinality(role) not in '?1': - return False - return related_rset.get_entity(0, 0).cw_has_perm('update') - - def _may_delete_related(self, related_rset, rschema, role): - # we assume may_edit_related, only 1 related entity - if not related_rset: - return False - rentity = related_rset.get_entity(0, 0) - entity = self.entity - if role == 'subject': - kwargs = {'fromeid': entity.eid, 'toeid': rentity.eid} - cardinality = rschema.rdefs[(entity.cw_etype, rentity.cw_etype)].cardinality[0] - else: - kwargs = {'fromeid': rentity.eid, 'toeid': entity.eid} - cardinality = rschema.rdefs[(rentity.cw_etype, entity.cw_etype)].cardinality[1] - if cardinality in '1+': - return False - # NOTE: should be sufficient given a well built schema/security - return rschema.has_perm(self._cw, 'delete', **kwargs) - - def _build_zone(self, zonedef, msg, logo): - return zonedef % {'msg': xml_escape(self._cw._(msg)), - 'logo': xml_escape(self._cw.data_url(logo))} - - def _build_edit_zone(self): - return self._build_zone(self._editzone, self._editzonemsg, self._editlogo) - - def _build_delete_zone(self): - return self._build_zone(self._deletezone, self._deletemsg, self._deletelogo) - - def _build_add_zone(self): - return self._build_zone(self._addzone, self._addmsg, self._addlogo) - - 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, reload, action, - 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), 'action': action, - 'role' : role, 'vid' : u''} - if extradata: - event_args.update(extradata) - return event_args - - def _prepare_form(self, entity, rschema, role, action): - assert action in ('edit_rtype', 'edit_related', 'add', 'delete'), action - if action == 'edit_rtype': - return False, entity - label = True - if action in ('edit_related', 'delete'): - edit_entity = entity.related(rschema, role).get_entity(0, 0) - elif action == 'add': - add_etype = self._compute_ttypes(rschema, role)[0] - _new_entity = self._cw.vreg['etypes'].etype_class(add_etype)(self._cw) - _new_entity.eid = next(self._cw.varmaker) - edit_entity = _new_entity - # XXX see forms.py ~ 276 and entities.linked_to method - # is there another way? - self._cw.form['__linkto'] = '%s:%s:%s' % (rschema, entity.eid, neg_role(role)) - assert edit_entity - return label, edit_entity - - def _build_renderer(self, related_entity, display_label): - return self._cw.vreg['formrenderers'].select( - self._form_renderer_id, self._cw, entity=related_entity, - display_label=display_label, - table_class='attributeForm' if display_label else '', - display_help=False, button_bar_class='buttonbar', - display_progress_div=False) - - def _build_form(self, entity, rschema, role, divid, formid, reload, action, - extradata=None, **formargs): - rtype = rschema.type - event_args = self._build_args(entity, rtype, role, formid, reload, action, extradata) - if not action: - form = _DummyForm() - form.event_args = event_args - return form, None - label, edit_entity = self._prepare_form(entity, rschema, role, action) - cancelclick = self._cancelclick % divid - form = self._cw.vreg['forms'].select( - formid, self._cw, rset=edit_entity.as_rset(), entity=edit_entity, - domid='%s-form' % divid, formtype='inlined', - action=self._cw.build_url('validateform', __onsuccess='window.parent.cw.reledit.onSuccess'), - cwtarget='eformframe', cssclass='releditForm', - **formargs) - # pass reledit arguments - for pname, pvalue in event_args.items(): - 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 - if formid == 'base': - field = form.field_by_name(rtype, role, entity.e_schema) - form.append_field(field) - return form, self._build_renderer(edit_entity, label) - - def _should_edit_attribute(self, rschema): - entity = self.entity - rdef = entity.e_schema.rdef(rschema) - # check permissions - if not entity.cw_has_perm('update'): - return False - rdef = entity.e_schema.rdef(rschema) - return rdef.has_perm(self._cw, 'update', eid=entity.eid) - - def _should_edit_relation(self, rschema, role): - eeid = self.entity.eid - perm_args = {'fromeid': eeid} if role == 'subject' else {'toeid': eeid} - return rschema.has_perm(self._cw, 'add', **perm_args) - - def _open_form_wrapper(self, divid, value, form, renderer, - _edit_related, _add_related, _delete_related): - w = self.w - w(u'
' % - {'id': divid, 'css': 'releditField', - 'out': "jQuery('#%s').addClass('invisible')" % divid, - 'over': "jQuery('#%s').removeClass('invisible')" % divid}) - w(u'
' % divid) - w(value) - w(u'
') - form.render(w=w, renderer=renderer) - w(u'') - self.w(u'
') - - def view_form(self, divid, value, form=None, renderer=None, - edit_related=False, add_related=False, delete_related=False): - self._open_form_wrapper(divid, value, form, renderer, - edit_related, add_related, delete_related) - args = form.event_args.copy() - self._edit_action(divid, args, edit_related, add_related, delete_related) - self._add_action(divid, args, edit_related, add_related, delete_related) - self._del_action(divid, args, edit_related, add_related, delete_related) - self._close_form_wrapper() - - -ClickAndEditFormView = class_renamed('ClickAndEditFormView', AutoClickAndEditFormView) - - -@ajaxfunc(output_type='xhtml') -def reledit_form(self): - req = self._cw - args = dict((x, req.form[x]) - for x in ('formid', 'rtype', 'role', 'reload', 'action')) - rset = req.eid_rset(int(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('reledit', req, rset=rset, rtype=args['rtype']) - return self._call_view(view, **args)