more form works tls-sprint
authorsylvain.thenault@logilab.fr
Thu, 26 Mar 2009 18:59:01 +0100
branchtls-sprint
changeset 1147 402e8a8b1d6a
parent 1146 547681592765
child 1148 55a8238f8f7c
more form works
web/form.py
web/formfields.py
web/formrenderers.py
web/formwidgets.py
web/test/unittest_form.py
web/views/editforms.py
--- a/web/form.py	Thu Mar 26 18:58:14 2009 +0100
+++ b/web/form.py	Thu Mar 26 18:59:01 2009 +0100
@@ -8,36 +8,24 @@
 
 from warnings import warn
 
-from simplejson import dumps
-
 from logilab.common.compat import any
 from logilab.common.decorators import iclassmethod
-from logilab.mtconverter import html_escape
 
-from cubicweb import typed_eid
-from cubicweb.appobject import AppObject
-from cubicweb.selectors import yes, non_final_entity
+from cubicweb.appobject import AppRsetObject
+from cubicweb.selectors import yes, non_final_entity, match_kwargs, one_line_rset
 from cubicweb.view import NOINDEX, NOFOLLOW
 from cubicweb.common import tags
 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
 from cubicweb.web.httpcache import NoHTTPCacheManager
-from cubicweb.web.controller import NAV_FORM_PARAMETERS, redirect_params
+from cubicweb.web.controller import NAV_FORM_PARAMETERS
 from cubicweb.web.formfields import (Field, StringField, RelationField,
                                      HiddenInitialValueField)
-from cubicweb.web.formwidgets import HiddenInput
+from cubicweb.web.formrenderers import FormRenderer
+from cubicweb.web import formwidgets as fwdgs
 
 
 
-def relation_id(eid, rtype, target, reid):
-    if target == 'subject':
-        return u'%s:%s:%s' % (eid, rtype, reid)
-    return u'%s:%s:%s' % (reid, rtype, eid)
-        
-def toggable_relation_link(eid, nodeid, label='x'):
-    js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
-    return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
-
-
+# XXX should disappear 
 class FormMixIn(object):
     """abstract form mix-in
     XXX: you should inherit from this FIRST (obscure pb with super call)"""
@@ -49,8 +37,8 @@
     add_to_breadcrumbs = False
     skip_relations = set()
     
-    def __init__(self, req, rset):
-        super(FormMixIn, self).__init__(req, rset)
+    def __init__(self, req, rset, **kwargs):
+        super(FormMixIn, self).__init__(req, rset, **kwargs)
         self.maxrelitems = self.req.property_value('navigation.related-limit')
         self.maxcomboitems = self.req.property_value('navigation.combobox-limit')
         self.force_display = not not req.form.get('__force_display')
@@ -110,7 +98,42 @@
                 if inlined_entity.get_widget(irschema, x).need_multipart:
                     return True
         return False
+    
 
+    def button(self, label, klass='validateButton', tabindex=None, **kwargs):
+        if tabindex is None:
+            tabindex = self.req.next_tabindex()
+        return tags.input(value=label, klass=klass, **kwargs)
+
+    def action_button(self, label, onclick=None, __action=None, **kwargs):
+        if onclick is None:
+            onclick = "postForm('__action_%s', \'%s\', \'%s\')" % (
+                __action, label, self.domid)
+        return self.button(label, onclick=onclick, **kwargs)
+
+    def button_ok(self, label=None, type='submit', name='defaultsubmit',
+                  **kwargs):
+        label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
+        return self.button(label, name=name, type=type, **kwargs)
+    
+    def button_apply(self, label=None, type='button', **kwargs):
+        label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
+        return self.action_button(label, __action='apply', type=type, **kwargs)
+
+    def button_delete(self, label=None, type='button', **kwargs):
+        label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
+        return self.action_button(label, __action='delete', type=type, **kwargs)
+    
+    def button_cancel(self, label=None, type='button', **kwargs):
+        label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+        return self.action_button(label, __action='cancel', type=type, **kwargs)
+    
+    def button_reset(self, label=None, type='reset', name='__action_cancel',
+                     **kwargs):
+        label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+        return self.button(label, type=type, **kwargs)
+
+    # XXX deprecated with new form system
     def error_message(self):
         """return formatted error message
 
@@ -137,113 +160,6 @@
                     errormsg = '<ul>%s</ul>' % errormsg
             return u'<div class="errorMessage">%s</div>' % errormsg
         return u''
-    
-    def restore_pending_inserts(self, entity, cell=False):
-        """used to restore edition page as it was before clicking on
-        'search for <some entity type>'
-        
-        """
-        eid = entity.eid
-        cell = cell and "div_insert_" or "tr"
-        pending_inserts = set(self.req.get_pending_inserts(eid))
-        for pendingid in pending_inserts:
-            eidfrom, rtype, eidto = pendingid.split(':')
-            if typed_eid(eidfrom) == entity.eid: # subject
-                label = display_name(self.req, rtype, 'subject')
-                reid = eidto
-            else:
-                label = display_name(self.req, rtype, 'object')
-                reid = eidfrom
-            jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
-                     % (pendingid, cell, eid)
-            rset = self.req.eid_rset(reid)
-            eview = self.view('text', rset, row=0)
-            # XXX find a clean way to handle baskets
-            if rset.description[0][0] == 'Basket':
-                eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
-            yield rtype, pendingid, jscall, label, reid, eview
-        
-    
-    def force_display_link(self):
-        return (u'<span class="invisible">' 
-                u'[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
-                u'</span>' % self.req._('view all'))
-    
-    def relations_table(self, entity):
-        """yiels 3-tuples (rtype, target, related_list)
-        where <related_list> itself a list of :
-          - node_id (will be the entity element's DOM id)
-          - appropriate javascript's togglePendingDelete() function call
-          - status 'pendingdelete' or ''
-          - oneline view of related entity
-        """
-        eid = entity.eid
-        pending_deletes = self.req.get_pending_deletes(eid)
-        # XXX (adim) : quick fix to get Folder relations
-        for label, rschema, target in entity.srelations_by_category(('generic', 'metadata'), 'add'):
-            if rschema in self.skip_relations:
-                continue
-            relatedrset = entity.related(rschema, target, limit=self.limit)
-            toggable_rel_link = self.toggable_relation_link_func(rschema)
-            related = []
-            for row in xrange(relatedrset.rowcount):
-                nodeid = relation_id(eid, rschema, target, relatedrset[row][0])
-                if nodeid in pending_deletes:
-                    status = u'pendingDelete'
-                    label = '+'
-                else:
-                    status = u''
-                    label = 'x'
-                dellink = toggable_rel_link(eid, nodeid, label)
-                eview = self.view('oneline', relatedrset, row=row)
-                related.append((nodeid, dellink, status, eview))
-            yield (rschema, target, related)
-        
-    def toggable_relation_link_func(self, rschema):
-        if not rschema.has_perm(self.req, 'delete'):
-            return lambda x, y, z: u''
-        return toggable_relation_link
-
-
-    def redirect_url(self, entity=None):
-        """return a url to use as next direction if there are some information
-        specified in current form params, else return the result the reset_url
-        method which should be defined in concrete classes
-        """
-        rparams = redirect_params(self.req.form)
-        if rparams:
-            return self.build_url('view', **rparams)
-        return self.reset_url(entity)
-
-    def reset_url(self, entity):
-        raise NotImplementedError('implement me in concrete classes')
-
-    BUTTON_STR = u'<input class="validateButton" type="submit" name="%s" value="%s" tabindex="%s"/>'
-    ACTION_SUBMIT_STR = u'<input class="validateButton" type="button" onclick="postForm(\'%s\', \'%s\', \'%s\')" value="%s" tabindex="%s"/>'
-
-    def button_ok(self, label=None, tabindex=None):
-        label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
-        return self.BUTTON_STR % ('defaultsubmit', label, tabindex or 2)
-    
-    def button_apply(self, label=None, tabindex=None):
-        label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
-        return self.ACTION_SUBMIT_STR % ('__action_apply', label, self.domid,
-                                         label, tabindex or 3)
-
-    def button_delete(self, label=None, tabindex=None):
-        label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
-        return self.ACTION_SUBMIT_STR % ('__action_delete', label, self.domid,
-                                         label, tabindex or 3)
-    
-    def button_cancel(self, label=None, tabindex=None):
-        label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
-        return self.ACTION_SUBMIT_STR % ('__action_cancel', label, self.domid,
-                                         label, tabindex or 4)
-    
-    def button_reset(self, label=None, tabindex=None):
-        label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
-        return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
-            label, tabindex or 4)
 
 
 ###############################################################################
@@ -264,38 +180,43 @@
         return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict)
     
 
-class FieldsForm(FormMixIn, AppObject):
+class FieldsForm(FormMixIn, AppRsetObject):
     __metaclass__ = metafieldsform
     __registry__ = 'forms'
     __select__ = yes()
+    
+    is_subform = False
+    
+    # attributes overrideable through __init__
     internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
     needs_js = ('cubicweb.edition.js',)
     needs_css = ('cubicweb.form.css',)
-    
-    def __init__(self, req, rset=None, domid=None, title=None, action='edit',
-                 onsubmit="return freezeFormButtons('%(domid)s');",
-                 cssclass=None, cssstyle=None, cwtarget=None, buttons=None,
-                 redirect_path=None, set_error_url=True, copy_nav_params=False):
-        self.req = req
-        self.rset = rset
-        self.config = req.vreg.config
-        self.domid = domid or 'form'
-        self.title = title
-        self.action = action
-        self.onsubmit = onsubmit
-        self.cssclass = cssclass
-        self.cssstyle = cssstyle
-        self.cwtarget = cwtarget
-        self.redirect_path = redirect_path
+    domid = 'form'
+    title = None
+    action = None
+    onsubmit = "return freezeFormButtons('%(domid)s');"
+    cssclass = None
+    cssstyle = None
+    cwtarget = None
+    buttons = None
+    redirect_path = None
+    set_error_url = True
+    copy_nav_params = False
+                 
+    def __init__(self, req, rset=None, row=None, col=None, **kwargs):
+        super(FieldsForm, self).__init__(req, rset, row=row, col=col)
+        self.buttons = kwargs.pop('buttons', [])
+        for key, val in kwargs.items():
+            assert hasattr(self.__class__, key) and not key[0] == '_', key
+            setattr(self, key, val)
         self.fields = list(self.__class__._fields_)
-        if set_error_url:
+        if self.set_error_url:
             self.form_add_hidden('__errorurl', req.url())
-        if copy_nav_params:
+        if self.copy_nav_params:
             for param in NAV_FORM_PARAMETERS:
-                value = req.form.get(param)
+                value = kwargs.get(param, req.form.get(param))
                 if value:
                     self.form_add_hidden(param, initial=value)
-        self.buttons = buttons or []
         self.context = None
 
     @iclassmethod
@@ -309,14 +230,24 @@
                 return field
         raise Exception('field %s not found' % name)
     
+    @iclassmethod
+    def remove_field(cls_or_self, field):
+        if isinstance(cls_or_self, type):
+            fields = cls_or_self._fields_
+        else:
+            fields = cls_or_self.fields
+        fields.remove(field)
+    
     @property
     def form_needs_multipart(self):
         return any(field.needs_multipart for field in self.fields) 
 
     def form_add_hidden(self, name, value=None, **kwargs):
-        self.fields.append(StringField(name=name, widget=HiddenInput,
-                                       initial=value, **kwargs))
-
+        field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
+                            **kwargs)
+        self.fields.append(field)
+        return field
+    
     def add_media(self):
         """adds media (CSS & JS) required by this widget"""
         if self.needs_js:
@@ -357,6 +288,14 @@
         else:
             value = field.initial
         return value
+    
+    def form_field_error(self, field):
+        """return validation error for widget's field, if any"""
+        errex = self.req.data.get('formerrors')
+        if errex and field.name in errex.errors:
+            self.req.data['displayederrors'].add(field.name)
+            return u'<span class="error">%s</span>' % errex.errors[field.name]
+        return u''
 
     def form_field_format(self, field):
         return self.req.property_value('ui.default-text-format')
@@ -378,19 +317,28 @@
 
    
 class EntityFieldsForm(FieldsForm):
-    __select__ = non_final_entity()
+    __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
     
     internal_fields = FieldsForm.internal_fields + ('__type', 'eid')
+    domid = 'entityForm'
     
     def __init__(self, *args, **kwargs):
-        kwargs.setdefault('domid', 'entityForm')
-        self.entity = kwargs.pop('entity', None)
+        self.edited_entity = kwargs.pop('entity', None)
+        msg = kwargs.pop('submitmsg', None)
         super(EntityFieldsForm, self).__init__(*args, **kwargs)
+        if self.edited_entity is None:
+            self.edited_entity = self.complete_entity(self.row)
         self.form_add_hidden('__type', eidparam=True)
         self.form_add_hidden('eid')
+        if msg is not None:
+            # If we need to directly attach the new object to another one
+            for linkto in self.req.list_form_param('__linkto'):
+                self.form_add_hidden('__linkto', linkto)
+                msg = '%s %s' % (msg, self.req._('and linked'))
+            self.form_add_hidden('__message', msg)
         
     def form_render(self, **values):
-        self.form_add_entity_hiddens(self.entity.e_schema)
+        self.form_add_entity_hiddens(self.edited_entity.e_schema)
         return super(EntityFieldsForm, self).form_render(**values)
 
     def form_add_entity_hiddens(self, eschema):
@@ -433,21 +381,24 @@
         fieldname = field.name
         if fieldname.startswith('edits-') or fieldname.startswith('edito-'):
             # edit[s|o]- fieds must have the actual value stored on the entity
-            if self.entity.has_eid():
-                value = self._form_field_entity_value(field.visible_field,
-                                                      default_initial=False)
+            if hasattr(field, 'visible_field'):
+                if self.edited_entity.has_eid():
+                    value = self._form_field_entity_value(field.visible_field,
+                                                          default_initial=False)
+                else:
+                    value = INTERNAL_FIELD_VALUE
             else:
-                value = INTERNAL_FIELD_VALUE
+                value = field.initial
         elif fieldname == '__type':
-            value = self.entity.id
+            value = self.edited_entity.id
         elif fieldname == 'eid':
-            value = self.entity.eid
+            value = self.edited_entity.eid
         elif fieldname in values:
             value = values[fieldname]
         elif fieldname in self.req.form:
             value = self.req.form[fieldname]
         else:
-            if self.entity.has_eid() and field.eidparam:
+            if self.edited_entity.has_eid() and field.eidparam:
                 # use value found on the entity or field's initial value if it's
                 # not an attribute of the entity (XXX may conflicts and get
                 # undesired value)
@@ -455,11 +406,11 @@
                                                       load_bytes=load_bytes)
             else:
                 defaultattr = 'default_%s' % fieldname
-                if hasattr(self.entity, defaultattr):
+                if hasattr(self.edited_entity, defaultattr):
                     # XXX bw compat, default_<field name> on the entity
                     warn('found %s on %s, should be set on a specific form'
-                         % (defaultattr, self.entity.id), DeprecationWarning)
-                    value = getattr(self.entity, defaultattr)
+                         % (defaultattr, self.edited_entity.id), DeprecationWarning)
+                    value = getattr(self.edited_entity, defaultattr)
                 elif hasattr(self, defaultattr):
                     # search for default_<field name> on the form instance
                     value = getattr(self, defaultattr)
@@ -471,38 +422,47 @@
         return value
     
     def form_field_format(self, field):
-        entity = self.entity
+        entity = self.edited_entity
         if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
             entity.has_eid() or '%s_format' % field.name in entity):
-            return self.entity.attribute_metadata(field.name, 'format')
+            return self.edited_entity.attribute_metadata(field.name, 'format')
         return self.req.property_value('ui.default-text-format')
 
     def form_field_encoding(self, field):
-        entity = self.entity
+        entity = self.edited_entity
         if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
             entity.has_eid() or '%s_encoding' % field.name in entity):
-            return self.entity.attribute_metadata(field.name, 'encoding')
+            return self.edited_entity.attribute_metadata(field.name, 'encoding')
         return super(EntityFieldsForm, self).form_field_encoding(field)
+    
+    def form_field_error(self, field):
+        """return validation error for widget's field, if any"""
+        errex = self.req.data.get('formerrors')
+        if errex and errex.eid == self.edited_entity.eid and field.name in errex.errors:
+            self.req.data['displayederrors'].add(field.name)
+            return u'<span class="error">%s</span>' % errex.errors[field.name]
+        return u''
 
     def _form_field_entity_value(self, field, default_initial=True, load_bytes=False):
-        attr = field.name 
+        attr = field.name
+        entity = self.edited_entity
         if field.role == 'object':
             attr = 'reverse_' + attr
-        else:
-            attrtype = self.entity.e_schema.destination(attr)
+        elif entity.e_schema.subject_relation(attr).is_final():
+            attrtype = entity.e_schema.destination(attr)
             if attrtype == 'Password':
-                return self.entity.has_eid() and INTERNAL_FIELD_VALUE or ''
+                return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
             if attrtype == 'Bytes':
-                if self.entity.has_eid():
+                if entity.has_eid():
                     if load_bytes:
-                        return getattr(self.entity, attr)
+                        return getattr(entity, attr)
                     # XXX value should reflect if some file is already attached
                     return True
                 return False
         if default_initial:
-            value = getattr(self.entity, attr, field.initial)
+            value = getattr(entity, attr, field.initial)
         else:
-            value = getattr(self.entity, attr)
+            value = getattr(entity, attr)
         if isinstance(field, RelationField):
             # in this case, value is the list of related entities
             value = [ent.eid for ent in value]
@@ -510,24 +470,24 @@
     
     def form_field_name(self, field):
         if field.eidparam:
-            return eid_param(field.name, self.entity.eid)
+            return eid_param(field.name, self.edited_entity.eid)
         return field.name
 
     def form_field_id(self, field):
         if field.eidparam:
-            return eid_param(field.id, self.entity.eid)
+            return eid_param(field.id, self.edited_entity.eid)
         return field.id
         
     def form_field_vocabulary(self, field, limit=None):
         role, rtype = field.role, field.name
         try:
-            vocabfunc = getattr(self.entity, '%s_%s_vocabulary' % (role, rtype))
+            vocabfunc = getattr(self.edited_entity, '%s_%s_vocabulary' % (role, rtype))
         except AttributeError:
             vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
         else:
             # XXX bw compat, default_<field name> on the entity
             warn('found %s_%s_vocabulary on %s, should be set on a specific form'
-                 % (role, rtype, self.entity.id), DeprecationWarning)
+                 % (role, rtype, self.edited_entity.id), DeprecationWarning)
         # NOTE: it is the responsibility of `vocabfunc` to sort the result
         #       (direclty through RQL or via a python sort). This is also
         #       important because `vocabfunc` might return a list with
@@ -544,7 +504,7 @@
         """defaut vocabulary method for the given relation, looking for
         relation's object entities (i.e. self is the subject)
         """
-        entity = self.entity
+        entity = self.edited_entity
         if isinstance(rtype, basestring):
             rtype = entity.schema.rschema(rtype)
         done = None
@@ -566,7 +526,7 @@
         """defaut vocabulary method for the given relation, looking for
         relation's subject entities (i.e. self is the object)
         """
-        entity = self.entity
+        entity = self.edited_entity
         if isinstance(rtype, basestring):
             rtype = entity.schema.rschema(rtype)
         done = None
@@ -587,7 +547,7 @@
                             limit=None, done=None):
         if done is None:
             done = set()
-        rset = self.entity.unrelated(rtype, targettype, role, limit)
+        rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
         res = []
         for entity in rset.entities():
             if entity.eid in done:
@@ -597,118 +557,13 @@
         return res
 
 
-class MultipleFieldsForm(FieldsForm):
+class CompositeForm(FieldsForm):
+    """form composed for sub-forms"""
+    
     def __init__(self, *args, **kwargs):
-        super(MultipleFieldsForm, self).__init__(*args, **kwargs)
+        super(CompositeForm, self).__init__(*args, **kwargs)
         self.forms = []
 
     def form_add_subform(self, subform):
+        subform.is_subform = True
         self.forms.append(subform)
-
-
-# form renderers ############
-
-class FormRenderer(object):
-    button_bar_class = u'formButtonBar'
-    
-    def __init__(self, display_fields=None, display_label=True,
-                 display_help=True, button_bar_class=None):
-        self.display_fields = display_fields # None -> all fields
-        self.display_label = display_label
-        self.display_help = display_help
-        if button_bar_class is not None:
-            self.button_bar_class = button_bar_class
-            
-    # renderer interface ######################################################
-    
-    def render(self, form, values):
-        form.add_media()
-        data = []
-        w = data.append
-        w(self.open_form(form))
-        w(u'<div id="progress">%s</div>' % form.req._('validating...'))
-        w(u'<fieldset>')
-        w(tags.input(type='hidden', name='__form_id', value=form.domid))
-        if form.redirect_path:
-            w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
-        self.render_fields(w, form, values)
-        self.render_buttons(w, form)
-        w(u'</fieldset>')
-        w(u'</form>')
-        return '\n'.join(data)
-        
-    def render_label(self, form, field):
-        label = form.req._(field.label)
-        attrs = {'for': form.context[field]['id']}
-        if field.required:
-            attrs['class'] = 'required'
-        return tags.label(label, **attrs)
-
-    def render_help(self, form, field):
-        help = [ u'<br/>' ]
-        descr = field.help
-        if descr:
-            help.append('<span class="helper">%s</span>' % form.req._(descr))
-        example = field.example_format(form.req)
-        if example:
-            help.append('<span class="helper">(%s: %s)</span>'
-                        % (form.req._('sample format'), example))
-        return u'&nbsp;'.join(help)
-
-    # specific methods (mostly to ease overriding) #############################
-    
-    def open_form(self, form):
-        if form.form_needs_multipart:
-            enctype = 'multipart/form-data'
-        else:
-            enctype = 'application/x-www-form-urlencoded'
-        tag = ('<form action="%s" method="post" id="%s" enctype="%s"' % (
-            html_escape(form.action or '#'), form.domid, enctype))
-        if form.onsubmit:
-            tag += ' onsubmit="%s"' % html_escape(form.onsubmit % form.__dict__)
-        if form.cssstyle:
-            tag += ' style="%s"' % html_escape(form.cssstyle)
-        if form.cssclass:
-            tag += ' class="%s"' % html_escape(form.cssclass)
-        if form.cwtarget:
-            tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
-        return tag + '>'
-
-    def display_field(self, form, field):
-        return (self.display_fields is None
-                or field.name in self.display_fields
-                or field.name in form.internal_fields)
-    
-    def render_fields(self, w, form, values):
-        form.form_build_context(values)
-        fields = form.fields[:]
-        for field in form.fields:
-            if not self.display_field(form, field):
-                fields.remove(field)
-                
-            if not field.is_visible():
-                w(field.render(form, self))
-                fields.remove(field)
-        if fields:
-            self._render_fields(fields, w, form)
-        for childform in getattr(form, 'forms', []):
-            self.render_fields(w, childform, values)
-            
-    def _render_fields(self, fields, w, form,):
-            w(u'<table>')
-            for field in fields:
-                w(u'<tr>')
-                if self.display_label:
-                    w(u'<th>%s</th>' % self.render_label(form, field))
-                w(u'<td style="width:100%;">')
-                w(field.render(form, self))
-                if self.display_help:
-                    w(self.render_help(form, field))
-                w(u'</td></tr>')
-            w(u'</table>')
-
-    def render_buttons(self, w, form):
-        w(u'<table class="%s">\n<tr>\n' % self.button_bar_class)
-        for button in form.form_buttons():
-            w(u'<td>%s</td>\n' % button)
-        w(u'</tr></table>')
--- a/web/formfields.py	Thu Mar 26 18:58:14 2009 +0100
+++ b/web/formfields.py	Thu Mar 26 18:59:01 2009 +0100
@@ -307,7 +307,7 @@
         return RelationField(widget=Select(multiple=card in '*+'), **kwargs)
         
     def vocabulary(self, form):
-        entity = form.entity
+        entity = form.edited_entity
         req = entity.req
         # first see if its specified by __linkto form parameters
         linkedto = entity.linked_to(self.name, self.role)
@@ -334,8 +334,8 @@
     field = None
     for cstr in constraints:
         if isinstance(cstr, StaticVocabularyConstraint):
-            return StringField(widget=Select(vocabulary=cstr.vocabulary),
-                               **kwargs)
+            kwargs.setdefault('widget', Select(vocabulary=cstr.vocabulary))
+            return StringField(**kwargs)
         if isinstance(cstr, SizeConstraint) and cstr.max is not None:
             if cstr.max > 257:
                 rows_cols_from_constraint(cstr, kwargs)
@@ -375,7 +375,8 @@
         if fieldclass is StringField:
             if targetschema == 'Password':
                 # special case for Password field: specific PasswordInput widget
-                return StringField(widget=PasswordInput(), **kwargs)
+                kwargs.setdefault('widget', PasswordInput())
+                return StringField(**kwargs)
             if eschema.has_metadata(rschema, 'format'):
                 # use RichTextField instead of StringField if the attribute has
                 # a "format" metadata. But getting information from constraints
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/formrenderers.py	Thu Mar 26 18:59:01 2009 +0100
@@ -0,0 +1,399 @@
+"""form renderers, responsible to layout a form to html
+
+:organization: Logilab
+:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common import dictattr
+from logilab.mtconverter import html_escape
+
+from simplejson import dumps
+
+from cubicweb.common import tags
+from cubicweb.web import eid_param
+from cubicweb.web import formwidgets as fwdgs
+from cubicweb.web.widgets import checkbox
+
+class FormRenderer(object):
+    """basic renderer displaying fields in a two columns table label | value
+    """
+    button_bar_class = u'formButtonBar'
+    
+    def __init__(self, display_fields=None, display_label=True,
+                 display_help=True, button_bar_class=None):
+        self.display_fields = display_fields # None -> all fields
+        self.display_label = display_label
+        self.display_help = display_help
+        if button_bar_class is not None:
+            self.button_bar_class = button_bar_class
+            
+    # renderer interface ######################################################
+    
+    def render(self, form, values):
+        form.add_media()
+        data = []
+        w = data.append
+        w(self.open_form(form, values))
+        w(u'<div id="progress">%s</div>' % form.req._('validating...'))
+        w(u'<fieldset>')
+        w(tags.input(type=u'hidden', name=u'__form_id',
+                     value=values.get('formvid', form.id)))
+        if form.redirect_path:
+            w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
+        self.render_fields(w, form, values)
+        self.render_buttons(w, form)
+        w(u'</fieldset>')
+        w(u'</form>')
+        errormsg = self.error_message(form)
+        if errormsg:
+            data.insert(0, errormsg)          
+        return '\n'.join(data)
+
+    def render_label(self, form, field):
+        label = form.req._(field.label)
+        attrs = {'for': form.context[field]['id']}
+        if field.required:
+            attrs['class'] = 'required'
+        return tags.label(label, **attrs)
+
+    def render_help(self, form, field):
+        help = [ u'<br/>' ]
+        descr = field.help
+        if descr:
+            help.append('<span class="helper">%s</span>' % form.req._(descr))
+        example = field.example_format(form.req)
+        if example:
+            help.append('<span class="helper">(%s: %s)</span>'
+                        % (form.req._('sample format'), example))
+        return u'&nbsp;'.join(help)
+
+    # specific methods (mostly to ease overriding) #############################
+
+    def error_message(self, form):
+        """return formatted error message
+
+        This method should be called once inlined field errors has been consumed
+        """
+        req = form.req
+        errex = req.data.get('formerrors')
+        # get extra errors
+        if errex is not None:
+            errormsg = req._('please correct the following errors:')
+            displayed = req.data['displayederrors']
+            errors = sorted((field, err) for field, err in errex.errors.items()
+                            if not field in displayed)
+            if errors:
+                if len(errors) > 1:
+                    templstr = '<li>%s</li>\n' 
+                else:
+                    templstr = '&nbsp;%s\n'
+                for field, err in errors:
+                    if field is None:
+                        errormsg += templstr % err
+                    else:
+                        errormsg += templstr % '%s: %s' % (req._(field), err)
+                if len(errors) > 1:
+                    errormsg = '<ul>%s</ul>' % errormsg
+            return u'<div class="errorMessage">%s</div>' % errormsg
+        return u''
+    
+    def open_form(self, form, values):
+        if form.form_needs_multipart:
+            enctype = 'multipart/form-data'
+        else:
+            enctype = 'application/x-www-form-urlencoded'
+        if form.action is None:
+            action = form.req.build_url('edit')
+        else:
+            action = form.action
+        tag = ('<form action="%s" method="post" id="%s" enctype="%s"' % (
+            html_escape(action or '#'), form.domid, enctype))
+        if form.onsubmit:
+            tag += ' onsubmit="%s"' % html_escape(form.onsubmit % dictattr(form))
+        if form.cssstyle:
+            tag += ' style="%s"' % html_escape(form.cssstyle)
+        if form.cssclass:
+            tag += ' class="%s"' % html_escape(form.cssclass)
+        if form.cwtarget:
+            tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
+        return tag + '>'
+    
+    def display_field(self, form, field):
+        return (self.display_fields is None
+                or field.name in self.display_fields
+                or field.name in form.internal_fields)
+    
+    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, values)
+        self.render_child_forms(w, form, values)
+        
+    def render_child_forms(self, w, form, values):
+        # render 
+        for childform in getattr(form, 'forms', []):
+            self.render_fields(w, childform, values)
+
+    def _render_hidden_fields(self, w, form):
+        fields = form.fields[:]
+        for field in form.fields:
+            if not self.display_field(form, field):
+                fields.remove(field)
+            elif not field.is_visible():
+                w(field.render(form, self))
+                fields.remove(field)
+        return fields
+    
+    def _render_fields(self, fields, w, form, values):
+        w(u'<table id="%s" class="attributeForm" style="width:100%%;">')
+        for field in fields:
+            w(u'<tr>')
+            if self.display_label:
+                w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
+            error = form.form_field_error(field)
+            if error:
+                w(u'<td class="error" style="width:100%;">')
+                w(error)
+            else:
+                w(u'<td style="width:100%;">')
+            w(field.render(form, self))
+            if self.display_help:
+                w(self.render_help(form, field))
+            w(u'</td></tr>')
+        w(u'</table>')
+
+    def render_buttons(self, w, form):
+        w(u'<table class="%s">\n<tr>\n' % self.button_bar_class)
+        for button in form.form_buttons():
+            w(u'<td>%s</td>\n' % button)
+        w(u'</tr></table>')
+
+
+    
+class EntityCompositeFormRenderer(FormRenderer):
+    """specific renderer for multiple entities edition form (muledit)"""
+    def render_fields(self, w, form, values):
+        if not form.is_subform:
+            w(u'<table class="listing">')
+        super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
+        if not form.is_subform:
+            w(u'</table>')
+        
+    def _render_fields(self, fields, w, form, values):
+        if form.is_subform:
+            entity = form.edited_entity
+            values = form.req.data.get('formvalues', ())
+            qeid = eid_param('eid', entity.eid)
+            cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid))
+            w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd'))
+            # XXX turn this into a widget used on the eid field
+            w(u'<td>%s</td>' % checkbox('eid', entity.eid, checked=qeid in values))
+            for field in fields:
+                error = form.form_field_error(field)
+                if error:
+                    w(u'<td class="error">')
+                    w(error)
+                else:
+                    w(u'<td>')
+                if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)):
+                    field.widget.attrs['onchange'] = cbsetstate
+                elif isinstance(field.widget, fwdgs.Input):
+                    field.widget.attrs['onkeypress'] = cbsetstate
+                w(u'<div>%s</div>' % field.render(form, self))
+                w(u'/<td>')
+        else:
+            # main form, display table headers
+            w(u'<tr class="header">')
+            w(u'<th align="left">%s</th>'
+              % tags.input(type='checkbox', title=form.req._('toggle check boxes'),
+                           onclick="setCheckboxesState('eid', this.checked)"))
+            for field in self.forms[0].fields:
+                if self.display_field(form, field) and field.is_visible():
+                    w(u'<th>%s</th>' % form.req._(field.label))
+        w(u'</tr>')
+            
+
+            
+class EntityFormRenderer(FormRenderer):
+    """specific renderer for entity edition form (edition)"""
+        
+    def open_form(self, form, values):
+        attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>'
+                          % form.req._('main informations'))
+        attrs_fs_label += '<div class="formBody">'
+        return super(EntityFormRenderer, self).open_form(form, values) + attrs_fs_label
+
+    def render_fields(self, w, form, values):
+        super(EntityFormRenderer, self).render_fields(w, form, values)
+        self.inline_entities_form(w, form)
+        if form.edited_entity.has_eid():
+            self.relations_form(w, form)
+        w(u'</div>') # close extra div introducted by open_form
+
+    def _render_fields(self, fields, w, form, values):
+        if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'):
+            super(EntityFormRenderer, self)._render_fields(fields, w, form, values)
+            
+    def render_buttons(self, w, form):
+        buttons = form.form_buttons()
+        if len(buttons) == 3:
+            w("""<table width="100%%">
+  <tbody>
+   <tr><td align="center">
+     %s
+   </td><td style="align: right; width: 50%%;">
+     %s
+     %s
+   </td></tr>
+  </tbody>
+ </table>""" % tuple(buttons))
+        else:
+            super(EntityFormRenderer, self).render_buttons(w, form)
+        
+    def relations_form(self, w, form):
+        srels_by_cat = form.srelations_by_category(('generic', 'metadata'), 'add')
+        if not srels_by_cat:
+            return u''
+        req = form.req
+        _ = req._
+        label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize()
+        eid = form.edited_entity.eid
+        w(u'<fieldset class="subentity">')
+        w(u'<legend class="iformTitle">%s</legend>' % label)
+        w(u'<table id="relatedEntities">')
+        for rschema, target, related in form.relations_table():
+            # already linked entities
+            if related:
+                w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target))
+                w(u'<td>')
+                w(u'<ul>')
+                for viewparams in related:
+                    w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
+                      % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
+                if not form.force_display and form.maxrelitems < len(related):
+                    link = (u'<span class="invisible">' 
+                            '[<a href="javascript: window.location.href+=\'&amp;__force_display=1\'">%s</a>]'
+                            '</span>' % form.req._('view all'))
+                    w(u'<li class="invisible">%s</li>' % link)
+                w(u'</ul>')
+                w(u'</td>')
+                w(u'</tr>')
+        pendings = list(form.restore_pending_inserts())
+        if not pendings:
+            w(u'<tr><th>&nbsp;</th><td>&nbsp;</td></tr>')
+        else:
+            for row in pendings:
+                # soon to be linked to entities
+                w(u'<tr id="tr%s">' % row[1])
+                w(u'<th>%s</th>' % row[3])
+                w(u'<td>')
+                w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
+                  (_('cancel this insert'), row[2]))
+                w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
+                  % (row[1], row[4], html_escape(row[5])))
+                w(u'</td>')
+                w(u'</tr>')
+        w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
+        w(u'<th class="labelCol">')
+        w(u'<span>%s</span>' % _('add relation'))
+        w(u'<select id="relationSelector_%s" tabindex="%s" onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
+          % (eid, req.next_tabindex(), html_escape(dumps(eid))))
+        w(u'<option value="">%s</option>' % _('select a relation'))
+        for i18nrtype, rschema, target in srels_by_cat:
+            # more entities to link to
+            w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype))
+        w(u'</select>')
+        w(u'</th>')
+        w(u'<td id="unrelatedDivs_%s"></td>' % eid)
+        w(u'</tr>')
+        w(u'</table>')
+        w(u'</fieldset>')
+        
+    def inline_entities_form(self, w, form):
+        """create a form to edit entity's inlined relations"""
+        entity = form.edited_entity
+        __ = form.req.__
+        for rschema, targettypes, role in form.relations_by_category('inlineview', 'add'):
+            # 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):
+                w(u'<div id="inline%sslot">' % rschema)
+                existant = entity.has_eid() and entity.related(rschema)
+                if existant:
+                    # display inline-edition view for all existing related entities
+                    w(self.view('inline-edition', existant, rtype=rschema, role=role, 
+                                ptype=entity.e_schema, peid=entity.eid,
+                                **kwargs))
+                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):
+                    w(self.view('inline-creation', None, etype=targettype,
+                                peid=entity.eid, ptype=entity.e_schema,
+                                rtype=rschema, role=role, **kwargs))
+                # 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, entity.e_schema, targettype, rschema, role)
+                    if card in '1?':
+                        js = "toggleVisibility('%s'); %s" % (divid, js)
+                    w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
+                      % (rschema, entity.eid, js, __('add a %s' % targettype)))
+                    w(u'</div>')
+                    w(u'<div class="trame_grise">&nbsp;</div>')
+                w(u'</div>')
+
+    
+class EntityInlinedFormRenderer(EntityFormRenderer):
+    """specific renderer for entity inlined edition form
+    (inline-[creation|edition])
+    """
+    def render(self, form, values):
+        form.add_media()
+        data = []
+        w = data.append
+        try:
+            w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
+        except KeyError:
+            w(u'<div id="div-%(divid)s">' % values)
+        else:
+            w(u'<div id="notice-%s" class="notice">%s</div>' % (
+                values['divid'], form.req._('click on the box to cancel the deletion')))
+        w(u'<div class="iformBody">')
+        values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema)
+        w(u'<div class="iformTitle"><span>%(title)s</span> '
+          '#<span class="icounter">1</span> '
+          '[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>'
+          % values)
+        self.render_fields(w, form, values)
+        w(u'</div>')
+        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>')
+        w(u'<fieldset class="subentity">')
+        if fields:
+            self._render_fields(fields, w, form, values)
+        self.render_child_forms(w, form, values)
+        self.inline_entities_form(w, form)
+        w(u'</fieldset>')
+    
--- a/web/formwidgets.py	Thu Mar 26 18:58:14 2009 +0100
+++ b/web/formwidgets.py	Thu Mar 26 18:59:01 2009 +0100
@@ -206,8 +206,6 @@
         if not value:
             value = date.today()
         year, month = value.year, value.month
-        onclick = "toggleCalendar('%s', '%s', %s, %s);" % (
-            helperid, inputid, year, month)
         return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
                 % (helperid, inputid, year, month,
@@ -226,5 +224,5 @@
             
     def render(self, form, field):
         self.add_media(form)
-        name, values, attrs = self._render_attrs(form, field)
+        attrs = self._render_attrs(form, field)[-1]
         return tags.div(**attrs)
--- a/web/test/unittest_form.py	Thu Mar 26 18:58:14 2009 +0100
+++ b/web/test/unittest_form.py	Thu Mar 26 18:59:01 2009 +0100
@@ -25,31 +25,56 @@
             creation_date = DateTimeField(widget=DateTimePicker)
         form = CustomChangeStateForm(self.req, redirect_path='perdu.com',
                                      entity=self.entity)
-        self.assertTextEquals(form.form_render(state=123, trcomment=u''),
-                              ''' ''')
+        form.form_render(state=123, trcomment=u'')
 
     def test_change_state_form(self):
         form = ChangeStateForm(self.req, redirect_path='perdu.com',
                                entity=self.entity)
-        self.assertTextEquals(form.form_render(state=123, trcomment=u''),
-                              ''' ''')
+        form.form_render(state=123, trcomment=u'')
         
     def test_delete_conf_form_multi(self):
         rset = self.execute('EGroup X')
-        self.assertTextEquals(self.view('deleteconf', rset, template=None).source,
-                              '')
+        self.view('deleteconf', rset, template=None).source
         
     def test_massmailing_form(self):
         self.execute('INSERT EmailAddress X: X address L + "@cubicweb.org", '
                      'U use_email X WHERE U is EUser, U login L')
         rset = self.execute('EUser X')
-        self.assertTextEquals(self.view('massmailing', rset, template=None).source,
-                              '')
+        self.view('massmailing', rset, template=None)
+        
+    def test_automatic_edition_form(self):
+        rset = self.execute('EUser X')
+        self.view('edition', rset, row=0, template=None).source
+        
+    def test_automatic_edition_form(self):
+        rset = self.execute('EUser X')
+        self.view('copy', rset, row=0, template=None).source
+        
+    def test_automatic_creation_form(self):
+        self.view('creation', None, etype='EUser', template=None).source
+        
+    def test_automatic_muledit_form(self):
+        rset = self.execute('EUser X')
+        self.view('muledit', rset, template=None).source
+        
+    def test_automatic_reledit_form(self):
+        rset = self.execute('EUser X')
+        self.view('reledit', rset, row=0, rtype='login', template=None).source
+        
+    def test_automatic_inline_edit_form(self):
+        geid = self.execute('EGroup X LIMIT 1')[0][0]
+        rset = self.execute('EUser X LIMIT 1')
+        self.view('inline-edition', rset, row=0, rtype='in_group', peid=geid, template=None).source
+                              
+    def test_automatic_inline_creation_form(self):
+        geid = self.execute('EGroup X LIMIT 1')[0][0]
+        self.view('inline-creation', None, etype='EUser', rtype='in_group', peid=geid, template=None).source
+
 
     # fields tests ############################################################
 
     def _render_entity_field(self, name, form):
-        form.form_add_entity_hiddens(form.entity.e_schema)
+        form.form_add_entity_hiddens(form.edited_entity.e_schema)
         form.form_build_context({})
         return form.field_by_name(name).render(form, self.renderer)
     
--- a/web/views/editforms.py	Thu Mar 26 18:58:14 2009 +0100
+++ b/web/views/editforms.py	Thu Mar 26 18:59:01 2009 +0100
@@ -7,20 +7,43 @@
 """
 __docformat__ = "restructuredtext en"
 
+from copy import copy
+
 from simplejson import dumps
 
-from cubicweb.selectors import match_kwargs, one_line_rset, non_final_entity
+from logilab.mtconverter import html_escape
+
+from cubicweb import typed_eid
+from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
+                                specified_etype_implements, yes)
+from cubicweb.rtags import RelationTags
 from cubicweb.utils import make_uid
 from cubicweb.view import EntityView
 from cubicweb.common import tags
-from cubicweb.web import stdmsgs
-from cubicweb.web.form import MultipleFieldsForm, EntityFieldsForm, FormMixIn, FormRenderer
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, formwidgets
+from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormMixIn
 from cubicweb.web.formfields import guess_field
+from cubicweb.web.formrenderers import (FormRenderer, EntityFormRenderer,
+                                        EntityCompositeFormRenderer,
+                                        EntityInlinedFormRenderer)
+_ = unicode
 
-_ = unicode
+def relation_id(eid, rtype, role, reid):
+    """return an identifier for a relation between two entities"""
+    if role == 'subject':
+        return u'%s:%s:%s' % (eid, rtype, reid)
+    return u'%s:%s:%s' % (reid, rtype, eid)
+        
+def toggable_relation_link(eid, nodeid, label='x'):
+    """return javascript snippet to delete/undelete a relation between two
+    entities
+    """
+    js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
+    return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
 
 
 class DeleteConfForm(EntityView):
+    """form used to confirm deletion of some entities"""
     id = 'deleteconf'
     title = _('delete')
     domid = 'deleteconf'
@@ -37,19 +60,18 @@
           % _('this action is not reversible!'))
         # XXX above message should have style of a warning
         w(u'<h4>%s</h4>\n' % _('Do you want to delete the following element(s) ?'))
-        form = MultipleFieldsForm(req, domid='deleteconf', action=self.build_url('edit'),
+        form = CompositeForm(req, domid='deleteconf', action=self.build_url('edit'),
                                   onsubmit=self.onsubmit, copy_nav_params=True)
+        # XXX tabindex
         form.buttons.append(form.button_delete(label=stdmsgs.YES))
         form.buttons.append(form.button_cancel(label=stdmsgs.NO))
         done = set()
         w(u'<ul>\n')
-        for i in xrange(self.rset.rowcount):
-            if self.rset[i][0] in done:
+        for entity in self.rset.entities():
+            if entity.eid in done:
                 continue
-            done.add(self.rset[i][0])
-            entity = self.rset.get_entity(i, 0)
-            subform = EntityFieldsForm(req, set_error_url=False,
-                                       entity=entity)
+            done.add(entity.eid)
+            subform = EntityFieldsForm(req, entity=entity, set_error_url=False)
             form.form_add_subform(subform)
             # don't use outofcontext view or any other that may contain inline edition form
             w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
@@ -59,12 +81,21 @@
 
 
 class ClickAndEditForm(FormMixIn, EntityView):
+    """form used to permit ajax edition of an attribute of an entity in a view
+    
+    (double-click on the field to see an appropriate edition widget)
+    """
     id = 'reledit'
     __select__ = non_final_entity() & match_kwargs('rtype')
-
+    
     # FIXME editableField class could be toggleable from userprefs
-      
+    
+    onsubmit = ("return inlineValidateForm('%(divid)s-form', '%(rtype)s', "
+                "'%(eid)s', '%(divid)s', %(reload)s);")
+    ondblclick = "showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
+    
     def cell_call(self, row, col, rtype=None, role='subject', reload=False):
+        """display field to edit entity's `rtype` relation on double-click"""
         entity = self.entity(row, col)
         if getattr(entity, rtype) is None:
             value = self.req._('not specified')
@@ -78,84 +109,520 @@
         edit_key = make_uid('%s-%s' % (rtype, eid))
         divid = 'd%s' % edit_key
         reload = dumps(reload)
-        buttons = [tags.input(klass="validateButton", type="submit", name="__action_apply",
-                              value=self.req._(stdmsgs.BUTTON_OK), tabindex=self.req.next_tabindex()),
+        # XXX tab index
+        buttons = [tags.input(klass="validateButton", type="submit",
+                              name="__action_apply",
+                              value=self.req._(stdmsgs.BUTTON_OK),
+                              tabindex=self.req.next_tabindex()),
                    tags.input(klass="validateButton", type="button",
                               value=self.req._(stdmsgs.BUTTON_CANCEL),
                               onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (eid, rtype, divid),
                               tabindex=self.req.next_tabindex())]
-        form = self.vreg.select_object('forms', 'edition', self.req, self.rset, row=row, col=col,
-                                       entity=entity, domid='%s-form' % divid, action='#',
-                                       cssstyle='display: none', buttons=buttons,
-                                       onsubmit="return inlineValidateForm('%(divid)s-form', '%(rtype)s', '%(eid)s', '%(divid)s', %(reload)s);" % locals())
+        form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+                                       row=row, col=col, buttons=buttons,
+                                       domid='%s-form' % divid, action='#',
+                                       cssstyle='display: none',
+                                       onsubmit=self.onsubmit % locals())
         renderer = FormRenderer(display_label=False, display_help=False,
                                 display_fields=(rtype,), button_bar_class='buttonbar')
         self.w(tags.div(value, klass='editableField', id=divid,
-                        ondblclick="showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" % locals()))
-        self.w(form.render(renderer=renderer))
+                        ondblclick=self.ondblclick % locals()))
+        self.w(form.form_render(renderer=renderer))
 
 
 class AutomaticEntityForm(EntityFieldsForm):
+    """base automatic form to edit any entity
+
+    Designed to be flly generated from schema but highly configurable through:
+    * rtags (rcategories, rwidgets, inlined, rpermissions)
+    * various standard form parameters
+
+    You can also easily customise it by adding/removing fields in
+    AutomaticEntityForm instances.
+    """
     id = 'edition'
+    
     needs_js = EntityFieldsForm.needs_js + ('cubicweb.ajax.js',)
+    cwtarget = 'eformframe'
+    cssclass = 'entityForm'
+    copy_nav_params = True
+    attrcategories = ('primary', 'secondary')
+    
+    # relations'category (eg primary/secondary/generic/metadata/generated)
+    rcategories = RelationTags()
+    # use primary and not generated for eid since it has to be an hidden
+    rcategories.set_rtag('primary', 'subject', 'eid')
+    rcategories.set_rtag('metadata', 'subject', 'creation_date')
+    rcategories.set_rtag('metadata', 'subject', 'modification_date')
+    rcategories.set_rtag('generated', 'subject', 'has_text')        
+
+    # relations'widget (eg one of available class name in cubicweb.web.formwidgets)
+    rwidgets = RelationTags()
+    # inlined view flag for non final relations
+    inlined = RelationTags()
+    # set of tags of the form <action>_on_new on relations. <action> is a
+    # schema action (add/update/delete/read), and when such a tag is found
+    # permissions checking is by-passed and supposed to be ok
+    rpermissions_overrides = RelationTags(use_set=True)
+
+    @classmethod
+    def registered(cls, registry):
+        """build class using descriptor at registration time"""
+        super(AutomaticEntityForm, cls).registered(registry)
+        cls.init_rtags_category()
+        return cls
+        
+    @classmethod
+    def init_rtags_category(cls):
+        """set default category tags for relations where it's not yet defined in
+        the category relation tags
+        """
+        for eschema in cls.schema.entities():
+            for rschema, tschemas, role in eschema.relation_definitions(True):
+                for tschema in tschemas:
+                    if role == 'subject':
+                        X, Y = eschema, tschema
+                        card = rschema.rproperty(X, Y, 'cardinality')[0]
+                        composed = rschema.rproperty(X, Y, 'composite') == 'object'
+                    else:
+                        X, Y = tschema, eschema
+                        card = rschema.rproperty(X, Y, 'cardinality')[1]
+                        composed = rschema.rproperty(X, Y, 'composite') == 'subject'
+                    if not cls.rcategories.rtag(role, rschema, X, Y):
+                        if card in '1+':
+                            if not rschema.is_final() and composed:
+                                category = 'generated'
+                            else:
+                                category = 'primary'
+                        elif rschema.is_final():
+                            category = 'secondary'
+                        else: 
+                            category = 'generic'
+                        cls.rcategories.set_rtag(category, role, rschema, X, Y)
+                        
     
     def __init__(self, *args, **kwargs):
         super(AutomaticEntityForm, self).__init__(*args, **kwargs)
-        self.entity.complete()
-        for rschema, target in self.editable_attributes(self.entity):
-            field = guess_field(self.entity.__class__, self.entity.e_schema,
-                                rschema, target)
+        if self.edited_entity.has_eid():
+            self.edited_entity.complete()
+        for rschema, role in self.editable_attributes():
+            wdgname = self.rwidgets.etype_rtag(self.edited_entity.id, role, rschema)
+            if wdgname:
+                field = guess_field(self.edited_entity.__class__, rschema, role,
+                                    eidparam=True, widget=getattr(formwidgets, wdgname))
+            else:
+                field = guess_field(self.edited_entity.__class__, rschema, role,
+                                    eidparam=True)
             self.fields.append(field)
-            
+        
+    def action(self):
+        """return the form's action attribute"""
+        try:
+            return self._action
+        except AttributeError:
+            return self.build_url('validateform')
+        
+    def set_action(self, value):
+        self._action = value
+        
+    action = property(action, set_action)
+    
     def form_buttons(self):
+        """return the form's buttons (as string)"""
         return [self.button_ok(tabindex=self.req.next_tabindex()),
                 self.button_apply(tabindex=self.req.next_tabindex()),
                 self.button_cancel(tabindex=self.req.next_tabindex())]
 
-    def editable_attributes(self, entity):
-        # XXX both (add, delete) required for non final relations
-        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
+    def editable_attributes(self):
+        """return a list of (relation schema, role) to edit for the entity
+        """
+        return [(rschema, x) for rschema, _, x in self.relations_by_category(self.attrcategories, 'add')
                 if rschema != 'eid']
     
-class _EditionForm(EntityView):
-    """primary entity edition form
+    def relations_by_category(self, categories=None, permission=None):
+        """return a list of (relation schema, target schemas, role) matching
+        categories and permission
+        """
+        if categories is not None:
+            if not isinstance(categories, (list, tuple, set, frozenset)):
+                categories = (categories,)
+            if not isinstance(categories, (set, frozenset)):
+                categories = frozenset(categories)
+        eschema  = self.edited_entity.e_schema
+        rtags = self.rcategories
+        permsoverrides = self.rpermissions_overrides
+        if self.edited_entity.has_eid():
+            eid = self.edited_entity.eid
+        else:
+            eid = None
+        for rschema, targetschemas, role in eschema.relation_definitions(True):
+            if rschema in ('identity', 'has_text'):
+                continue
+            # check category first, potentially lower cost than checking
+            # permission which may imply rql queries
+            if categories is not None:
+                targetschemas = [tschema for tschema in targetschemas
+                                 if rtags.etype_rtag(eschema, role, rschema, tschema) in categories]
+                if not targetschemas:
+                    continue
+            if permission is not None:
+                # tag allowing to hijack the permission machinery when
+                # permission is not verifiable until the entity is actually
+                # created...
+                if eid is None and '%s_on_new' % permission in permsoverrides.etype_rtags(eschema, role, rschema):
+                    yield (rschema, targetschemas, role)
+                    continue
+                if rschema.is_final():
+                    if not rschema.has_perm(self.req, permission, eid):
+                        continue
+                elif role == 'subject':
+                    if not ((eid is None and rschema.has_local_role(permission)) or
+                            rschema.has_perm(self.req, permission, fromeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
+                        and eid and self.edited_entity.related(rschema.type, role)
+                        and not rschema.has_perm(self.req, 'delete', fromeid=eid,
+                                                 toeid=self.edited_entity.related(rschema.type, role)[0][0])):
+                        continue
+                elif role == 'object':
+                    if not ((eid is None and rschema.has_local_role(permission)) or
+                            rschema.has_perm(self.req, permission, toeid=eid)):
+                        continue
+                    # on relation with cardinality 1 or ?, we need delete perm as well
+                    # if the relation is already set
+                    if (permission == 'add'
+                        and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
+                        and eid and self.edited_entity.related(rschema.type, role)
+                        and not rschema.has_perm(self.req, 'delete', toeid=eid,
+                                                 fromeid=self.edited_entity.related(rschema.type, role)[0][0])):
+                        continue
+            yield (rschema, targetschemas, role)
+    
+    def srelations_by_category(self, categories=None, permission=None):
+        """filter out result of relations_by_category(categories, permission) by
+        removing final relations
 
-    When generating a new attribute_input, the editor will look for a method
-    named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the
-    name of the attribute being edited. You may use this feature to compute
-    dynamic default values such as the 'tomorrow' date or the user's login
-    being connected
-    """    
+        return a list of (relation's label, relation'schema, role)
+        """
+        result = []
+        for rschema, ttypes, role in self.relations_by_category(categories,
+                                                                permission):
+            if rschema.is_final():
+                continue
+            result.append( (rschema.display_name(self.req, role), rschema, role) )
+        return sorted(result)
+        
+    def relations_table(self):
+        """yiels 3-tuples (rtype, target, related_list)
+        where <related_list> itself a list of :
+          - node_id (will be the entity element's DOM id)
+          - appropriate javascript's togglePendingDelete() function call
+          - status 'pendingdelete' or ''
+          - oneline view of related entity
+        """
+        entity = self.edited_entity
+        pending_deletes = self.req.get_pending_deletes(entity.eid)
+        # XXX add metadata category quick fix to get Folder relations
+        for label, rschema, role in self.srelations_by_category(('generic', 'metadata'), 'add'):
+            relatedrset = entity.related(rschema, role, limit=self.limit)
+            if rschema.has_perm(self.req, 'delete'):
+                toggable_rel_link_func = toggable_relation_link
+            else:
+                toggable_rel_link_func = lambda x, y, z: u''
+            related = []
+            for row in xrange(relatedrset.rowcount):
+                nodeid = relation_id(entity.eid, rschema, role,
+                                     relatedrset[row][0])
+                if nodeid in pending_deletes:
+                    status = u'pendingDelete'
+                    label = '+'
+                else:
+                    status = u''
+                    label = 'x'
+                dellink = toggable_rel_link_func(entity.eid, nodeid, label)
+                eview = self.view('oneline', relatedrset, row=row)
+                related.append((nodeid, dellink, status, eview))
+            yield (rschema, role, related)
+            
+    def restore_pending_inserts(self, cell=False):
+        """used to restore edition page as it was before clicking on
+        'search for <some entity type>'
+        """
+        eid = self.edited_entity.eid
+        cell = cell and "div_insert_" or "tr"
+        pending_inserts = set(self.req.get_pending_inserts(eid))
+        for pendingid in pending_inserts:
+            eidfrom, rtype, eidto = pendingid.split(':')
+            if typed_eid(eidfrom) == eid: # subject
+                label = display_name(self.req, rtype, 'subject')
+                reid = eidto
+            else:
+                label = display_name(self.req, rtype, 'object')
+                reid = eidfrom
+            jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
+                     % (pendingid, cell, eid)
+            rset = self.req.eid_rset(reid)
+            eview = self.view('text', rset, row=0)
+            # XXX find a clean way to handle baskets
+            if rset.description[0][0] == 'Basket':
+                eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
+            yield rtype, pendingid, jscall, label, reid, eview
+            
+    # should_* method extracted to allow overriding
+    
+    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
+        """
+        return self.inlined.etype_rtag(self.edited_entity.id, role, rschema, targettype)
+
+    def should_display_inline_creation_form(self, rschema, existant, card):
+        """return true if a creation form should be inlined
+
+        by default true if there is no related entity and we need at least one
+        """
+        return not existant and card in '1+'
+
+    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)
+
+        by default true if there is no related entity or if the relation has
+        multiple cardinality
+        """
+        return not existant or card in '+*'
+
+    
+class EditionFormView(EntityView):
+    """display primary entity edition form"""    
     id = 'edition'
-    __select__ = one_line_rset() & non_final_entity()
+    # add yes() so it takes precedence over deprecated views in baseforms,
+    # though not baseforms based customized view
+    __select__ = one_line_rset() & non_final_entity() & yes()
 
     title = _('edition')
     controller = 'edit'
-    skip_relations = FormMixIn.skip_relations.copy()
-
+    
     def cell_call(self, row, col, **kwargs):
-        self.req.add_js( ('cubicweb.ajax.js',) )
-        self.initialize_varmaker()
         entity = self.complete_entity(row, col)
+        self.render_form(entity)
+        
+    def render_form(self, entity):
+        """fetch and render the form"""
+        self.form_title(entity)
+        form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+                                       row=self.row, col=self.col, entity=entity,
+                                       domid=self.id, submitmsg=self.submited_message())
+        self.init_form(form, entity)
+        self.w(form.form_render(renderer=EntityFormRenderer(), formvid=u'edition'))
 
-    def initialize_varmaker(self):
-        varmaker = self.req.get_page_data('rql_varmaker')
-        if varmaker is None:
-            varmaker = self.req.varmaker
-            self.req.set_page_data('rql_varmaker', varmaker)
-        self.varmaker = varmaker
+    def init_form(self, form, entity):
+        """customize your form before rendering here"""
+        form.form_add_hidden(u'__maineid', entity.eid)
+        
+    def form_title(self, entity):
+        """the form view title"""
+        ptitle = self.req._(self.title)
+        self.w(u'<div class="formTitle"><span>%s %s</span></div>' % (
+            entity.dc_type(), ptitle and '(%s)' % ptitle))
+    
+    def submited_message(self):
+        """return the message that will be displayed on successful edition"""
+        return self.req._('entity edited')
+
         
-    def edit_form(self, entity, kwargs):
-        form = EntityFieldsForm(self.req, entity=entity)
-        for rschema, target in self.editable_attributes(entity):
-            field = guess_field(entity.__class__, entity.e_schema, rschema, target)
-            form.fields.append(field)
-        form.buttons.append(form.button_ok())
-        form.buttons.append(form.button_apply())
-        form.buttons.append(form.button_cancel())
-        self.w(form.form_render())
+class CreationFormView(EditionFormView):
+    """display primary entity creation form"""    
+    id = 'creation'
+    __select__ = specified_etype_implements('Any') & yes()
+    
+    title = _('creation')
+    
+    def call(self, **kwargs):
+        """creation view for an entity"""
+        etype = kwargs.pop('etype', self.req.form.get('etype'))
+        try:
+            entity = self.vreg.etype_class(etype)(self.req, None, None)
+        except:
+            self.w(self.req._('no such entity type %s') % etype)
+        else:
+            self.initialize_varmaker()
+            entity.eid = self.varmaker.next()
+            self.render_form(entity)
+    
+    def form_title(self, entity):
+        """the form view title"""
+        if '__linkto' in self.req.form:
+            if isinstance(self.req.form['__linkto'], list):
+                # XXX which one should be considered (case: add a ticket to a
+                # version in jpl)
+                rtype, linkto_eid, role = self.req.form['__linkto'][0].split(':')
+            else:
+                rtype, linkto_eid, role = self.req.form['__linkto'].split(':')
+            linkto_rset = self.req.eid_rset(linkto_eid)
+            linkto_type = linkto_rset.description[0][0]
+            if role == 'subject':
+                title = self.req.__('creating %s (%s %s %s %%(linkto)s)' % (
+                    entity.e_schema, entity.e_schema, rtype, linkto_type))
+            else:
+                title = self.req.__('creating %s (%s %%(linkto)s %s %s)' % (
+                    entity.e_schema, linkto_type, rtype, entity.e_schema))
+            msg = title % {'linkto' : self.view('incontext', linkto_rset)}
+            self.w(u'<div class="formTitle notransform"><span>%s</span></div>' % msg)
+        else:
+            super(CreationFormView, self).form_title(entity)
+    
+    def url(self):
+        """return the url associated with this view"""
+        return self.create_url(self.req.form.get('etype'))
+    
+    def submited_message(self):
+        """return the message that will be displayed on successful edition"""
+        return self.req._('entity created')
+
+
+class CopyFormView(EditionFormView):
+    """display primary entity creation form initialized with values from another
+    entity
+    """    
+    id = 'copy'
+    def render_form(self, entity):
+        """fetch and render the form"""
+        # make a copy of entity to avoid altering the entity in the
+        # request's cache. 
+        self.newentity = copy(entity)
+        self.copying = self.newentity.eid
+        self.newentity.eid = None
+        self.w(u'<script type="text/javascript">updateMessage("%s");</script>\n'
+               % self.req._('Please note that this is only a shallow copy'))
+        super(CopyFormView, self).render_form(entity)
+        del self.newentity
+    
+    def init_form(self, form, entity):
+        """customize your form before rendering here"""
+        super(CopyFormView, self).init_form(form, entity)
+        if entity.eid == self.newentity.eid:
+            form.form_add_hidden('__cloned_eid', self.copying, eidparam=True)
+    
+    def submited_message(self):
+        """return the message that will be displayed on successful edition"""
+        return self.req._('entity copied')
+
+    
+class TableEditForm(CompositeForm):
+    id = 'muledit'
+    onsubmit = "return validateForm('entityForm', null);"
+    
+    def __init__(self, *args, **kwargs):
+        super(TableEditForm, self).__init__(*args, **kwargs)
+        for row in xrange(len(self.rset)):
+            form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+                                           row=row, domid=self.id,
+                                           attrcategories=('primary',),
+                                           set_error_url=False)
+            # XXX rely on the MultipleEntityFormRenderer to put the eid input
+            form.remove_field(form.field_by_name('eid'))
+            self.form_add_subform(form)
 
-    def editable_attributes(self, entity):
-        # XXX both (add, delete)
-        return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
-                if rschema != 'eid']
+    def form_buttons(self):
+        """return the form's buttons (as string)"""
+        okt = self.req._('validate modifications on selected items').capitalize()
+        resett = self.req._('revert changes').capitalize()
+        return [self.button_ok(title=okt), self.button_reset(title=resett)]
+
+        
+class TableEditFormView(EntityView):
+    id = 'muledit'
+    __select__ = EntityView.__select__ & yes()
+    title = _('multiple edit')
+    
+    def call(self, **kwargs):
+        """a view to edit multiple entities of the same type the first column
+        should be the eid
+        """
+        #self.form_title(entity)
+        form = self.vreg.select_object('forms', self.id, self.req, self.rset,
+                                       domid=self.id)
+        self.w(form.form_render(renderer=EntityCompositeFormRenderer()))
+
+
+class InlineEntityEditionFormView(EntityView):
+    id = 'inline-edition'
+    __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
+    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.rset
+        for i in xrange(len(rset)):
+            self.wview(self.id, rset, row=i, **kwargs)
+
+    def cell_call(self, row, col, peid, rtype, role='subject', **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.entity(row, col)
+        divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype,
+                                                                 entity.eid)
+        self.render_form(entity, peid, rtype, role, divonclick=divonclick)
+        
+    def render_form(self, entity, peid, rtype, role, **kwargs):
+        """fetch and render the form"""
+        rschema = self.schema.rschema(rtype)
+        divid = '%s-%s-%s' % (peid, rtype, entity.eid)
+        title = rschema.display_name(self.req, role)
+        form = self.vreg.select_object('forms', 'edition', self.req,
+                                       entity=entity)
+        removejs = self.removejs % (peid, rtype,entity.eid)
+        if self.keep_entity(entity, peid, rtype):
+            if entity.has_eid():
+                rval = entity.eid
+            else:
+                rval = INTERNAL_FIELD_VALUE
+            form.form_add_hidden('edit%s-%s:%s' % (role[0], rtype, peid), rval)
+        form.form_add_hidden(name='%s:%s' % (rtype, peid), value=entity.eid,
+                             id='rel-%s-%s-%s'  % (peid, rtype, entity.eid))
+        self.w(form.form_render(renderer=EntityInlinedFormRenderer(), divid=divid,
+                                title=title, removejs=removejs,**kwargs))
+
+    def keep_entity(self, entity, peid, rtype):
+        if not entity.has_eid():
+            return True
+        # are we regenerating form because of a validation error ?
+        erroneous_post = self.req.data.get('formvalues')
+        if erroneous_post:
+            cdvalues = self.req.list_form_param('%s:%s' % (rtype, peid),
+                                                erroneous_post)
+            if unicode(entity.eid) not in cdvalues:
+                return False
+        return True
+
+
+class InlineEntityCreationFormView(InlineEntityEditionFormView):
+    id = 'inline-creation'
+    __select__ = (match_kwargs('peid', 'rtype')
+                  & specified_etype_implements('Any'))
+    
+    def call(self, etype, peid, rtype, role='subject', **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
+        """
+        try:
+            entity = self.vreg.etype_class(etype)(self.req, None, None)
+        except:
+            self.w(self.req._('no such entity type %s') % etype)
+            return
+        self.initialize_varmaker()
+        entity.eid = self.varmaker.next()
+        self.render_form(entity, peid, rtype, role)