backport stable branch
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 29 Sep 2009 15:58:44 +0200
changeset 3524 a3431f4e2f40
parent 3505 c0c7a944c00d (current diff)
parent 3523 16880e7ee3fa (diff)
child 3536 f6c9a5df80fb
backport stable branch
selectors.py
server/test/unittest_rql2sql.py
web/form.py
web/formfields.py
web/test/unittest_application.py
web/test/unittest_form.py
web/views/autoform.py
web/views/basecontrollers.py
web/views/boxes.py
web/views/cwproperties.py
web/views/editforms.py
web/views/formrenderers.py
web/views/forms.py
web/views/ibreadcrumbs.py
web/views/massmailing.py
web/views/workflow.py
--- a/selectors.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/selectors.py	Tue Sep 29 15:58:44 2009 +0200
@@ -150,8 +150,8 @@
 class ImplementsMixIn(object):
     """mix-in class for selectors checking implemented interfaces of something
     """
-    def __init__(self, *expected_ifaces):
-        super(ImplementsMixIn, self).__init__()
+    def __init__(self, *expected_ifaces, **kwargs):
+        super(ImplementsMixIn, self).__init__(**kwargs)
         self.expected_ifaces = expected_ifaces
 
     def __str__(self):
@@ -186,8 +186,9 @@
       - `once_is_enough` is False, in which case if score_class return 0, 0 is
         returned
     """
-    def __init__(self, once_is_enough=False):
+    def __init__(self, once_is_enough=False, accept_none=True):
         self.once_is_enough = once_is_enough
+        self.accept_none = accept_none
 
     @lltrace
     def __call__(self, cls, req, rset=None, row=None, col=0, **kwargs):
@@ -195,9 +196,12 @@
             return 0
         score = 0
         if row is None:
+            if not self.accept_none:
+                if any(rset[i][col] is None for i in xrange(len(rset))):
+                    return 0
             for etype in rset.column_types(col):
                 if etype is None: # outer join
-                    continue
+                    return 0
                 escore = self.score(cls, req, etype)
                 if not escore and not self.once_is_enough:
                     return 0
--- a/server/test/unittest_rql2sql.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/server/test/unittest_rql2sql.py	Tue Sep 29 15:58:44 2009 +0200
@@ -583,17 +583,13 @@
 FROM entities AS X
 WHERE X.type='Note' AND X.eid>12"""),
 
-    ('Any X, T WHERE X eid > 12, X title T',
+    ('Any X, T WHERE X eid > 12, X title T, X is IN (Bookmark, Card)',
      """SELECT X.cw_eid, X.cw_title
 FROM cw_Bookmark AS X
 WHERE X.cw_eid>12
 UNION ALL
 SELECT X.cw_eid, X.cw_title
 FROM cw_Card AS X
-WHERE X.cw_eid>12
-UNION ALL
-SELECT X.cw_eid, X.cw_title
-FROM cw_EmailThread AS X
 WHERE X.cw_eid>12"""),
 
     ('Any X',
@@ -754,12 +750,10 @@
 FROM cw_Note AS X
 WHERE NOT EXISTS(SELECT 1 FROM evaluee_relation AS rel_evaluee0,cw_CWUser AS Y WHERE rel_evaluee0.eid_from=Y.cw_eid AND rel_evaluee0.eid_to=X.cw_eid)'''),
 
-    ('Any X,T WHERE X title T, NOT X is Bookmark',
-     '''SELECT X.cw_eid, X.cw_title
-FROM cw_Card AS X
-UNION ALL
-SELECT X.cw_eid, X.cw_title
-FROM cw_EmailThread AS X'''),
+    ('Any X,RT WHERE X relation_type RT, NOT X is CWAttribute',
+     '''SELECT X.cw_eid, X.cw_relation_type
+FROM cw_CWRelation AS X
+WHERE X.cw_relation_type IS NOT NULL'''),
 
     ('Any K,V WHERE P is CWProperty, P pkey K, P value V, NOT P for_user U',
      '''SELECT P.cw_pkey, P.cw_value
@@ -774,7 +768,17 @@
 SELECT S.cw_eid
 FROM cw_State AS S
 WHERE NOT EXISTS(SELECT 1 FROM cw_CWUser AS X WHERE X.cw_in_state=S.cw_eid)'''),
-    ]
+
+    ('Any S WHERE NOT(X in_state S, S name "somename"), X is CWUser',
+     '''SELECT S.cw_eid
+FROM cw_State AS S
+WHERE NOT EXISTS(SELECT 1 FROM cw_CWUser AS X WHERE X.cw_in_state=S.cw_eid AND S.cw_name=somename)'''),
+   
+# XXXFIXME fail
+#         ('Any X,RT WHERE X relation_type RT?, NOT X is CWAttribute',
+#      '''SELECT X.cw_eid, X.cw_relation_type
+# FROM cw_CWRelation AS X'''),
+]
 
 OUTER_JOIN = [
     ('Any X,S WHERE X travaille S?',
--- a/web/form.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/form.py	Tue Sep 29 15:58:44 2009 +0200
@@ -7,6 +7,8 @@
 """
 __docformat__ = "restructuredtext en"
 
+from logilab.common.decorators import iclassmethod
+
 from cubicweb.appobject import AppObject
 from cubicweb.view import NOINDEX, NOFOLLOW
 from cubicweb.common import tags
@@ -62,10 +64,65 @@
     __metaclass__ = metafieldsform
     __registry__ = 'forms'
 
+    parent_form = None
+
     def __init__(self, req, rset, **kwargs):
         super(Form, self).__init__(req, rset=rset, **kwargs)
         self.restore_previous_post(self.session_key())
 
+    @property
+    def root_form(self):
+        """return the root form"""
+        if self.parent_form is None:
+            return self
+        return self.parent_form.root_form
+
+    @iclassmethod
+    def _fieldsattr(cls_or_self):
+        if isinstance(cls_or_self, type):
+            fields = cls_or_self._fields_
+        else:
+            fields = cls_or_self.fields
+        return fields
+
+    @iclassmethod
+    def field_by_name(cls_or_self, name, role='subject'):
+        """return field with the given name and role.
+        Raise FieldNotFound if the field can't be found.
+        """
+        for field in cls_or_self._fieldsattr():
+            if field.name == name and field.role == role:
+                return field
+        raise FieldNotFound(name)
+
+    @iclassmethod
+    def fields_by_name(cls_or_self, name, role='subject'):
+        """return a list of fields with the given name and role"""
+        return [field for field in cls_or_self._fieldsattr()
+                if field.name == name and field.role == role]
+
+    @iclassmethod
+    def remove_field(cls_or_self, field):
+        """remove a field from form class or instance"""
+        cls_or_self._fieldsattr().remove(field)
+
+    @iclassmethod
+    def append_field(cls_or_self, field):
+        """append a field to form class or instance"""
+        cls_or_self._fieldsattr().append(field)
+
+    @iclassmethod
+    def insert_field_before(cls_or_self, new_field, name, role='subject'):
+        field = cls_or_self.field_by_name(name, role)
+        fields = cls_or_self._fieldsattr()
+        fields.insert(fields.index(field), new_field)
+
+    @iclassmethod
+    def insert_field_after(cls_or_self, new_field, name, role='subject'):
+        field = cls_or_self.field_by_name(name, role)
+        fields = cls_or_self._fieldsattr()
+        fields.insert(fields.index(field)+1, new_field)
+
     def session_key(self):
         """return the key that may be used to store / retreive data about a
         previous post which failed because of a validation error
--- a/web/formfields.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/formfields.py	Tue Sep 29 15:58:44 2009 +0200
@@ -209,7 +209,7 @@
         return vocab
 
     def form_init(self, form):
-        """method called before by form_build_context to trigger potential field
+        """method called before by build_context to trigger potential field
         initialization requiring the form instance
         """
         pass
--- a/web/test/unittest_application.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/test/unittest_application.py	Tue Sep 29 15:58:44 2009 +0200
@@ -138,6 +138,11 @@
 
 
 class ApplicationTC(CubicWebTC):
+    def setUp(self):
+        super(ApplicationTC, self).setUp()
+        def raise_hdlr(*args, **kwargs):
+            raise
+        self.app.error_handler = raise_hdlr
 
     def publish(self, req, path='view'):
         return self.app.publish(path, req)
@@ -411,6 +416,12 @@
         self.assertRaises(AuthenticationError, self.publish, req, 'logout')
         self.assertEquals(len(self.open_sessions), 0)
 
+    def test_non_regr_optional_first_var(self):
+        req = self.request()
+        # expect a rset with None in [0][0]
+        req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
+        self.publish(req)
+        print 'yuea'
 
 if __name__ == '__main__':
     unittest_main()
--- a/web/test/unittest_form.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/test/unittest_form.py	Tue Sep 29 15:58:44 2009 +0200
@@ -82,7 +82,7 @@
         form = EntityFieldsForm(self.request(login=u'toto'), None, entity=e)
         field = StringField(name='login', eidparam=True)
         form.append_field(field)
-        form.form_build_context({})
+        form.build_context({})
         self.assertEquals(form.form_field_display_value(field, {}), 'toto')
 
 
@@ -137,7 +137,7 @@
     # fields tests ############################################################
 
     def _render_entity_field(self, name, form):
-        form.form_build_context({})
+        form.build_context({})
         renderer = FormRenderer(self.req)
         return form.field_by_name(name).render(form, renderer)
 
@@ -171,7 +171,7 @@
         class FFForm(EntityFieldsForm):
             data = FileField(format_field=StringField(name='data_format', max_length=50),
                              encoding_field=StringField(name='data_encoding', max_length=20))
-        file = self.add_entity('File', name=u"pouet.txt", data_encoding=u'UTF-8',
+        file = self.add_entity('File', data_name=u"pouet.txt", data_encoding=u'UTF-8',
                                data=Binary('new widgets system'))
         form = FFForm(self.req, redirect_path='perdu.com', entity=file)
         self.assertTextEquals(self._render_entity_field('data', form),
@@ -195,7 +195,7 @@
                 return 'ascii'
             def form_field_format(self, field):
                 return 'text/plain'
-        file = self.add_entity('File', name=u"pouet.txt", data_encoding=u'UTF-8',
+        file = self.add_entity('File', data_name=u"pouet.txt", data_encoding=u'UTF-8',
                                data=Binary('new widgets system'))
         form = EFFForm(self.req, redirect_path='perdu.com', entity=file)
         self.assertTextEquals(self._render_entity_field('data', form),
--- a/web/views/autoform.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/autoform.py	Tue Sep 29 15:58:44 2009 +0200
@@ -8,7 +8,7 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from logilab.common.decorators import iclassmethod
+from logilab.common.decorators import iclassmethod, cached
 
 from cubicweb import typed_eid
 from cubicweb.web import stdmsgs, uicfg
@@ -113,6 +113,11 @@
         """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()
@@ -260,6 +265,46 @@
 
     # inlined forms support ####################################################
 
+    @cached
+    def inlined_form_views(self):
+        """compute and return list of inlined form views (hosting the inlined form object)
+        """
+        formviews = []
+        entity = self.edited_entity
+        for rschema, ttypes, role in self.inlined_relations():
+            # show inline forms only if there's one possible target type
+            # for rschema
+            if len(ttypes) != 1:
+                self.warning('entity related by the %s relation should have '
+                             'inlined form but there is multiple target types, '
+                             'dunno what to do', rschema)
+                continue
+            ttype = ttypes[0].type
+            if self.should_inline_relation_form(rschema, ttype, role):
+                formviews += self.inline_edition_form_view(rschema, ttype, role)
+                if role == 'subject':
+                    card = rschema.rproperty(entity.e_schema, ttype, 'cardinality')[0]
+                else:
+                    card = rschema.rproperty(ttype, entity.e_schema, 'cardinality')[1]
+                # there is no related entity and we need at least one: we need to
+                # display one explicit inline-creation view
+                if self.should_display_inline_creation_form(rschema, formviews, card):
+                    formviews += self.inline_creation_form_view(rschema, ttype, role)
+                # we can create more than one related entity, we thus display a link
+                # to add new related entities
+                if self.should_display_add_new_relation_link(rschema, formviews, card):
+                    addnewlink = self.vreg['views'].select(
+                        'inline-addnew-link', self.req,
+                        etype=ttype, rtype=rschema, role=role,
+                        peid=self.edited_entity.eid, pform=self, card=card)
+                    formviews.append(addnewlink)
+        return formviews
+
+    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
@@ -269,25 +314,6 @@
         """
         return True
 
-    def display_inline_edition_form(self, w, rschema, targettype, role,
-                                     i18nctx):
-        """display inline forms for already related entities.
-
-        Return True if some inlined form are actually displayed
-        """
-        existant = False
-        entity = self.edited_entity
-        related = entity.has_eid() and entity.related(rschema, role)
-        if related:
-            # display inline-edition view for all existing related entities
-            for i, relentity in enumerate(related.entities()):
-                if relentity.has_perm('update'):
-                    w(self._cw.view('inline-edition', related, row=i, col=0,
-                                    rtype=rschema, role=role, ptype=entity.e_schema,
-                                    peid=entity.eid, i18nctx=i18nctx))
-                    existant = True
-        return existant
-
     def should_display_inline_creation_form(self, rschema, existant, card):
         """return true if a creation form should be inlined
 
@@ -295,17 +321,6 @@
         """
         return not existant and card in '1+' or self._cw.form.has_key('force_%s_display' % rschema)
 
-    def display_inline_creation_form(self, w, rschema, targettype, role,
-                                     i18nctx):
-        """display inline forms to a newly related (hence created) entity.
-
-        Return True if some inlined form are actually displayed
-        """
-        entity = self.edited_entity
-        w(self._cw.view('inline-creation', None, etype=targettype,
-                        peid=entity.eid, ptype=entity.e_schema,
-                        rtype=rschema, role=role, i18nctx=i18nctx))
-
     def should_display_add_new_relation_link(self, rschema, existant, card):
         """return true if we should add a link to add a new creation form
         (through ajax call)
@@ -323,6 +338,29 @@
         """
         return card in '1?'
 
+    def inline_edition_form_view(self, rschema, ttype, role):
+        """yield inline form views for already related entities through the
+        given relation
+        """
+        entity = self.edited_entity
+        related = entity.has_eid() and entity.related(rschema, role)
+        if related:
+            vvreg = self._cw.vreg['views']
+            # 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, related,
+                                       row=i, col=0, rtype=rschema, role=role,
+                                       peid=entity.eid, pform=self)
+
+    def inline_creation_form_view(self, rschema, ttype, role):
+        """yield inline form views to a newly related (hence created) entity
+        through the given relation
+        """
+        yield self._cw.vreg['views'].select('inline-creation', self._cw,
+                                            etype=ttype, rtype=rschema, role=role,
+                                            peid=self.edited_entity.eid, pform=self)
+
 
 def etype_relation_field(etype, rtype, role='subject'):
     eschema = AutomaticEntityForm.schema.eschema(etype)
@@ -337,7 +375,8 @@
 uicfg.autoform_section.tag_attribute(('*', 'creation_date'), 'metadata')
 uicfg.autoform_section.tag_attribute(('*', 'modification_date'), 'metadata')
 uicfg.autoform_section.tag_attribute(('*', 'cwuri'), 'metadata')
-uicfg.autoform_section.tag_subject_of(('*', 'in_state', '*'), 'primary')
+uicfg.autoform_section.tag_attribute(('*', 'has_text'), 'generated')
+uicfg.autoform_section.tag_subject_of(('*', 'in_state', '*'), 'generated')
 uicfg.autoform_section.tag_subject_of(('*', 'owned_by', '*'), 'metadata')
 uicfg.autoform_section.tag_subject_of(('*', 'created_by', '*'), 'metadata')
 uicfg.autoform_section.tag_subject_of(('*', 'require_permission', '*'), 'generated')
--- a/web/views/basecontrollers.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/basecontrollers.py	Tue Sep 29 15:58:44 2009 +0200
@@ -354,7 +354,7 @@
         entity.eid = varname
         entity['pkey'] = propkey
         form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity)
-        form.form_build_context()
+        form.build_context()
         vfield = form.field_by_name('value')
         renderer = FormRenderer(self._cw)
         return vfield.render(form, renderer, tabindex=tabindex) \
@@ -381,8 +381,7 @@
         view = self._cw.vreg['views'].select('inline-creation', self._cw,
                                          etype=ttype, peid=peid, rtype=rtype,
                                          role=role)
-        return self._call_view(view, etype=ttype, peid=peid,
-                               rtype=rtype, role=role, i18nctx=i18nctx)
+        return self._call_view(view, i18nctx=i18nctx)
 
     @jsonize
     def js_validate_form(self, action, names, values):
--- a/web/views/boxes.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/boxes.py	Tue Sep 29 15:58:44 2009 +0200
@@ -60,8 +60,8 @@
                                       ('addrelated', None)):
             for action in actions.get(category, ()):
                 if category == 'addrelated':
-                    warn('"addrelated" category is deprecated, use "moreaction"'
-                         ' category w/ "addrelated" submenu',
+                    warn('[3.5] "addrelated" category is deprecated, use '
+                         '"moreactions" category w/ "addrelated" submenu',
                          DeprecationWarning)
                     defaultmenu = self._get_menu('addrelated', _('add'), _('add'))
                 if action.submenu:
--- a/web/views/cwproperties.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/cwproperties.py	Tue Sep 29 15:58:44 2009 +0200
@@ -216,7 +216,7 @@
                                                 eidparam=True))
         #subform.vreg = self._cw.vreg
         subform.form_add_hidden('pkey', key, eidparam=True)
-        form.form_add_subform(subform)
+        form.add_subform(subform)
         return subform
 
 
--- a/web/views/editforms.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/editforms.py	Tue Sep 29 15:58:44 2009 +0200
@@ -14,6 +14,7 @@
 from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
+from logilab.common.decorators import cached
 
 from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
                                 specified_etype_implements, yes)
@@ -66,7 +67,7 @@
             subform = self._cw.vreg['forms'].select('base', self._cw,
                                                     entity=entity,
                                                     mainform=False)
-            self.form_add_subform(subform)
+            self.add_subform(subform)
 
 
 class DeleteConfFormView(FormViewMixIn, EntityView):
@@ -418,7 +419,7 @@
                                                  mainform=False)
             # XXX rely on the EntityCompositeFormRenderer to put the eid input
             form.remove_field(form.field_by_name('eid'))
-            self.form_add_subform(form)
+            self.add_subform(form)
 
 
 class TableEditFormView(FormViewMixIn, EntityView):
@@ -436,62 +437,83 @@
 
 
 class InlineEntityEditionFormView(FormViewMixIn, EntityView):
+    """
+    :attr peid: the parent entity's eid hosting the inline form
+    :attr rtype: the relation bridging `etype` and `peid`
+    :attr role: the role played by the `peid` in the relation
+    :attr pform: the parent form where this inlined form is being displayed
+    """
     __regid__ = 'inline-edition'
     __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
+
+    _select_attrs = ('peid', 'rtype', 'role', 'pform')
     removejs = "removeInlinedEntity('%s', '%s', '%s')"
 
-    def call(self, **kwargs):
-        """redefine default call() method to avoid automatic
-        insertions of <div class="section"> between each row of
-        the resultset
-        """
-        rset = self.cw_rset
-        for i in xrange(len(rset)):
-            self.wview(self.__regid__, rset, row=i, **kwargs)
+    def __init__(self, *args, **kwargs):
+        for attr in self._select_attrs:
+            setattr(self, attr, kwargs.pop(attr, None))
+        super(InlineEntityEditionFormView, self).__init__(*args, **kwargs)
+
+    def _entity(self):
+        assert self.cw_row is not None, self
+        return self.cw_rset.get_entity(self.cw_row, self.cw_col)
 
-    def cell_call(self, row, col, peid, rtype, role, i18nctx, **kwargs):
+    @property
+    @cached
+    def form(self):
+        entity = self._entity()
+        form = self.vreg['forms'].select('edition', self._cw,
+                                         entity=entity,
+                                         form_renderer_id='inline',
+                                         mainform=False, copy_nav_params=False,
+                                         **self.extra_kwargs)
+        form.parent_form = self.pform
+        self.add_hiddens(form, entity)
+        return form
+
+    def cell_call(self, row, col, i18nctx, **kwargs):
         """
         :param peid: the parent entity's eid hosting the inline form
         :param rtype: the relation bridging `etype` and `peid`
         :param role: the role played by the `peid` in the relation
         """
-        entity = self.cw_rset.get_entity(row, col)
-        divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype,
-                                                                 entity.eid)
-        self.render_form(entity, peid, rtype, role, i18nctx,
-                         divonclick=divonclick)
+        entity = self._entity()
+        divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (
+            self.peid, self.rtype, entity.eid)
+        self.render_form(i18nctx, divonclick=divonclick, **kwargs)
 
-    def render_form(self, entity, peid, rtype, role, i18nctx, **kwargs):
+    def render_form(self, i18nctx, **kwargs):
         """fetch and render the form"""
-        form = self._cw.vreg['forms'].select('edition', self._cw, entity=entity,
-                                             form_renderer_id='inline',
-                                             formtype='inlined',
-                                             mainform=False,
-                                             copy_nav_params=False)
-        self.add_hiddens(form, entity, peid, rtype, role)
-        divid = '%s-%s-%s' % (peid, rtype, entity.eid)
-        title = self._cw.pgettext(i18nctx, 'This %s' % entity.e_schema)
-        removejs = self.removejs % (peid, rtype, entity.eid)
-        countkey = '%s_count' % rtype
+        entity = self._entity()
+        divid = '%s-%s-%s' % (self.peid, self.rtype, entity.eid)
+        title = self.req.pgettext(i18nctx, 'This %s' % entity.e_schema)
+        removejs = self.removejs % (self.peid, self.rtype, entity.eid)
+        countkey = '%s_count' % self.rtype
         try:
             self._cw.data[countkey] += 1
         except:
             self._cw.data[countkey] = 1
-        self.w(form.form_render(divid=divid, title=title, removejs=removejs,
-                                i18nctx=i18nctx,
-                                counter=self._cw.data[countkey], **kwargs))
+        self.w(self.form.form_render(
+            divid=divid, title=title, removejs=removejs, i18nctx=i18nctx,
+            counter=self.req.data[countkey], **kwargs))
 
-    def add_hiddens(self, form, entity, peid, rtype, role):
+    def add_hiddens(self, form, entity):
         # to ease overriding (see cubes.vcsfile.views.forms for instance)
-        form.form_add_hidden(name='%s:%s' % (rtype, peid), value=entity.eid,
-                             id='rel-%s-%s-%s'  % (peid, rtype, entity.eid))
+        if self.keep_entity(form, entity):
+            if entity.has_eid():
+                rval = entity.eid
+            else:
+                rval = INTERNAL_FIELD_VALUE
+            form.form_add_hidden('edit%s-%s:%s' % (self.role[0], self.rtype, self.peid), rval)
+        form.form_add_hidden(name='%s:%s' % (self.rtype, self.peid), value=entity.eid,
+                             id='rel-%s-%s-%s'  % (self.peid, self.rtype, entity.eid))
 
-    def keep_entity(self, form, entity, peid, rtype):
+    def keep_entity(self, form, entity):
         if not entity.has_eid():
             return True
         # are we regenerating form because of a validation error ?
         if form.form_previous_values:
-            cdvalues = self._cw.list_form_param(eid_param(rtype, peid),
+            cdvalues = self._cw.list_form_param(eid_param(self.rtype, self.peid),
                                                 form.form_previous_values)
             if unicode(entity.eid) not in cdvalues:
                 return False
@@ -499,23 +521,52 @@
 
 
 class InlineEntityCreationFormView(InlineEntityEditionFormView):
+    """
+    :attr etype: the entity type being created in the inline form
+    """
     __regid__ = 'inline-creation'
     __select__ = (match_kwargs('peid', 'rtype')
                   & specified_etype_implements('Any'))
+    _select_attrs = InlineEntityEditionFormView._select_attrs + ('etype',)
     removejs = "removeInlineForm('%s', '%s', '%s')"
 
-    def call(self, etype, peid, rtype, role, i18nctx, **kwargs):
-        """
-        :param etype: the entity type being created in the inline form
-        :param peid: the parent entity's eid hosting the inline form
-        :param rtype: the relation bridging `etype` and `peid`
-        :param role: the role played by the `peid` in the relation
-        """
+    @cached
+    def _entity(self):
         try:
-            cls = self._cw.vreg['etypes'].etype_class(etype)
+            cls = self._cw.vreg['etypes'].etype_class(self.etype)
         except:
             self.w(self._cw._('no such entity type %s') % etype)
             return
-        entity = cls(self._cw)
-        entity.eid = self._cw.varmaker.next()
-        self.render_form(entity, peid, rtype, role, i18nctx, **kwargs)
+        self.initialize_varmaker()
+        entity = cls(self.req)
+        entity.eid = self.varmaker.next()
+        return entity
+
+    def call(self, i18nctx, **kwargs):
+        self.render_form(i18nctx, **kwargs)
+
+
+class InlineAddNewLinkView(InlineEntityCreationFormView):
+    """
+    :attr card: the cardinality of the relation according to role of `peid`
+    """
+    __regid__ = 'inline-addnew-link'
+    __select__ = (match_kwargs('peid', 'rtype')
+                  & specified_etype_implements('Any'))
+
+    _select_attrs = InlineEntityCreationFormView._select_attrs + ('card',)
+    form = None # no actual form wrapped
+
+    def call(self, i18nctx, **kwargs):
+        divid = "addNew%s%s%s:%s" % (self.etype, self.rtype, self.role, self.peid)
+        self.w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
+          % divid)
+        js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % (
+            self.peid, self.etype, self.rtype, self.role, i18nctx)
+        if self.pform.should_hide_add_new_relation_link(self.rtype, self.card):
+            js = "toggleVisibility('%s'); %s" % (divid, js)
+        __ = self.req.pgettext
+        self.w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
+          % (self.rtype, self.peid, js, __(i18nctx, 'add a %s' % self.etype)))
+        self.w(u'</div>')
+        self.w(u'<div class="trame_grise">&#160;</div>')
--- a/web/views/formrenderers.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/formrenderers.py	Tue Sep 29 15:58:44 2009 +0200
@@ -169,7 +169,6 @@
         return tag + '>'
 
     def render_fields(self, w, form, values):
-        form.form_build_context(values)
         fields = self._render_hidden_fields(w, form)
         if fields:
             self._render_fields(fields, w, form)
@@ -292,7 +291,7 @@
     _main_display_fields = None
 
     def render_fields(self, w, form, values):
-        if not form.is_subform:
+        if form.parent_form is None:
             w(u'<table class="listing">')
             subfields = [field for field in form.forms[0].fields
                          if self.display_field(form, field)
@@ -308,14 +307,14 @@
                     w(u'<th>%s</th>' % self._cw._(field.label))
                 w(u'</tr>')
         super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
-        if not form.is_subform:
+        if form.parent_form is None:
             w(u'</table>')
             if self._main_display_fields:
                 super(EntityCompositeFormRenderer, self)._render_fields(
                     self._main_display_fields, w, form)
 
     def _render_fields(self, fields, w, form):
-        if form.is_subform:
+        if form.parent_form is not None:
             entity = form.edited_entity
             values = form.form_previous_values
             qeid = eid_param('eid', entity.eid)
@@ -351,16 +350,19 @@
     # needs some additional points in some case (XXX explain cases)
     __select__ = entity_implements('Any') & yes()
 
-    _options = FormRenderer._options + ('display_relations_form',)
+    _options = FormRenderer._options + ('display_relations_form', 'main_form_title')
     display_relations_form = True
+    main_form_title = _('main information')
 
     def render(self, form, values):
         rendered = super(EntityFormRenderer, self).render(form, values)
         return rendered + u'</div>' # close extra div introducted by open_form
 
     def open_form(self, form, values):
-        attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>'
-                          % self._cw._('main informations'))
+        attrs_fs_label = ''
+        if self.main_form_title:
+            attrs_fs_label += ('<div class="iformTitle"><span>%s</span></div>'
+                               % self.req._(self.main_form_title))
         attrs_fs_label += '<div class="formBody">'
         return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values)
 
@@ -460,51 +462,23 @@
 
     def inline_entities_form(self, w, form):
         """create a form to edit entity's inlined relations"""
-        if not hasattr(form, 'inlined_relations'):
+        if not hasattr(form, 'inlined_form_views'):
             return
-        for rschema, targettypes, role in form.inlined_relations():
-            # show inline forms only if there's one possible target type
-            # for rschema
-            if len(targettypes) != 1:
-                self.warning('entity related by the %s relation should have '
-                             'inlined form but there is multiple target types, '
-                             'dunno what to do', rschema)
-                continue
-            targettype = targettypes[0].type
-            if form.should_inline_relation_form(rschema, targettype, role):
-                self.inline_relation_form(w, form, rschema, targettype, role)
+        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, rschema, targettype, role):
-        entity = form.edited_entity
-        __ = self._cw.pgettext
-        i18nctx = 'inlined:%s.%s.%s' % (entity.e_schema, rschema, role)
-        w(u'<div id="inline%sslot">' % rschema)
-        existant = form.display_inline_edition_form(w, rschema, targettype,
-                                                    role, i18nctx)
-        if role == 'subject':
-            card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0]
-        else:
-            card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1]
-        # there is no related entity and we need at least one: we need to
-        # display one explicit inline-creation view
-        if form.should_display_inline_creation_form(rschema, existant, card):
-            form.display_inline_creation_form(w, rschema, targettype,
-                                              role, i18nctx)
-            existant = True
-        # we can create more than one related entity, we thus display a link
-        # to add new related entities
-        if form.should_display_add_new_relation_link(rschema, existant, card):
-            divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid)
-            w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
-              % divid)
-            js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % (
-                entity.eid, targettype, rschema, role, i18nctx)
-            if form.should_hide_add_new_relation_link(rschema, card):
-                js = "toggleVisibility('%s'); %s" % (divid, js)
-            w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
-              % (rschema, entity.eid, js, __(i18nctx, 'add a %s' % targettype)))
-            w(u'</div>')
-            w(u'<div class="trame_grise">&#160;</div>')
+    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.row, col=formview.col))
         w(u'</div>')
 
 
@@ -541,7 +515,6 @@
         return '\n'.join(data)
 
     def render_fields(self, w, form, values):
-        form.form_build_context(values)
         w(u'<fieldset id="fs-%(divid)s">' % values)
         fields = self._render_hidden_fields(w, form)
         w(u'</fieldset>')
--- a/web/views/forms.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/forms.py	Tue Sep 29 15:58:44 2009 +0200
@@ -10,7 +10,6 @@
 from warnings import warn
 
 from logilab.common.compat import any
-from logilab.common.decorators import iclassmethod
 
 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
@@ -56,7 +55,6 @@
     """
     __regid__ = 'base'
 
-    is_subform = False
     internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
 
     # attributes overrideable by subclasses or through __init__
@@ -84,6 +82,8 @@
                 self.form_add_hidden(key, val)
             elif hasattr(self.__class__, key) and not key[0] == '_':
                 setattr(self, key, val)
+            else:
+                self.extra_kwargs[key] = val
             # skip other parameters, usually given for selection
             # (else write a custom class to handle them)
         if mainform:
@@ -101,52 +101,6 @@
         if 'domid' in kwargs:# session key changed
             self.restore_previous_post(self.session_key())
 
-    @iclassmethod
-    def _fieldsattr(cls_or_self):
-        if isinstance(cls_or_self, type):
-            fields = cls_or_self._fields_
-        else:
-            fields = cls_or_self.fields
-        return fields
-
-    @iclassmethod
-    def field_by_name(cls_or_self, name, role='subject'):
-        """return field with the given name and role.
-        Raise FieldNotFound if the field can't be found.
-        """
-        for field in cls_or_self._fieldsattr():
-            if field.name == name and field.role == role:
-                return field
-        raise form.FieldNotFound(name)
-
-    @iclassmethod
-    def fields_by_name(cls_or_self, name, role='subject'):
-        """return a list of fields with the given name and role"""
-        return [field for field in cls_or_self._fieldsattr()
-                if field.name == name and field.role == role]
-
-    @iclassmethod
-    def remove_field(cls_or_self, field):
-        """remove a field from form class or instance"""
-        cls_or_self._fieldsattr().remove(field)
-
-    @iclassmethod
-    def append_field(cls_or_self, field):
-        """append a field to form class or instance"""
-        cls_or_self._fieldsattr().append(field)
-
-    @iclassmethod
-    def insert_field_before(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
-        fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field), new_field)
-
-    @iclassmethod
-    def insert_field_after(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
-        fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field)+1, new_field)
-
     @property
     def form_needs_multipart(self):
         """true if the form needs enctype=multipart/form-data"""
@@ -174,6 +128,7 @@
         """render this form, using the renderer given in args or the default
         FormRenderer()
         """
+        self.build_context(values)
         renderer = values.pop('renderer', None)
         if renderer is None:
             renderer = self.form_default_renderer()
@@ -184,7 +139,7 @@
                                                      self._cw, rset=self.cw_rset,
                                                      row=self.cw_row, col=self.cw_col)
 
-    def form_build_context(self, rendervalues=None):
+    def build_context(self, rendervalues=None):
         """build form context values (the .context attribute which is a
         dictionary with field instance as key associated to a dictionary
         containing field 'name' (qualified), 'id', 'value' (for display, always
@@ -193,6 +148,8 @@
         rendervalues is an optional dictionary containing extra kwargs given to
         form_render()
         """
+        if self.context is not None:
+            return # already built
         self.context = context = {}
         # ensure rendervalues is a dict
         if rendervalues is None:
@@ -224,6 +181,8 @@
         if value is None:
             if field.name in rendervalues:
                 value = rendervalues[field.name]
+            elif field.name in self.extra_kwargs:
+                value = self.extra_kwargs[field.name]
             else:
                 value = self.form_field_value(field, load_bytes)
                 if callable(value):
@@ -349,9 +308,9 @@
                 searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role)
                                   for eid in value]
                 # remove associated __linkto hidden fields
-                for field in self.fields_by_name('__linkto'):
+                for field in self.root_form.fields_by_name('__linkto'):
                     if field.initial in searchedvalues:
-                        self.remove_field(field)
+                        self.root_form.remove_field(field)
             else:
                 value = None
         return value
@@ -375,21 +334,6 @@
             self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row,
             col=self.cw_col, entity=self.edited_entity)
 
-##    def form_build_context(self, values=None):
-##        """overriden to add edit[s|o] hidden fields and to ensure schema fields
-##        have eidparam set to True
-##        """
-##        eschema = self.edited_entity.e_schema
-##        for field in self.fields[:]:
-##            for field in field.actual_fields(self):
-##                fieldname = field.name
-##                if fieldname != 'eid' and (
-##                    (eschema.has_subject_relation(fieldname) or
-##                     eschema.has_object_relation(fieldname))):
-##                    # XXX why do we need to do this here ?
-##                    field.eidparam = True
-##        return super(EntityFieldsForm, self).form_build_context(values)
-
     def form_field_value(self, field, load_bytes=False):
         """return field's *typed* value
 
@@ -554,31 +498,28 @@
         return False
 
 
-class CompositeForm(FieldsForm):
+class CompositeFormMixIn(object):
     """form composed of sub-forms"""
     __regid__ = 'composite'
     form_renderer_id = __regid__
 
     def __init__(self, *args, **kwargs):
-        super(CompositeForm, self).__init__(*args, **kwargs)
+        super(CompositeFormMixIn, self).__init__(*args, **kwargs)
         self.forms = []
 
-    def form_add_subform(self, subform):
+    def add_subform(self, subform):
         """mark given form as a subform and append it"""
-        subform.is_subform = True
+        subform.parent_form = self
         self.forms.append(subform)
 
+    def build_context(self, rendervalues=None):
+        super(CompositeFormMixIn, self).build_context(rendervalues)
+        for form in self.forms:
+            form.build_context(rendervalues)
+
 
-class CompositeEntityForm(EntityFieldsForm):
-    """form composed of sub-forms"""
-    __regid__ = 'composite'
-    form_renderer_id = __regid__
+class CompositeForm(CompositeFormMixIn, FieldsForm):
+    pass
 
-    def __init__(self, *args, **kwargs):
-        super(CompositeEntityForm, self).__init__(*args, **kwargs)
-        self.forms = []
-
-    def form_add_subform(self, subform):
-        """mark given form as a subform and append it"""
-        subform.is_subform = True
-        self.forms.append(subform)
+class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm):
+    pass # XXX why is this class necessary?
--- a/web/views/ibreadcrumbs.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/ibreadcrumbs.py	Tue Sep 29 15:58:44 2009 +0200
@@ -21,7 +21,7 @@
 
 class BreadCrumbEntityVComponent(Component):
     __regid__ = 'breadcrumbs'
-    __select__ = one_line_rset() & implements(IBreadCrumbs)
+    __select__ = one_line_rset() & implements(IBreadCrumbs, accept_none=False)
 
     cw_property_defs = {
         _('visible'):  dict(type='Boolean', default=True,
@@ -70,7 +70,8 @@
 
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
-    __select__ = two_lines_rset() & one_etype_rset() & implements(IBreadCrumbs)
+    __select__ = two_lines_rset() & one_etype_rset() & \
+                 implements(IBreadCrumbs, accept_none=False)
 
     def render_breadcrumbs(self, contextentity, path):
         # XXX hack: only display etype name or first non entity path part
--- a/web/views/massmailing.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/massmailing.py	Tue Sep 29 15:58:44 2009 +0200
@@ -13,18 +13,16 @@
 from cubicweb.interfaces import IEmailable
 from cubicweb.selectors import implements, match_user_groups
 from cubicweb.view import EntityView
-from cubicweb.web import stdmsgs
-from cubicweb.web.action import Action
-from cubicweb.web.form import FormViewMixIn
-from cubicweb.web.formfields import StringField
+from cubicweb.web import stdmsgs, action, form, formfields as ff
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
 from cubicweb.web.views import forms, formrenderers
 
 
-class SendEmailAction(Action):
+class SendEmailAction(action.Action):
     __regid__ = 'sendemail'
     # XXX should check email is set as well
-    __select__ = implements(IEmailable) & match_user_groups('managers', 'users')
+    __select__ = (action.Action.__select__ & implements(IEmailable)
+                  & match_user_groups('managers', 'users'))
 
     title = _('send email')
     category = 'mainactions'
@@ -40,11 +38,12 @@
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
-    sender = StringField(widget=TextInput({'disabled': 'disabled'}), label=_('From:'))
-    recipient = StringField(widget=CheckBox(), label=_('Recipients:'))
-    subject = StringField(label=_('Subject:'), max_length=256)
-    mailbody = StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
-                                             inputid='mailbody'))
+    sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
+                            label=_('From:'))
+    recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'))
+    subject = ff.StringField(label=_('Subject:'), max_length=256)
+    mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
+                                                inputid='mailbody'))
 
     form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()",
                               _('send email'), 'SEND_EMAIL_ICON'),
@@ -117,7 +116,7 @@
     def render_buttons(self, w, form):
         pass
 
-class MassMailingFormView(FormViewMixIn, EntityView):
+class MassMailingFormView(form.FormViewMixIn, EntityView):
     __regid__ = 'massmailing'
     __select__ = implements(IEmailable) & match_user_groups('managers', 'users')
 
--- a/web/views/workflow.py	Mon Sep 28 16:39:10 2009 +0200
+++ b/web/views/workflow.py	Tue Sep 29 15:58:44 2009 +0200
@@ -83,7 +83,7 @@
         subform = self._cw.vreg['forms'].select('edition', self._cw, entity=trinfo,
                                             mainform=False)
         subform.field_by_name('by_transition').widget = fwdgs.HiddenInput()
-        form.form_add_subform(subform)
+        form.add_subform(subform)
         self.w(form.form_render(wf_info_for=entity.eid,
                                 by_transition=transition.eid))