[forms] the last touch: handle inlined relation forms as fields
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 26 Jan 2010 20:25:56 +0100
changeset 4378 785c56bdacc6
parent 4377 0e9cf6593382
child 4379 72a5a8e075e9
[forms] the last touch: handle inlined relation forms as fields by introducing a simple InlinedRelationField. This makese things more flexible while removing a lost of overriding necessary.
web/data/cubicweb.edition.js
web/views/autoform.py
web/views/editforms.py
web/views/formrenderers.py
--- a/web/data/cubicweb.edition.js	Tue Jan 26 20:22:13 2010 +0100
+++ b/web/data/cubicweb.edition.js	Tue Jan 26 20:25:56 2010 +0100
@@ -230,8 +230,8 @@
 }
 
 
-function updateInlinedEntitiesCounters(rtype) {
-    jQuery('#inline' + rtype + 'slot span.icounter').each(function (i) {
+function updateInlinedEntitiesCounters(rtype, role) {
+    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function (i) {
 	this.innerHTML = i+1;
     });
 }
@@ -252,7 +252,7 @@
         var form = jQuery(dom);
         form.css('display', 'none');
         form.insertBefore(insertBefore).slideDown('fast');
-        updateInlinedEntitiesCounters(rtype);
+        updateInlinedEntitiesCounters(rtype, role);
         reorderTabindex();
         jQuery(CubicWeb).trigger('inlinedform-added', form);
         // if the inlined form contains a file input, we must force
@@ -273,10 +273,10 @@
 /*
  * removes the part of the form used to edit an inlined entity
  */
-function removeInlineForm(peid, rtype, eid, showaddnewlink) {
+function removeInlineForm(peid, rtype, role, eid, showaddnewlink) {
     jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
 	$(this).remove();
-	updateInlinedEntitiesCounters(rtype);
+	updateInlinedEntitiesCounters(rtype, role);
     });
     if (showaddnewlink) {
 	toggleVisibility(showaddnewlink);
--- a/web/views/autoform.py	Tue Jan 26 20:22:13 2010 +0100
+++ b/web/views/autoform.py	Tue Jan 26 20:25:56 2010 +0100
@@ -9,14 +9,16 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.common.decorators import cached, iclassmethod
+from logilab.common.decorators import iclassmethod
 
 from cubicweb import typed_eid
-from cubicweb.web import stdmsgs, uicfg
-from cubicweb.web import form, formwidgets as fwdgs
+from cubicweb.web import stdmsgs, uicfg, form, \
+     formwidgets as fw, formfields as ff
 from cubicweb.web.views import forms, editforms, editviews
 
-_afs = uicfg.autoform_section
+_AFS = uicfg.autoform_section
+_AFFK = uicfg.autoform_field_kwargs
+
 
 class AutomaticEntityForm(forms.EntityFieldsForm):
     """base automatic form to edit any entity.
@@ -35,9 +37,9 @@
     cwtarget = 'eformframe'
     cssclass = 'entityForm'
     copy_nav_params = True
-    form_buttons = [fwdgs.SubmitButton(),
-                    fwdgs.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'),
-                    fwdgs.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
+    form_buttons = [fw.SubmitButton(),
+                    fw.Button(stdmsgs.BUTTON_APPLY, cwaction='apply'),
+                    fw.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
     # for attributes selection when searching in uicfg.autoform_section
     formtype = 'main'
     # set this to a list of [(relation, role)] if you want to explictily tell
@@ -89,14 +91,31 @@
                 except form.FieldNotFound:
                     # meta attribute such as <attr>_format
                     continue
-        if self.formtype == 'main' and entity.has_eid() and (
-            self.display_fields is None or
-            '_cw_generic_field' in self.display_fields):
-            try:
-                self.fields.append(self.field_by_name('_cw_generic_field'))
-            except form.FieldNotFound:
-                # no editable relation
-                pass
+        if self.formtype == 'main':
+            if self.fieldsets_in_order:
+                fsio = list(self.fieldsets_in_order)
+            else:
+                fsio = [None]
+            self.fieldsets_in_order = fsio
+            # add fields for relation whose target should have an inline form
+            for formview in self.inlined_form_views():
+                field = self._inlined_form_view_field(formview)
+                self.fields.append(field)
+                if not field.fieldset in fsio:
+                    fsio.append(field.fieldset)
+            # add the generic relation field if necessary
+            if entity.has_eid() and (
+                self.display_fields is None or
+                '_cw_generic_field' in self.display_fields):
+                try:
+                    field = self.field_by_name('_cw_generic_field')
+                except form.FieldNotFound:
+                    # no editable relation
+                    pass
+                else:
+                    self.fields.append(field)
+                    if not field.fieldset in fsio:
+                        fsio.append(field.fieldset)
         self.maxrelitems = self._cw.property_value('navigation.related-limit')
         self.force_display = bool(self._cw.form.get('__force_display'))
         fnum = len(self.fields)
@@ -108,42 +127,6 @@
             return None
         return self.maxrelitems + 1
 
-    @property
-    def needs_multipart(self):
-        """true if the form needs enctype=multipart/form-data"""
-        return self._subform_needs_multipart()
-
-    def build_context(self, rendervalues=None):
-        super(AutomaticEntityForm, self).build_context(rendervalues)
-        for form in self.inlined_forms():
-            form.build_context(rendervalues)
-
-    def _subform_needs_multipart(self, _tested=None):
-        if _tested is None:
-            _tested = set()
-        if super(AutomaticEntityForm, self).needs_multipart:
-            return True
-        # take a look at inlined forms to check (recursively) if they
-        # need multipart handling.
-        # XXX: this is very suboptimal because inlined forms will be
-        #      selected / instantiated twice : here and during form rendering.
-        #      Potential solutions:
-        #       -> use subforms for inlined forms to get easiser access
-        #       -> use a simple onload js function to check if there is
-        #          a input type=file in the form
-        #       -> generate the <form> node when the content is rendered
-        #          and we know the correct enctype (formrenderer's w attribute
-        #          is not a StringIO)
-        for formview in self.inlined_form_views():
-            if formview.form:
-                if hasattr(formview.form, '_subform_needs_multipart'):
-                    needs_multipart = formview.form._subform_needs_multipart(_tested)
-                else:
-                    needs_multipart = formview.form.needs_multipart
-                if needs_multipart:
-                    return True
-        return False
-
     def action(self):
         """return the form's action attribute. Default to validateform if not
         explicitly overriden.
@@ -159,13 +142,38 @@
 
     action = property(action, set_action)
 
+    # autoform specific fields #################################################
+
+    def _generic_relations_field(self):
+        try:
+            srels_by_cat = self.srelations_by_category('generic', 'add', strict=True)
+            warn('[3.6] %s: srelations_by_category is deprecated, use uicfg or '
+                 'override editable_relations instead' % classid(form),
+                 DeprecationWarning)
+        except AttributeError:
+            srels_by_cat = self.editable_relations()
+        if not srels_by_cat:
+            raise form.FieldNotFound('_cw_generic_field')
+        fieldset = u'%s :' % self._cw.__('This %s' % self.edited_entity.e_schema)
+        fieldset = fieldset.capitalize()
+        return editviews.GenericRelationsField(self.editable_relations(),
+                                               fieldset=fieldset, label=None)
+
+    def _inlined_form_view_field(self, view):
+        # XXX allow more customization
+        kwargs = _AFFK.etype_get(self.edited_entity.e_schema, view.rtype,
+                                 view.role, view.etype)
+        if kwargs is None:
+            kwargs = {}
+        return editforms.InlinedFormField(view=view, **kwargs)
+
     # methods mapping edited entity relations to fields in the form ############
 
     def _relations_by_section(self, section, permission='add', strict=False):
         """return a list of (relation schema, target schemas, role) matching
         given category(ies) and permission
         """
-        return _afs.relations_by_section(
+        return _AFS.relations_by_section(
             self.edited_entity, self.formtype, section, permission, strict)
 
     def editable_attributes(self, strict=False):
@@ -194,13 +202,11 @@
         """
         return self._relations_by_section('inlined')
 
-    # generic relations modifier ###############################################
-
-    # inlined forms support ####################################################
+    # inlined forms control ####################################################
 
-    @cached
     def inlined_form_views(self):
-        """compute and return list of inlined form views (hosting the inlined form object)
+        """compute and return list of inlined form views (hosting the inlined
+        form object)
         """
         allformviews = []
         entity = self.edited_entity
@@ -231,11 +237,6 @@
                 allformviews += formviews
         return allformviews
 
-    def inlined_forms(self):
-        for formview in self.inlined_form_views():
-            if formview.form: # may be None for the addnew_link artefact form
-                yield formview.form
-
     def should_inline_relation_form(self, rschema, targettype, role):
         """return true if the given relation with entity has role and a
         targettype target should be inlined
@@ -280,8 +281,9 @@
             # display inline-edition view for all existing related entities
             for i, relentity in enumerate(related.entities()):
                 if relentity.has_perm('update'):
-                    yield vvreg.select('inline-edition', self._cw, rset=related,
-                                       row=i, col=0, rtype=rschema, role=role,
+                    yield vvreg.select('inline-edition', self._cw,
+                                       rset=related, row=i, col=0,
+                                       etype=ttype, rtype=rschema, role=role,
                                        peid=entity.eid, pform=self)
 
     def inline_creation_form_view(self, rschema, ttype, role):
@@ -295,46 +297,45 @@
 
 ## default form ui configuration ##############################################
 
-_afs = uicfg.autoform_section
 # use primary and not generated for eid since it has to be an hidden
-_afs.tag_attribute(('*', 'eid'), 'main', 'attributes')
-_afs.tag_attribute(('*', 'eid'), 'muledit', 'attributes')
-_afs.tag_attribute(('*', 'description'), 'main', 'attributes')
-_afs.tag_attribute(('*', 'creation_date'), 'main', 'metadata')
-_afs.tag_attribute(('*', 'modification_date'), 'main', 'metadata')
-_afs.tag_attribute(('*', 'cwuri'), 'main', 'metadata')
-_afs.tag_attribute(('*', 'has_text'), 'main', 'hidden')
-_afs.tag_subject_of(('*', 'in_state', '*'), 'main', 'hidden')
-_afs.tag_subject_of(('*', 'owned_by', '*'), 'main', 'metadata')
-_afs.tag_subject_of(('*', 'created_by', '*'), 'main', 'metadata')
-_afs.tag_subject_of(('*', 'require_permission', '*'), 'main', 'hidden')
-_afs.tag_subject_of(('*', 'by_transition', '*'), 'main', 'attributes')
-_afs.tag_subject_of(('*', 'by_transition', '*'), 'muledit', 'attributes')
-_afs.tag_object_of(('*', 'by_transition', '*'), 'main', 'hidden')
-_afs.tag_object_of(('*', 'from_state', '*'), 'main', 'hidden')
-_afs.tag_object_of(('*', 'to_state', '*'), 'main', 'hidden')
-_afs.tag_subject_of(('*', 'wf_info_for', '*'), 'main', 'attributes')
-_afs.tag_subject_of(('*', 'wf_info_for', '*'), 'muledit', 'attributes')
-_afs.tag_object_of(('*', 'wf_info_for', '*'), 'main', 'hidden')
-_afs.tag_subject_of(('CWPermission', 'require_group', '*'), 'main', 'attributes')
-_afs.tag_subject_of(('CWPermission', 'require_group', '*'), 'muledit', 'attributes')
-_afs.tag_attribute(('CWEType', 'final'), 'main', 'hidden')
-_afs.tag_attribute(('CWRType', 'final'), 'main', 'hidden')
-_afs.tag_attribute(('CWUser', 'firstname'), 'main', 'attributes')
-_afs.tag_attribute(('CWUser', 'surname'), 'main', 'attributes')
-_afs.tag_attribute(('CWUser', 'last_login_time'), 'main', 'metadata')
-_afs.tag_subject_of(('CWUser', 'in_group', '*'), 'main', 'attributes')
-_afs.tag_subject_of(('CWUser', 'in_group', '*'), 'muledit', 'attributes')
-_afs.tag_subject_of(('*', 'primary_email', '*'), 'main', 'relations')
-_afs.tag_subject_of(('*', 'use_email', '*'), 'main', 'inlined')
-_afs.tag_subject_of(('CWRelation', 'relation_type', '*'), 'main', 'inlined')
-_afs.tag_subject_of(('CWRelation', 'from_entity', '*'), 'main', 'inlined')
-_afs.tag_subject_of(('CWRelation', 'to_entity', '*'), 'main', 'inlined')
+_AFS.tag_attribute(('*', 'eid'), 'main', 'attributes')
+_AFS.tag_attribute(('*', 'eid'), 'muledit', 'attributes')
+_AFS.tag_attribute(('*', 'description'), 'main', 'attributes')
+_AFS.tag_attribute(('*', 'creation_date'), 'main', 'metadata')
+_AFS.tag_attribute(('*', 'modification_date'), 'main', 'metadata')
+_AFS.tag_attribute(('*', 'cwuri'), 'main', 'metadata')
+_AFS.tag_attribute(('*', 'has_text'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'in_state', '*'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'owned_by', '*'), 'main', 'metadata')
+_AFS.tag_subject_of(('*', 'created_by', '*'), 'main', 'metadata')
+_AFS.tag_subject_of(('*', 'require_permission', '*'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'by_transition', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('*', 'by_transition', '*'), 'muledit', 'attributes')
+_AFS.tag_object_of(('*', 'by_transition', '*'), 'main', 'hidden')
+_AFS.tag_object_of(('*', 'from_state', '*'), 'main', 'hidden')
+_AFS.tag_object_of(('*', 'to_state', '*'), 'main', 'hidden')
+_AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('*', 'wf_info_for', '*'), 'muledit', 'attributes')
+_AFS.tag_object_of(('*', 'wf_info_for', '*'), 'main', 'hidden')
+_AFS.tag_subject_of(('CWPermission', 'require_group', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('CWPermission', 'require_group', '*'), 'muledit', 'attributes')
+_AFS.tag_attribute(('CWEType', 'final'), 'main', 'hidden')
+_AFS.tag_attribute(('CWRType', 'final'), 'main', 'hidden')
+_AFS.tag_attribute(('CWUser', 'firstname'), 'main', 'attributes')
+_AFS.tag_attribute(('CWUser', 'surname'), 'main', 'attributes')
+_AFS.tag_attribute(('CWUser', 'last_login_time'), 'main', 'metadata')
+_AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'main', 'attributes')
+_AFS.tag_subject_of(('CWUser', 'in_group', '*'), 'muledit', 'attributes')
+_AFS.tag_subject_of(('*', 'primary_email', '*'), 'main', 'relations')
+_AFS.tag_subject_of(('*', 'use_email', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'relation_type', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'from_entity', '*'), 'main', 'inlined')
+_AFS.tag_subject_of(('CWRelation', 'to_entity', '*'), 'main', 'inlined')
 
-uicfg.autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'),
-                                          {'widget': fwdgs.TextInput})
-uicfg.autoform_field_kwargs.tag_subject_of(('TrInfo', 'wf_info_for', '*'),
-                                           {'widget': fwdgs.HiddenInput})
+_AFFK.tag_attribute(('RQLExpression', 'expression'),
+                    {'widget': fw.TextInput})
+_AFFK.tag_subject_of(('TrInfo', 'wf_info_for', '*'),
+                     {'widget': fw.HiddenInput})
 
 def registration_callback(vreg):
     global etype_relation_field
--- a/web/views/editforms.py	Tue Jan 26 20:22:13 2010 +0100
+++ b/web/views/editforms.py	Tue Jan 26 20:25:56 2010 +0100
@@ -21,10 +21,9 @@
                                 specified_etype_implements, yes)
 from cubicweb.view import EntityView
 from cubicweb import tags
-from cubicweb.web import uicfg, stdmsgs, eid_param
+from cubicweb.web import uicfg, stdmsgs, eid_param, \
+     formfields as ff, formwidgets as fw
 from cubicweb.web.form import FormViewMixIn, FieldNotFound
-from cubicweb.web.formfields import guess_field
-from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton
 from cubicweb.web.views import forms
 
 _pvdc = uicfg.primaryview_display_ctrl
@@ -36,8 +35,8 @@
 
     domid = 'deleteconf'
     copy_nav_params = True
-    form_buttons = [Button(stdmsgs.BUTTON_DELETE, cwaction='delete'),
-                    Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
+    form_buttons = [fw.Button(stdmsgs.BUTTON_DELETE, cwaction='delete'),
+                    fw.Button(stdmsgs.BUTTON_CANCEL, cwaction='cancel')]
     @property
     def action(self):
         return self._cw.build_url('edit')
@@ -230,8 +229,8 @@
         form = self._cw.vreg['forms'].select(
             formid, self._cw, entity=entity, domid='%s-form' % divid,
             cssstyle='display: none', onsubmit=onsubmit, action='#',
-            form_buttons=[SubmitButton(), Button(stdmsgs.BUTTON_CANCEL,
-                                                 onclick=cancelclick)],
+            form_buttons=[fw.SubmitButton(),
+                          fw.Button(stdmsgs.BUTTON_CANCEL, onclick=cancelclick)],
             **formargs)
         form.event_args = event_args
         return form
@@ -410,8 +409,8 @@
     __regid__ = 'muledit'
     domid = 'entityForm'
     onsubmit = "return validateForm('%s', null);" % domid
-    form_buttons = [SubmitButton(_('validate modifications on selected items')),
-                    ResetButton(_('revert changes'))]
+    form_buttons = [fw.SubmitButton(_('validate modifications on selected items')),
+                    fw.ResetButton(_('revert changes'))]
 
     def __init__(self, req, rset, **kwargs):
         kwargs.setdefault('__redirectrql', rset.printable_rql())
@@ -450,6 +449,53 @@
         self.w(form.render(formvid='edition'))
 
 
+# inlined form handling ########################################################
+
+class InlinedFormField(ff.Field):
+    def __init__(self, view=None, **kwargs):
+        if view.role == 'object':
+            fieldset = u'%s_object%s' % view.rtype
+        else:
+            fieldset = view.rtype
+        #kwargs.setdefault('fieldset', fieldset)
+        kwargs.setdefault('label', None)
+        super(InlinedFormField, self).__init__(name=view.rtype, role=view.role,
+                                               eidparam=True, **kwargs)
+        self.view = view
+
+    def render(self, form, renderer):
+        """render this field, which is part of form, using the given form
+        renderer
+        """
+        view = self.view
+        i18nctx = 'inlined:%s.%s.%s' % (form.edited_entity.e_schema,
+                                        view.rtype, view.role)
+        return u'<div class="inline-%s-%s-slot">%s</div>' % (
+            view.rtype, view.role,
+            view.render(i18nctx=i18nctx, row=view.cw_row, col=view.cw_col))
+
+    def form_init(self, form):
+        """method called before by build_context to trigger potential field
+        initialization requiring the form instance
+        """
+        if self.view.form:
+            self.view.form.build_context(form.formvalues)
+
+    @property
+    def needs_multipart(self):
+        if self.view.form:
+            # take a look at inlined forms to check (recursively) if they need
+            # multipart handling.
+            return self.view.form.needs_multipart
+        return False
+
+    def has_been_modified(self, form):
+        return False
+
+    def process_posted(self, form):
+        pass # handled by the subform
+
+
 class InlineEntityEditionFormView(FormViewMixIn, EntityView):
     """
     :attr peid: the parent entity's eid hosting the inline form
@@ -460,7 +506,7 @@
     __regid__ = 'inline-edition'
     __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
 
-    _select_attrs = ('peid', 'rtype', 'role', 'pform')
+    _select_attrs = ('peid', 'rtype', 'role', 'pform', 'etype')
     removejs = "removeInlinedEntity('%s', '%s', '%s')"
 
     def __init__(self, *args, **kwargs):
@@ -548,7 +594,6 @@
     __regid__ = 'inline-creation'
     __select__ = (match_kwargs('peid', 'rtype')
                   & specified_etype_implements('Any'))
-    _select_attrs = InlineEntityEditionFormView._select_attrs + ('etype',)
 
     @property
     def removejs(self):
@@ -559,9 +604,11 @@
         # we have to make this link appears back. This is done by giving add new link
         # id to removeInlineForm.
         if card not in '?1':
-            return "removeInlineForm('%s', '%s', '%s')"
-        divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
-        return "removeInlineForm('%%s', '%%s', '%%s', '%s')" % divid
+            return "removeInlineForm('%%s', '%%s', '%s', '%%s')" % self.role
+        divid = "addNew%s%s%s:%s" % (
+            self.etype, self.rtype, self.role, self.peid)
+        return "removeInlineForm('%%s', '%%s', '%s', '%%s', '%s')" % (
+            self.role, divid)
 
     @cached
     def _entity(self):
--- a/web/views/formrenderers.py	Tue Jan 26 20:22:13 2010 +0100
+++ b/web/views/formrenderers.py	Tue Jan 26 20:25:56 2010 +0100
@@ -373,10 +373,6 @@
         attrs_fs_label += '<div class="formBody">'
         return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
 
-    def render_fields(self, w, form, values):
-        super(EntityFormRenderer, self).render_fields(w, form, values)
-        self.inline_entities_form(w, form)
-
     def _render_fields(self, fields, w, form):
         if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'):
             super(EntityFormRenderer, self)._render_fields(fields, w, form)
@@ -395,29 +391,6 @@
  </table>""" % tuple(button.render(form) for button in form.form_buttons))
         else:
             super(EntityFormRenderer, self).render_buttons(w, form)
-    # NOTE: should_* and display_* method extracted and moved to the form to
-    # ease overriding
-
-    def inline_entities_form(self, w, form):
-        """create a form to edit entity's inlined relations"""
-        if not hasattr(form, 'inlined_form_views'):
-            return
-        keysinorder = []
-        formviews = form.inlined_form_views()
-        for formview in formviews:
-            if not (formview.rtype, formview.role) in keysinorder:
-                keysinorder.append( (formview.rtype, formview.role) )
-        for key in keysinorder:
-            self.inline_relation_form(w, form, [fv for fv in formviews
-                                                if (fv.rtype, fv.role) == key])
-
-    def inline_relation_form(self, w, form, formviews):
-        i18nctx = 'inlined:%s.%s.%s' % (form.edited_entity.e_schema,
-                                        formviews[0].rtype, formviews[0].role)
-        w(u'<div id="inline%sslot">' % formviews[0].rtype)
-        for formview in formviews:
-            w(formview.render(i18nctx=i18nctx, row=formview.cw_row, col=formview.cw_col))
-        w(u'</div>')
 
 
 class EntityInlinedFormRenderer(EntityFormRenderer):
@@ -460,6 +433,5 @@
         if fields:
             self._render_fields(fields, w, form)
         self.render_child_forms(w, form, values)
-        self.inline_entities_form(w, form)
         w(u'</fieldset>')