web/views/reledit.py
changeset 5869 8a129b3a5aff
child 5874 6afc88f439e8
equal deleted inserted replaced
5868:c4380d8cfc25 5869:8a129b3a5aff
       
     1 import copy
       
     2 
       
     3 from logilab.mtconverter import xml_escape
       
     4 
       
     5 from cubicweb import neg_role
       
     6 from cubicweb.schema import display_name
       
     7 from cubicweb.utils import json
       
     8 from cubicweb.selectors import non_final_entity, match_kwargs
       
     9 from cubicweb.view import EntityView
       
    10 from cubicweb.web import uicfg, stdmsgs
       
    11 from cubicweb.web.form import FormViewMixIn, FieldNotFound
       
    12 from cubicweb.web.formwidgets import Button, SubmitButton
       
    13 
       
    14 class DummyForm(object):
       
    15     __slots__ = ('event_args',)
       
    16     def form_render(self, **_args):
       
    17         return u''
       
    18     def render(self, *_args, **_kwargs):
       
    19         return u''
       
    20     def append_field(self, *args):
       
    21         pass
       
    22     def field_by_name(self, rtype, role, eschema=None):
       
    23         return None
       
    24 
       
    25 class ClickAndEditFormView(FormViewMixIn, EntityView):
       
    26     __regid__ = 'doreledit'
       
    27     __select__ = non_final_entity() & match_kwargs('rtype')
       
    28 
       
    29     # ui side continuations
       
    30     _onclick = (u"cw.reledit.loadInlineEditionForm('%(formid)s', %(eid)s, '%(rtype)s', '%(role)s', "
       
    31                 "'%(divid)s', %(reload)s, '%(vid)s', '%(default_value)s');")
       
    32     _cancelclick = "cw.reledit.cleanupAfterCancel('%s')"
       
    33 
       
    34     # ui side actions/buttons
       
    35     _addzone = (u'<img title="%(msg)s" src="data/plus.png" '
       
    36                 u'alt="%(msg)s"/>')
       
    37     _addmsg = _('click to add a value')
       
    38     _deletezone = (u'<img title="%(msg)s" src="data/cancel.png" alt="%(msg)s"/>')
       
    39     _deletemsg = _('click to delete this value')
       
    40     _editzone = (u'<img title="%(msg)s" src="data/pen_icon.png" alt="%(msg)s"/>')
       
    41     _editzonemsg = _('click to edit this field')
       
    42 
       
    43     # default relation vids according to cardinality
       
    44     _one_rvid = 'incontext'
       
    45     _many_rvid = 'csv'
       
    46 
       
    47     def cell_call(self, row, col, rtype=None, role='subject',
       
    48                   reload=False, # controls reloading the whole page after change
       
    49                                 # boolean, eid (to redirect), or
       
    50                                 # function taking the subject entity & returning a boolean or an eid
       
    51                   rvid=None,    # vid to be applied to other side of rtype (non final relations only)
       
    52                   default_value=None,
       
    53                   formid=None
       
    54                   ):
       
    55         """display field to edit entity's `rtype` relation on click"""
       
    56         assert rtype
       
    57         assert role in ('subject', 'object'), '%s is not an acceptable role value' % role
       
    58         if self.__regid__ == 'doreledit':
       
    59             assert formid
       
    60         self._cw.add_js('cubicweb.reledit.js')
       
    61         if formid:
       
    62             self._cw.add_js('cubicweb.edition.js')
       
    63         self._cw.add_css('cubicweb.form.css')
       
    64         entity = self.cw_rset.get_entity(row, col)
       
    65         rschema = self._cw.vreg.schema[rtype]
       
    66         reload = self._compute_reload(entity, rschema, role, reload)
       
    67         default_value = self._compute_default_value(entity, rschema, role, default_value)
       
    68         # compute value, checking perms, build & display form
       
    69         divid = self._build_divid(rtype, role, entity.eid)
       
    70         if rschema.final:
       
    71             value = entity.printable_value(rtype)
       
    72             form, renderer = self._build_form(entity, rtype, role, divid, 'base',
       
    73                                               default_value, reload)
       
    74             if not self._should_edit_attribute(entity, rschema, form):
       
    75                 self.w(value)
       
    76                 return
       
    77             value = value or default_value
       
    78             field = form.field_by_name(rtype, role, entity.e_schema)
       
    79             form.append_field(field)
       
    80             self.view_form(divid, value, form, renderer)
       
    81         else:
       
    82             rvid = self._compute_best_vid(entity.e_schema, rschema, role)
       
    83             related_rset = entity.related(rtype, role)
       
    84             if related_rset:
       
    85                 value = self._cw.view(rvid, related_rset)
       
    86             else:
       
    87                 value = default_value
       
    88             ttypes = self._compute_ttypes(rschema, role)
       
    89 
       
    90             if not self._should_edit_relation(entity, rschema, role):
       
    91                 self.w(value)
       
    92                 return
       
    93             # this is for attribute-like composites (1 target type, 1 related entity at most)
       
    94             edit_related = self._may_edit_related_entity(related_rset, entity, rschema, role, ttypes)
       
    95             delete_related = edit_related and self._may_delete_related(related_rset, entity, rschema, role)
       
    96             add_related = self._may_add_related(related_rset, entity, rschema, role, ttypes)
       
    97             # compute formid
       
    98             if len(ttypes) > 1: # redundant safety belt
       
    99                 formid = 'base'
       
   100             else:
       
   101                 afs = uicfg.autoform_section.etype_get(entity.e_schema, rschema, role, ttypes[0])
       
   102                 # is there an afs spec that says we should edit
       
   103                 # the rschema as an attribute ?
       
   104                 if afs and 'main_attributes' in afs:
       
   105                     formid = 'base'
       
   106 
       
   107             form, renderer = self._build_form(entity, rtype, role, divid, formid, default_value,
       
   108                                               reload, dict(vid=rvid),
       
   109                                               edit_related, add_related and ttypes[0])
       
   110             self.view_form(divid, value, form, renderer, edit_related,
       
   111                            delete_related, add_related)
       
   112 
       
   113     def _compute_best_vid(self, eschema, rschema, role):
       
   114         if eschema.rdef(rschema, role).role_cardinality(role) in '+*':
       
   115             return self._many_rvid
       
   116         return self._one_rvid
       
   117 
       
   118     def _compute_ttypes(self, rschema, role):
       
   119         dual_role = neg_role(role)
       
   120         return getattr(rschema, '%ss' % dual_role)()
       
   121 
       
   122     def _compute_reload(self, entity, rschema, role, reload):
       
   123         rule = uicfg.reledit_ctrl.etype_get(entity.e_schema.type, rschema.type, role, '*')
       
   124         ctrl_reload = rule.get('reload', reload)
       
   125         if callable(ctrl_reload):
       
   126             ctrl_reload = ctrl_reload(entity)
       
   127         if isinstance(ctrl_reload, int) and ctrl_reload > 1: # not True/False
       
   128             ctrl_reload = self._cw.build_url(ctrl_reload)
       
   129         return ctrl_reload
       
   130 
       
   131     def _compute_default_value(self, entity, rschema, role, default_value):
       
   132         etype = entity.e_schema.type
       
   133         rule = uicfg.reledit_ctrl.etype_get(etype, rschema.type, role, '*')
       
   134         ctrl_default = rule.get('default_value', default_value)
       
   135         if ctrl_default:
       
   136             return ctrl_default
       
   137         if default_value is None:
       
   138             return xml_escape(self._cw._('<%s not specified>') %
       
   139                               display_name(self._cw, rschema.type, role))
       
   140         return default_value
       
   141 
       
   142     def _is_composite(self, eschema, rschema, role):
       
   143         return eschema.rdef(rschema, role).composite == role
       
   144 
       
   145     def _may_add_related(self, rset, entity, rschema, role, ttypes):
       
   146         """ ok for attribute-like composite entities """
       
   147         if self._is_composite(entity.e_schema, rschema, role):
       
   148             if len(ttypes) > 1: # wrong cardinality: do not handle
       
   149                 return False
       
   150             ttype = ttypes[0]
       
   151             if rset:
       
   152                 eschema = entity.e_schema
       
   153                 if role == 'subject':
       
   154                     source, target = eschema.type, ttype
       
   155                 else:
       
   156                     source, target = ttype, eschema.type
       
   157                 card = rschema.rdef(source, target).role_cardinality(role)
       
   158                 if card in '1?':
       
   159                     return False
       
   160             if rschema.has_perm(self._cw, 'add', toetype=ttype):
       
   161                 return True
       
   162         return False
       
   163 
       
   164     def _may_edit_related_entity(self, related_rset, entity, rschema, role, ttypes):
       
   165         """ controls the edition of the related entity """
       
   166         if len(ttypes) > 1:
       
   167             return False
       
   168         if not self._is_composite(entity.e_schema, rschema, role):
       
   169             return False
       
   170         if len(related_rset.rows) != 1:
       
   171             return False
       
   172         return related_rset.get_entity(0, 0).cw_has_perm('update')
       
   173 
       
   174     def _may_delete_related(self, related_rset, entity, rschema, role):
       
   175         # we assume may_edit_related
       
   176         assert len(related_rset.rows) == 1
       
   177         kwargs = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
       
   178         if not rschema.has_perm(self._cw, 'delete', **kwargs):
       
   179             return False
       
   180         for related_entity in related_rset.entities():
       
   181             if not related_entity.cw_has_perm('delete'):
       
   182                 return False
       
   183         return True
       
   184 
       
   185     def _build_edit_zone(self):
       
   186         return self._editzone % {'msg' : xml_escape(_(self._cw._(self._editzonemsg)))}
       
   187 
       
   188     def _build_delete_zone(self):
       
   189         return self._deletezone % {'msg': xml_escape(self._cw._(self._deletemsg))}
       
   190 
       
   191     def _build_add_zone(self):
       
   192         return self._addzone % {'msg': xml_escape(self._cw._(self._addmsg))}
       
   193 
       
   194     def _build_divid(self, rtype, role, entity_eid):
       
   195         """ builds an id for the root div of a reledit widget """
       
   196         return '%s-%s-%s' % (rtype, role, entity_eid)
       
   197 
       
   198     def _build_args(self, entity, rtype, role, formid, default_value, reload,
       
   199                     extradata=None):
       
   200         divid = self._build_divid(rtype, role, entity.eid)
       
   201         event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'formid': formid,
       
   202                       'reload' : json.dumps(reload), 'default_value' : default_value,
       
   203                       'role' : role, 'vid' : u''}
       
   204         if extradata:
       
   205             event_args.update(extradata)
       
   206         return event_args
       
   207 
       
   208     def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
       
   209                     extradata=None, edit_related=False, add_related=False, **formargs):
       
   210         event_args = self._build_args(entity, rtype, role, formid, default_value,
       
   211                                       reload, extradata)
       
   212         cancelclick = self._cancelclick % divid
       
   213         if edit_related:
       
   214             display_fields = None
       
   215             display_label = True
       
   216             related_entity = entity.related(rtype, role).get_entity(0, 0)
       
   217             self._cw.form['eid'] = related_entity.eid
       
   218         elif add_related:
       
   219             display_fields = None
       
   220             display_label = True
       
   221             _new_entity = self._cw.vreg['etypes'].etype_class(add_related)(self._cw)
       
   222             _new_entity.eid = self._cw.varmaker.next()
       
   223             related_entity = _new_entity
       
   224             self._cw.form['__linkto'] = '%s:%s:%s' % (rtype, entity.eid, neg_role(role))
       
   225         else: # base case: edition/attribute relation
       
   226             display_fields = [(rtype, role)]
       
   227             display_label = False
       
   228             related_entity = entity
       
   229         form = self._cw.vreg['forms'].select(
       
   230             formid, self._cw, rset=related_entity.as_rset(), entity=related_entity, domid='%s-form' % divid,
       
   231             display_fields=display_fields, formtype='inlined',
       
   232             action=self._cw.build_url('validateform?__onsuccess=window.parent.cw.reledit.onSuccess'),
       
   233             cwtarget='eformframe', cssstyle='display: none',
       
   234             **formargs)
       
   235         # pass reledit arguments
       
   236         for pname, pvalue in event_args.iteritems():
       
   237             form.add_hidden('__reledit|' + pname, pvalue)
       
   238         # handle buttons
       
   239         if form.form_buttons: # edition, delete
       
   240             form_buttons = []
       
   241             for button in form.form_buttons:
       
   242                 if not button.label.endswith('apply'):
       
   243                     if button.label.endswith('cancel'):
       
   244                         button = copy.deepcopy(button)
       
   245                         button.cwaction = None
       
   246                         button.onclick = cancelclick
       
   247                     form_buttons.append(button)
       
   248             form.form_buttons = form_buttons
       
   249         else: # base
       
   250             form.form_buttons = [SubmitButton(),
       
   251                                  Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)]
       
   252         form.event_args = event_args
       
   253         renderer = self._cw.vreg['formrenderers'].select(
       
   254             'base', self._cw, entity=related_entity, display_label=display_label,
       
   255             display_help=False, table_class='',
       
   256             button_bar_class='buttonbar', display_progress_div=False)
       
   257         return form, renderer
       
   258 
       
   259     def _should_edit_attribute(self, entity, rschema, form):
       
   260         # examine rtags
       
   261         noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rschema.type, 'subject').get('noedit', False)
       
   262         if noedit:
       
   263             return False
       
   264         rdef = entity.e_schema.rdef(rschema)
       
   265         afs = uicfg.autoform_section.etype_get(entity.__regid__, rschema, 'subject', rdef.object)
       
   266         if 'main_hidden' in  afs:
       
   267             return False
       
   268         # check permissions
       
   269         if not entity.cw_has_perm('update'):
       
   270             return False
       
   271         rdef = entity.e_schema.rdef(rschema)
       
   272         if not rdef.has_perm(self._cw, 'update', eid=entity.eid):
       
   273             return False
       
   274         # XXX ?
       
   275         try:
       
   276             form.field_by_name(str(rschema), 'subject', entity.e_schema)
       
   277         except FieldNotFound:
       
   278             return False
       
   279         return True
       
   280 
       
   281     def _should_edit_relation(self, entity, rschema, role):
       
   282         # examine rtags
       
   283         rtype = rschema.type
       
   284         noedit = uicfg.reledit_ctrl.etype_get(entity.e_schema, rtype, role).get('noedit', False)
       
   285         if noedit:
       
   286             return False
       
   287         rdef = entity.e_schema.rdef(rschema, role)
       
   288         afs = uicfg.autoform_section.etype_get(
       
   289             entity.__regid__, rschema, role, rdef.object)
       
   290         if 'main_hidden' in afs:
       
   291             return False
       
   292         perm_args = {'fromeid': entity.eid} if role == 'subject' else {'toeid': entity.eid}
       
   293         return rschema.has_perm(self._cw, 'add', **perm_args)
       
   294 
       
   295     def view_form(self, divid, value, form=None, renderer=None,
       
   296                   edit_related=False, delete_related=False, add_related=False):
       
   297         w = self.w
       
   298         w(u'<div id="%(id)s-reledit" onmouseout="%(out)s" onmouseover="%(over)s">' %
       
   299           {'id': divid,
       
   300            'out': "jQuery('#%s').addClass('hidden')" % divid,
       
   301            'over': "jQuery('#%s').removeClass('hidden')" % divid})
       
   302         w(u'<div id="%s-value" class="editableFieldValue">' % divid)
       
   303         w(value)
       
   304         w(u'</div>')
       
   305         w(form.render(renderer=renderer))
       
   306         w(u'<div id="%s" class="editableField hidden">' % divid)
       
   307         args = form.event_args.copy()
       
   308         if not add_related: # excludes edition
       
   309             args['formid'] = 'edition'
       
   310             w(u'<div id="%s-update" class="editableField" onclick="%s" title="%s">' %
       
   311               (divid, xml_escape(self._onclick % args), self._cw._(self._editzonemsg)))
       
   312             w(self._build_edit_zone())
       
   313             w(u'</div>')
       
   314         else:
       
   315             args['formid'] = 'edition'
       
   316             w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
       
   317               (divid, xml_escape(self._onclick % args), self._cw._(self._addmsg)))
       
   318             w(self._build_add_zone())
       
   319             w(u'</div>')
       
   320         if delete_related:
       
   321             args['formid'] = 'deleteconf'
       
   322             w(u'<div id="%s-add" class="editableField" onclick="%s" title="%s">' %
       
   323               (divid, xml_escape(self._onclick % args), self._cw._(self._deletemsg)))
       
   324             w(self._build_delete_zone())
       
   325             w(u'</div>')
       
   326         w(u'</div>')
       
   327         w(u'</div>')
       
   328 
       
   329 class AutoClickAndEditFormView(ClickAndEditFormView):
       
   330     __regid__ = 'reledit'
       
   331 
       
   332     def _build_form(self, entity, rtype, role, divid, formid, default_value, reload,
       
   333                   extradata=None, edit_related=False, add_related=False, **formargs):
       
   334         event_args = self._build_args(entity, rtype, role, 'base', default_value,
       
   335                                       reload, extradata)
       
   336         form = DummyForm()
       
   337         form.event_args = event_args
       
   338         return form, None