turn every form class into appobject. They should not be instantiated manually anymore.
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 29 May 2009 14:19:30 +0200
changeset 2005 e8032965f37a
parent 2004 ea9eab290dcd
child 2006 78d5b57d4964
child 2045 bf0643d4ef36
turn every form class into appobject. They should not be instantiated manually anymore.
common/mixins.py
entities/__init__.py
entity.py
web/form.py
web/test/unittest_form.py
web/test/unittest_formfields.py
web/views/autoform.py
web/views/cwproperties.py
web/views/editforms.py
web/views/formrenderers.py
web/views/forms.py
web/views/management.py
web/views/massmailing.py
web/views/workflow.py
--- a/common/mixins.py	Fri May 29 14:07:42 2009 +0200
+++ b/common/mixins.py	Fri May 29 14:19:30 2009 +0200
@@ -237,8 +237,8 @@
 
     @obsolete('use EntityFieldsForm.subject_in_state_vocabulary')
     def subject_in_state_vocabulary(self, rschema, limit=None):
-        from cubicweb.web.form import EntityFieldsForm
-        return EntityFieldsForm(self.req, None, entity=self).subject_in_state_vocabulary(rschema, limit)
+        form = self.vreg.select_object('forms', 'edition', self.req, entity=self)
+        return form.subject_in_state_vocabulary(rschema, limit)
 
 
 
--- a/entities/__init__.py	Fri May 29 14:07:42 2009 +0200
+++ b/entities/__init__.py	Fri May 29 14:19:30 2009 +0200
@@ -242,13 +242,13 @@
 
     @obsolete('use EntityFieldsForm.subject_relation_vocabulary')
     def subject_relation_vocabulary(self, rtype, limit):
-        from cubicweb.web.form import EntityFieldsForm
-        return EntityFieldsForm(self.req, None, entity=self).subject_relation_vocabulary(rtype, limit)
+        form = self.vreg.select_object('forms', 'edition', self.req, entity=self)
+        return form.subject_relation_vocabulary(rtype, limit)
 
     @obsolete('use EntityFieldsForm.object_relation_vocabulary')
     def object_relation_vocabulary(self, rtype, limit):
-        from cubicweb.web.form import EntityFieldsForm
-        return EntityFieldsForm(self.req, None, entity=self).object_relation_vocabulary(rtype, limit)
+        form = self.vreg.select_object('forms', 'edition', self.req, entity=self)
+        return form.object_relation_vocabulary(rtype, limit)
 
     @obsolete('use AutomaticEntityForm.[e]relations_by_category')
     def relations_by_category(self, categories=None, permission=None):
--- a/entity.py	Fri May 29 14:07:42 2009 +0200
+++ b/entity.py	Fri May 29 14:19:30 2009 +0200
@@ -724,9 +724,8 @@
         If `eid` is None in one of these couples, it should be
         interpreted as a separator in case vocabulary results are grouped
         """
-        from cubicweb.web.form import EntityFieldsForm
         from logilab.common.testlib import mock_object
-        form = EntityFieldsForm(self.req, entity=self)
+        form = self.vreg.select_object('forms', 'edition', self.req, entity=self)
         field = mock_object(name=rtype, role=role)
         return form.form_field_vocabulary(field, limit)
 
--- a/web/form.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/form.py	Fri May 29 14:19:30 2009 +0200
@@ -7,20 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-from warnings import warn
-
-from logilab.common.compat import any
-from logilab.common.decorators import iclassmethod
-
 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 import formwidgets as fwdgs, httpcache
-from cubicweb.web.controller import NAV_FORM_PARAMETERS
-from cubicweb.web.formfields import (Field, StringField, RelationField,
-                                     HiddenInitialValueField)
+from cubicweb.web import stdmsgs, httpcache, formfields
 
 
 class FormViewMixIn(object):
@@ -198,7 +188,7 @@
             if hasattr(base, '_fields_'):
                 allfields += base._fields_
         clsfields = (item for item in classdict.items()
-                     if isinstance(item[1], Field))
+                     if isinstance(item[1], formfields.Field))
         for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
             if not field.name:
                 field.set_name(fieldname)
@@ -212,509 +202,6 @@
     found
     """
 
-class FieldsForm(FormMixIn, AppRsetObject):
+class Form(FormMixIn, AppRsetObject):
     __metaclass__ = metafieldsform
     __registry__ = 'forms'
-    __select__ = yes()
-
-    is_subform = False
-
-    # attributes overrideable through __init__
-    internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
-    needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
-    needs_css = ('cubicweb.form.css',)
-    domid = 'form'
-    title = None
-    action = None
-    onsubmit = "return freezeFormButtons('%(domid)s');"
-    cssclass = None
-    cssstyle = None
-    cwtarget = None
-    redirect_path = None
-    set_error_url = True
-    copy_nav_params = False
-    form_buttons = None # form buttons (button widgets instances)
-    form_renderer_id = 'default'
-
-    def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
-                 **kwargs):
-        super(FieldsForm, self).__init__(req, rset, row=row, col=col)
-        self.fields = list(self.__class__._fields_)
-        for key, val in kwargs.items():
-            if key in NAV_FORM_PARAMETERS:
-                self.form_add_hidden(key, val)
-            else:
-                assert hasattr(self.__class__, key) and not key[0] == '_', key
-                setattr(self, key, val)
-        if self.set_error_url:
-            self.form_add_hidden('__errorurl', self.session_key())
-        if self.copy_nav_params:
-            for param in NAV_FORM_PARAMETERS:
-                if not param in kwargs:
-                    value = req.form.get(param)
-                    if value:
-                        self.form_add_hidden(param, value)
-        if submitmsg is not None:
-            self.form_add_hidden('__message', submitmsg)
-        self.context = None
-        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 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"""
-        return any(field.needs_multipart for field in self.fields)
-
-    def form_add_hidden(self, name, value=None, **kwargs):
-        """add an hidden field to the form"""
-        field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
-                            **kwargs)
-        if 'id' in kwargs:
-            # by default, hidden input don't set id attribute. If one is
-            # explicitly specified, ensure it will be set
-            field.widget.setdomid = True
-        self.append_field(field)
-        return field
-
-    def add_media(self):
-        """adds media (CSS & JS) required by this widget"""
-        if self.needs_js:
-            self.req.add_js(self.needs_js)
-        if self.needs_css:
-            self.req.add_css(self.needs_css)
-
-    def form_render(self, **values):
-        """render this form, using the renderer given in args or the default
-        FormRenderer()
-        """
-        renderer = values.pop('renderer', None)
-        if renderer is None:
-            renderer = self.form_default_renderer()
-        return renderer.render(self, values)
-
-    def form_default_renderer(self):
-        return self.vreg.select_object('formrenderers', self.form_renderer_id,
-                                       self.req, self.rset,
-                                       row=self.row, col=self.col)
-
-    def form_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
-        a string).
-
-        rendervalues is an optional dictionary containing extra kwargs given to
-        form_render()
-        """
-        self.context = context = {}
-        # ensure rendervalues is a dict
-        if rendervalues is None:
-            rendervalues = {}
-        # use a copy in case fields are modified while context is build (eg
-        # __linkto handling for instance)
-        for field in self.fields[:]:
-            for field in field.actual_fields(self):
-                field.form_init(self)
-                value = self.form_field_display_value(field, rendervalues)
-                context[field] = {'value': value,
-                                  'name': self.form_field_name(field),
-                                  'id': self.form_field_id(field),
-                                  }
-
-    def form_field_display_value(self, field, rendervalues, load_bytes=False):
-        """return field's *string* value to use for display
-
-        looks in
-        1. previously submitted form values if any (eg on validation error)
-        2. req.form
-        3. extra kw args given to render_form
-        4. field's typed value
-
-        values found in 1. and 2. are expected te be already some 'display'
-        value while those found in 3. and 4. are expected to be correctly typed.
-        """
-        value = self._req_display_value(field)
-        if value is None:
-            if field.name in rendervalues:
-                value = rendervalues[field.name]
-            else:
-                value = self.form_field_value(field, load_bytes)
-                if callable(value):
-                    value = value(self)
-            if value != INTERNAL_FIELD_VALUE:
-                value = field.format_value(self.req, value)
-        return value
-
-    def _req_display_value(self, field):
-        qname = self.form_field_name(field)
-        if qname in self.form_previous_values:
-            return self.form_previous_values[qname]
-        if qname in self.req.form:
-            return self.req.form[qname]
-        if field.name in self.req.form:
-            return self.req.form[field.name]
-        return None
-
-    def form_field_value(self, field, load_bytes=False):
-        """return field's *typed* value"""
-        myattr = '%s_%s_default' % (field.role, field.name)
-        if hasattr(self, myattr):
-            return getattr(self, myattr)()
-        value = field.initial
-        if callable(value):
-            value = value(self)
-        return value
-
-    def form_field_error(self, field):
-        """return validation error for widget's field, if any"""
-        if self._field_has_error(field):
-            self.form_displayed_errors.add(field.name)
-            return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
-        return u''
-
-    def form_field_format(self, field):
-        """return MIME type used for the given (text or bytes) field"""
-        return self.req.property_value('ui.default-text-format')
-
-    def form_field_encoding(self, field):
-        """return encoding used for the given (text) field"""
-        return self.req.encoding
-
-    def form_field_name(self, field):
-        """return qualified name for the given field"""
-        return field.name
-
-    def form_field_id(self, field):
-        """return dom id for the given field"""
-        return field.id
-
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field. Should be overriden in
-        specific forms using fields which requires some vocabulary
-        """
-        raise NotImplementedError
-
-    def _field_has_error(self, field):
-        """return true if the field has some error in given validation exception
-        """
-        return self.form_valerror and field.name in self.form_valerror.errors
-
-
-class EntityFieldsForm(FieldsForm):
-    __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
-
-    internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
-    domid = 'entityForm'
-
-    def __init__(self, *args, **kwargs):
-        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 or 0, self.col or 0)
-        self.form_add_hidden('__type', eidparam=True)
-        self.form_add_hidden('eid')
-        if msg:
-            # If we need to directly attach the new object to another one
-            self.form_add_hidden('__message', msg)
-        if not self.is_subform:
-            for linkto in self.req.list_form_param('__linkto'):
-                self.form_add_hidden('__linkto', linkto)
-                msg = '%s %s' % (msg, self.req._('and linked'))
-        # in case of direct instanciation
-        self.schema = self.edited_entity.schema
-        self.vreg = self.edited_entity.vreg
-
-    def _field_has_error(self, field):
-        """return true if the field has some error in given validation exception
-        """
-        return super(EntityFieldsForm, self)._field_has_error(field) \
-               and self.form_valerror.eid == self.edited_entity.eid
-
-    def _relation_vocabulary(self, rtype, targettype, role,
-                            limit=None, done=None):
-        """return unrelated entities for a given relation and target entity type
-        for use in vocabulary
-        """
-        if done is None:
-            done = set()
-        rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
-        res = []
-        for entity in rset.entities():
-            if entity.eid in done:
-                continue
-            done.add(entity.eid)
-            res.append((entity.view('combobox'), entity.eid))
-        return res
-
-    def _req_display_value(self, field):
-        value = super(EntityFieldsForm, self)._req_display_value(field)
-        if value is None:
-            value = self.edited_entity.linked_to(field.name, field.role)
-            if value:
-                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'):
-                    if field.initial in searchedvalues:
-                        self.remove_field(field)
-            else:
-                value = None
-        return value
-
-    def _form_field_default_value(self, field, load_bytes):
-        defaultattr = 'default_%s' % field.name
-        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.edited_entity.id), DeprecationWarning)
-            value = getattr(self.edited_entity, defaultattr)
-            if callable(value):
-                value = value()
-        else:
-            value = super(EntityFieldsForm, self).form_field_value(field,
-                                                                   load_bytes)
-        return value
-
-    def form_default_renderer(self):
-        return self.vreg.select_object('formrenderers', self.form_renderer_id,
-                                       self.req, self.rset,
-                                       row=self.row, col=self.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
-
-        edit[s|o] hidden fields are used to indicate the value for the
-        associated field before the (potential) modification made when
-        submitting the form.
-        """
-        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))):
-                    field.eidparam = True
-                    self.fields.append(HiddenInitialValueField(field))
-        return super(EntityFieldsForm, self).form_build_context(values)
-
-    def form_field_value(self, field, load_bytes=False):
-        """return field's *typed* value
-
-        overriden to deal with
-        * special eid / __type / edits- / edito- fields
-        * lookup for values on edited entities
-        """
-        attr = field.name
-        entity = self.edited_entity
-        if attr == 'eid':
-            return entity.eid
-        if not field.eidparam:
-            return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
-        if attr.startswith('edits-') or attr.startswith('edito-'):
-            # edit[s|o]- fieds must have the actual value stored on the entity
-            assert hasattr(field, 'visible_field')
-            vfield = field.visible_field
-            assert vfield.eidparam
-            if entity.has_eid():
-                return self.form_field_value(vfield)
-            return INTERNAL_FIELD_VALUE
-        if attr == '__type':
-            return entity.id
-        if self.schema.rschema(attr).is_final():
-            attrtype = entity.e_schema.destination(attr)
-            if attrtype == 'Password':
-                return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
-            if attrtype == 'Bytes':
-                if entity.has_eid():
-                    if load_bytes:
-                        return getattr(entity, attr)
-                    # XXX value should reflect if some file is already attached
-                    return True
-                return False
-            if entity.has_eid() or attr in entity:
-                value = getattr(entity, attr)
-            else:
-                value = self._form_field_default_value(field, load_bytes)
-            return value
-        # non final relation field
-        if entity.has_eid() or entity.relation_cached(attr, field.role):
-            value = [r[0] for r in entity.related(attr, field.role)]
-        else:
-            value = self._form_field_default_value(field, load_bytes)
-        return value
-
-    def form_field_format(self, field):
-        """return MIME type used for the given (text or bytes) field"""
-        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.edited_entity.attr_metadata(field.name, 'format')
-        return self.req.property_value('ui.default-text-format')
-
-    def form_field_encoding(self, field):
-        """return encoding used for the given (text) field"""
-        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.edited_entity.attr_metadata(field.name, 'encoding')
-        return super(EntityFieldsForm, self).form_field_encoding(field)
-
-    def form_field_name(self, field):
-        """return qualified name for the given field"""
-        if field.eidparam:
-            return eid_param(field.name, self.edited_entity.eid)
-        return field.name
-
-    def form_field_id(self, field):
-        """return dom id for the given field"""
-        if field.eidparam:
-            return eid_param(field.id, self.edited_entity.eid)
-        return field.id
-
-    def form_field_vocabulary(self, field, limit=None):
-        """return vocabulary for the given field"""
-        role, rtype = field.role, field.name
-        method = '%s_%s_vocabulary' % (role, rtype)
-        try:
-            vocabfunc = getattr(self, method)
-        except AttributeError:
-            try:
-                # XXX bw compat, <role>_<rtype>_vocabulary on the entity
-                vocabfunc = getattr(self.edited_entity, method)
-            except AttributeError:
-                vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
-            else:
-                warn('found %s on %s, should be set on a specific form'
-                     % (method, 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
-        #       couples (label, None) which act as separators. In these
-        #       cases, it doesn't make sense to sort results afterwards.
-        return vocabfunc(rtype, limit)
-
-    def subject_relation_vocabulary(self, rtype, limit=None):
-        """defaut vocabulary method for the given relation, looking for
-        relation's object entities (i.e. self is the subject)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = entity.schema.rschema(rtype)
-        done = None
-        assert not rtype.is_final(), rtype
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, str(rtype)))
-        result = []
-        rsetsize = None
-        for objtype in rtype.objects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, objtype, 'subject',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def object_relation_vocabulary(self, rtype, limit=None):
-        """defaut vocabulary method for the given relation, looking for
-        relation's subject entities (i.e. self is the object)
-        """
-        entity = self.edited_entity
-        if isinstance(rtype, basestring):
-            rtype = entity.schema.rschema(rtype)
-        done = None
-        if entity.has_eid():
-            done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
-        result = []
-        rsetsize = None
-        for subjtype in rtype.subjects(entity.e_schema):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relation_vocabulary(rtype, subjtype, 'object',
-                                                rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def subject_in_state_vocabulary(self, rtype, limit=None):
-        """vocabulary method for the in_state relation, looking for relation's
-        object entities (i.e. self is the subject) according to initial_state,
-        state_of and next_state relation
-        """
-        entity = self.edited_entity
-        if not entity.has_eid() or not entity.in_state:
-            # get the initial state
-            rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
-            rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
-            if rset:
-                return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
-            return []
-        results = []
-        for tr in entity.in_state[0].transitions(entity):
-            state = tr.destination_state[0]
-            results.append((state.view('combobox'), state.eid))
-        return sorted(results)
-
-
-class CompositeForm(FieldsForm):
-    """form composed for sub-forms"""
-    form_renderer_id = 'composite'
-
-    def __init__(self, *args, **kwargs):
-        super(CompositeForm, 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)
--- a/web/test/unittest_form.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/test/unittest_form.py	Fri May 29 14:19:30 2009 +0200
@@ -5,14 +5,16 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+
 from logilab.common.testlib import unittest_main, mock_object
+
 from cubicweb import Binary
 from cubicweb.devtools.testlib import WebTest
-from cubicweb.web.form import EntityFieldsForm, FieldsForm
 from cubicweb.web.formfields import (IntField, StringField, RichTextField,
                                      DateTimeField, DateTimePicker,
                                      FileField, EditableFileField)
 from cubicweb.web.formwidgets import PasswordInput
+from cubicweb.web.views.forms import EntityFieldsForm, FieldsForm
 from cubicweb.web.views.workflow import ChangeStateForm
 from cubicweb.web.views.formrenderers import FormRenderer
 
--- a/web/test/unittest_formfields.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/test/unittest_formfields.py	Fri May 29 14:19:30 2009 +0200
@@ -12,9 +12,9 @@
 
 from cubicweb.devtools import TestServerConfiguration
 from cubicweb.devtools.testlib import EnvBasedTC
-from cubicweb.web.form import EntityFieldsForm
 from cubicweb.web.formwidgets import PasswordInput, TextArea, Select
 from cubicweb.web.formfields import *
+from cubicweb.web.views.forms import EntityFieldsForm
 
 from cubes.file.entities import File
 
--- a/web/views/autoform.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/autoform.py	Fri May 29 14:19:30 2009 +0200
@@ -12,13 +12,13 @@
 
 from cubicweb import typed_eid
 from cubicweb.web import stdmsgs, uicfg
-from cubicweb.web.form import FieldNotFound, EntityFieldsForm
+from cubicweb.web.form import FieldNotFound
 from cubicweb.web.formfields import guess_field
 from cubicweb.web.formwidgets import Button, SubmitButton
-from cubicweb.web.views.editforms import toggleable_relation_link, relation_id
+from cubicweb.web.views import forms, editforms
 
 
-class AutomaticEntityForm(EntityFieldsForm):
+class AutomaticEntityForm(forms.EntityFieldsForm):
     """base automatic form to edit any entity.
 
     Designed to be fully generated from schema but highly configurable through:
@@ -235,13 +235,13 @@
         for label, rschema, role in self.srelations_by_category('generic', 'add'):
             relatedrset = entity.related(rschema, role, limit=self.related_limit)
             if rschema.has_perm(self.req, 'delete'):
-                toggleable_rel_link_func = toggleable_relation_link
+                toggleable_rel_link_func = editforms.toggleable_relation_link
             else:
                 toggleable_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])
+                nodeid = editforms.relation_id(entity.eid, rschema, role,
+                                               relatedrset[row][0])
                 if nodeid in pending_deletes:
                     status = u'pendingDelete'
                     label = '+'
--- a/web/views/cwproperties.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/cwproperties.py	Fri May 29 14:19:30 2009 +0200
@@ -17,7 +17,7 @@
                                 match_user_groups)
 from cubicweb.view import StartupView
 from cubicweb.web import uicfg, stdmsgs
-from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormViewMixIn
+from cubicweb.web.form import FormViewMixIn
 from cubicweb.web.formfields import FIELDS, StringField
 from cubicweb.web.formwidgets import Select, Button, SubmitButton
 from cubicweb.web.views import primary, formrenderers
@@ -189,10 +189,11 @@
 
     def form(self, formid, keys, splitlabel=False):
         buttons = [SubmitButton()]
-        form = CompositeForm(self.req, domid=formid, action=self.build_url(),
-                             form_buttons=buttons,
-                             onsubmit="return validatePrefsForm('%s')" % formid,
-                             submitmsg=self.req._('changes applied'))
+        form = self.vreg.select_object('forms', 'composite', self.req,
+                                  domid=formid, action=self.build_url(),
+                                  form_buttons=buttons,
+                                  onsubmit="return validatePrefsForm('%s')" % formid,
+                                  submitmsg=self.req._('changes applied'))
         path = self.req.relative_path()
         if '?' in path:
             path, params = path.split('?', 1)
@@ -200,7 +201,8 @@
         form.form_add_hidden('__redirectpath', path)
         for key in keys:
             self.form_row(form, key, splitlabel)
-        renderer = CWPropertiesFormRenderer(self.req, display_progress_div=False)
+        renderer = self.vreg.select_object('formrenderers', 'cwproperties', self.req,
+                                           display_progress_div=False)
         return form.form_render(renderer=renderer)
 
     def form_row(self, form, key, splitlabel):
@@ -209,8 +211,8 @@
             label = key.split('.')[-1]
         else:
             label = key
-        subform = EntityFieldsForm(self.req, entity=entity, set_error_url=False)
-
+        subform = self.vreg.select_object('forms', 'base', self.req, entity=entity,
+                                     set_error_url=False)
         subform.append_field(PropertyValueField(name='value', label=label,
                                                 eidparam=True))
         subform.vreg = self.vreg
--- a/web/views/editforms.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/editforms.py	Fri May 29 14:19:30 2009 +0200
@@ -21,10 +21,10 @@
 from cubicweb.view import EntityView
 from cubicweb.common import tags
 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, eid_param
-from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormViewMixIn
+from cubicweb.web.form import FormViewMixIn
 from cubicweb.web.formfields import RelationField
 from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton, Select
-from cubicweb.web.views.formrenderers import FormRenderer
+from cubicweb.web.views import forms
 
 
 def relation_id(eid, rtype, role, reid):
@@ -59,17 +59,19 @@
           % _('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 = CompositeForm(req, domid='deleteconf', copy_nav_params=True,
-                             action=self.build_url('edit'), onsubmit=onsubmit,
-                             form_buttons=[Button(stdmsgs.YES, cwaction='delete'),
-                                           Button(stdmsgs.NO, cwaction='cancel')])
+        form = self.vreg.select_object('forms', 'composite', req, domid='deleteconf',
+                                       copy_nav_params=True,
+                                       action=self.build_url('edit'), onsubmit=onsubmit,
+                                       form_buttons=[Button(stdmsgs.YES, cwaction='delete'),
+                                                     Button(stdmsgs.NO, cwaction='cancel')])
         done = set()
         w(u'<ul>\n')
         for entity in self.rset.entities():
             if entity.eid in done:
                 continue
             done.add(entity.eid)
-            subform = EntityFieldsForm(req, entity=entity, set_error_url=False)
+            subform = self.vreg.select_object('forms', 'base', 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'),
@@ -118,10 +120,12 @@
             form = self._build_relation_form(entity, value, rtype, role,
                                              row, col, vid, default)
         form.form_add_hidden(u'__maineid', entity.eid)
-        renderer = FormRenderer(self.req, display_label=False, display_help=False,
-                                display_fields=[(rtype, role)],
-                                table_class='', button_bar_class='buttonbar',
-                                display_progress_div=False)
+        renderer = self.vreg.select_object('formrenderers', 'base', self.req,
+                                      entity=entity,
+                                      display_label=False, display_help=False,
+                                      display_fields=[(rtype, role)],
+                                      table_class='', button_bar_class='buttonbar',
+                                      display_progress_div=False)
         self.w(form.form_render(renderer=renderer))
 
     def _build_relation_form(self, entity, value, rtype, role, row, col, vid, default):
@@ -129,16 +133,17 @@
         divid = 'd%s' % make_uid('%s-%s' % (rtype, entity.eid))
         event_data = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'vid' : vid,
                       'default' : default, 'role' : role}
-        form = EntityFieldsForm(self.req, entity=entity, action='#',
-                                domid='%s-form' % divid,
-                                cssstyle='display: none',
-                                onsubmit=("return inlineValidateRelationForm('%(divid)s-form', '%(rtype)s', "
-                                          "'%(role)s', '%(eid)s', '%(divid)s', '%(vid)s', '%(default)s');" %
-                                          event_data),
-                                form_buttons=[SubmitButton(),
-                                              Button(stdmsgs.BUTTON_CANCEL,
-                                                     onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" %\
-                                                         (entity.eid, rtype, divid))])
+        onsubmit = ("return inlineValidateRelationForm('%(divid)s-form', '%(rtype)s', "
+                    "'%(role)s', '%(eid)s', '%(divid)s', '%(vid)s', '%(default)s');"
+                    % event_data)
+        cancelclick = "cancelInlineEdit(%s,\'%s\',\'%s\')" % (
+            entity.eid, rtype, divid)
+        form = self.vreg.select_object('forms', 'base', self.req, entity=entity,
+                                       domid='%s-form' % divid, cssstyle='display: none',
+                                       onsubmit=onsubmit, action='#',
+                                       form_buttons=[SubmitButton(),
+                                                     Button(stdmsgs.BUTTON_CANCEL,
+                                                       onclick=cancelclick)])
         form.append_field(RelationField(name=rtype, role=role, sort=True,
                                         widget=Select(),
                                         label=u' '))
@@ -289,7 +294,7 @@
         return self.req._('entity copied')
 
 
-class TableEditForm(CompositeForm):
+class TableEditForm(forms.CompositeForm):
     id = 'muledit'
     domid = 'entityForm'
     onsubmit = "return validateForm('%s', null);" % domid
--- a/web/views/formrenderers.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/formrenderers.py	Fri May 29 14:19:30 2009 +0200
@@ -210,6 +210,13 @@
         w(u'</tr></table>')
 
 
+class BaseFormRenderer(FormRenderer):
+    """use form_renderer_id = 'base' if you want base FormRenderer without
+    adaptation by selection
+    """
+    id = 'base'
+
+
 class HTableFormRenderer(FormRenderer):
     """display fields horizontally in a table
 
@@ -298,10 +305,6 @@
                     w(u'<th>%s</th>' % form.req._(field.label))
         w(u'</tr>')
 
-class BaseFormRenderer(FormRenderer):
-    """use form_renderer_id = 'base' if you don't want adaptation by selection
-    """
-    id = 'base'
 
 class EntityFormRenderer(FormRenderer):
     """specific renderer for entity edition form (edition)"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/views/forms.py	Fri May 29 14:19:30 2009 +0200
@@ -0,0 +1,524 @@
+"""some base form classes for CubicWeb web client
+
+:organization: Logilab
+:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+__docformat__ = "restructuredtext en"
+
+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
+from cubicweb.web import form, formwidgets as fwdgs
+from cubicweb.web.controller import NAV_FORM_PARAMETERS
+from cubicweb.web.formfields import HiddenInitialValueField, StringField
+
+
+class FieldsForm(form.Form):
+    id = 'base'
+
+    is_subform = False
+
+    # attributes overrideable through __init__
+    internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
+    needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',)
+    needs_css = ('cubicweb.form.css',)
+    domid = 'form'
+    title = None
+    action = None
+    onsubmit = "return freezeFormButtons('%(domid)s');"
+    cssclass = None
+    cssstyle = None
+    cwtarget = None
+    redirect_path = None
+    set_error_url = True
+    copy_nav_params = False
+    form_buttons = None # form buttons (button widgets instances)
+    form_renderer_id = 'default'
+
+    def __init__(self, req, rset=None, row=None, col=None, submitmsg=None,
+                 **kwargs):
+        super(FieldsForm, self).__init__(req, rset, row=row, col=col)
+        self.fields = list(self.__class__._fields_)
+        for key, val in kwargs.items():
+            if key in NAV_FORM_PARAMETERS:
+                self.form_add_hidden(key, val)
+            else:
+                assert hasattr(self.__class__, key) and not key[0] == '_', key
+                setattr(self, key, val)
+        if self.set_error_url:
+            self.form_add_hidden('__errorurl', self.session_key())
+        if self.copy_nav_params:
+            for param in NAV_FORM_PARAMETERS:
+                if not param in kwargs:
+                    value = req.form.get(param)
+                    if value:
+                        self.form_add_hidden(param, value)
+        if submitmsg is not None:
+            self.form_add_hidden('__message', submitmsg)
+        self.context = None
+        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"""
+        return any(field.needs_multipart for field in self.fields)
+
+    def form_add_hidden(self, name, value=None, **kwargs):
+        """add an hidden field to the form"""
+        field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
+                            **kwargs)
+        if 'id' in kwargs:
+            # by default, hidden input don't set id attribute. If one is
+            # explicitly specified, ensure it will be set
+            field.widget.setdomid = True
+        self.append_field(field)
+        return field
+
+    def add_media(self):
+        """adds media (CSS & JS) required by this widget"""
+        if self.needs_js:
+            self.req.add_js(self.needs_js)
+        if self.needs_css:
+            self.req.add_css(self.needs_css)
+
+    def form_render(self, **values):
+        """render this form, using the renderer given in args or the default
+        FormRenderer()
+        """
+        renderer = values.pop('renderer', None)
+        if renderer is None:
+            renderer = self.form_default_renderer()
+        return renderer.render(self, values)
+
+    def form_default_renderer(self):
+        return self.vreg.select_object('formrenderers', self.form_renderer_id,
+                                       self.req, self.rset,
+                                       row=self.row, col=self.col)
+
+    def form_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
+        a string).
+
+        rendervalues is an optional dictionary containing extra kwargs given to
+        form_render()
+        """
+        self.context = context = {}
+        # ensure rendervalues is a dict
+        if rendervalues is None:
+            rendervalues = {}
+        # use a copy in case fields are modified while context is build (eg
+        # __linkto handling for instance)
+        for field in self.fields[:]:
+            for field in field.actual_fields(self):
+                field.form_init(self)
+                value = self.form_field_display_value(field, rendervalues)
+                context[field] = {'value': value,
+                                  'name': self.form_field_name(field),
+                                  'id': self.form_field_id(field),
+                                  }
+
+    def form_field_display_value(self, field, rendervalues, load_bytes=False):
+        """return field's *string* value to use for display
+
+        looks in
+        1. previously submitted form values if any (eg on validation error)
+        2. req.form
+        3. extra kw args given to render_form
+        4. field's typed value
+
+        values found in 1. and 2. are expected te be already some 'display'
+        value while those found in 3. and 4. are expected to be correctly typed.
+        """
+        value = self._req_display_value(field)
+        if value is None:
+            if field.name in rendervalues:
+                value = rendervalues[field.name]
+            else:
+                value = self.form_field_value(field, load_bytes)
+                if callable(value):
+                    value = value(self)
+            if value != INTERNAL_FIELD_VALUE:
+                value = field.format_value(self.req, value)
+        return value
+
+    def _req_display_value(self, field):
+        qname = self.form_field_name(field)
+        if qname in self.form_previous_values:
+            return self.form_previous_values[qname]
+        if qname in self.req.form:
+            return self.req.form[qname]
+        if field.name in self.req.form:
+            return self.req.form[field.name]
+        return None
+
+    def form_field_value(self, field, load_bytes=False):
+        """return field's *typed* value"""
+        myattr = '%s_%s_default' % (field.role, field.name)
+        if hasattr(self, myattr):
+            return getattr(self, myattr)()
+        value = field.initial
+        if callable(value):
+            value = value(self)
+        return value
+
+    def form_field_error(self, field):
+        """return validation error for widget's field, if any"""
+        if self._field_has_error(field):
+            self.form_displayed_errors.add(field.name)
+            return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name]
+        return u''
+
+    def form_field_format(self, field):
+        """return MIME type used for the given (text or bytes) field"""
+        return self.req.property_value('ui.default-text-format')
+
+    def form_field_encoding(self, field):
+        """return encoding used for the given (text) field"""
+        return self.req.encoding
+
+    def form_field_name(self, field):
+        """return qualified name for the given field"""
+        return field.name
+
+    def form_field_id(self, field):
+        """return dom id for the given field"""
+        return field.id
+
+    def form_field_vocabulary(self, field, limit=None):
+        """return vocabulary for the given field. Should be overriden in
+        specific forms using fields which requires some vocabulary
+        """
+        raise NotImplementedError
+
+    def _field_has_error(self, field):
+        """return true if the field has some error in given validation exception
+        """
+        return self.form_valerror and field.name in self.form_valerror.errors
+
+
+class EntityFieldsForm(FieldsForm):
+    id = 'base'
+    __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
+
+    internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid')
+    domid = 'entityForm'
+
+    def __init__(self, *args, **kwargs):
+        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 or 0, self.col or 0)
+        self.form_add_hidden('__type', eidparam=True)
+        self.form_add_hidden('eid')
+        if msg:
+            # If we need to directly attach the new object to another one
+            self.form_add_hidden('__message', msg)
+        if not self.is_subform:
+            for linkto in self.req.list_form_param('__linkto'):
+                self.form_add_hidden('__linkto', linkto)
+                msg = '%s %s' % (msg, self.req._('and linked'))
+
+    def _field_has_error(self, field):
+        """return true if the field has some error in given validation exception
+        """
+        return super(EntityFieldsForm, self)._field_has_error(field) \
+               and self.form_valerror.eid == self.edited_entity.eid
+
+    def _relation_vocabulary(self, rtype, targettype, role,
+                            limit=None, done=None):
+        """return unrelated entities for a given relation and target entity type
+        for use in vocabulary
+        """
+        if done is None:
+            done = set()
+        rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
+        res = []
+        for entity in rset.entities():
+            if entity.eid in done:
+                continue
+            done.add(entity.eid)
+            res.append((entity.view('combobox'), entity.eid))
+        return res
+
+    def _req_display_value(self, field):
+        value = super(EntityFieldsForm, self)._req_display_value(field)
+        if value is None:
+            value = self.edited_entity.linked_to(field.name, field.role)
+            if value:
+                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'):
+                    if field.initial in searchedvalues:
+                        self.remove_field(field)
+            else:
+                value = None
+        return value
+
+    def _form_field_default_value(self, field, load_bytes):
+        defaultattr = 'default_%s' % field.name
+        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.edited_entity.id), DeprecationWarning)
+            value = getattr(self.edited_entity, defaultattr)
+            if callable(value):
+                value = value()
+        else:
+            value = super(EntityFieldsForm, self).form_field_value(field,
+                                                                   load_bytes)
+        return value
+
+    def form_default_renderer(self):
+        return self.vreg.select_object('formrenderers', self.form_renderer_id,
+                                       self.req, self.rset,
+                                       row=self.row, col=self.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
+
+        edit[s|o] hidden fields are used to indicate the value for the
+        associated field before the (potential) modification made when
+        submitting the form.
+        """
+        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))):
+                    field.eidparam = True
+                    self.fields.append(HiddenInitialValueField(field))
+        return super(EntityFieldsForm, self).form_build_context(values)
+
+    def form_field_value(self, field, load_bytes=False):
+        """return field's *typed* value
+
+        overriden to deal with
+        * special eid / __type / edits- / edito- fields
+        * lookup for values on edited entities
+        """
+        attr = field.name
+        entity = self.edited_entity
+        if attr == 'eid':
+            return entity.eid
+        if not field.eidparam:
+            return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
+        if attr.startswith('edits-') or attr.startswith('edito-'):
+            # edit[s|o]- fieds must have the actual value stored on the entity
+            assert hasattr(field, 'visible_field')
+            vfield = field.visible_field
+            assert vfield.eidparam
+            if entity.has_eid():
+                return self.form_field_value(vfield)
+            return INTERNAL_FIELD_VALUE
+        if attr == '__type':
+            return entity.id
+        if self.schema.rschema(attr).is_final():
+            attrtype = entity.e_schema.destination(attr)
+            if attrtype == 'Password':
+                return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
+            if attrtype == 'Bytes':
+                if entity.has_eid():
+                    if load_bytes:
+                        return getattr(entity, attr)
+                    # XXX value should reflect if some file is already attached
+                    return True
+                return False
+            if entity.has_eid() or attr in entity:
+                value = getattr(entity, attr)
+            else:
+                value = self._form_field_default_value(field, load_bytes)
+            return value
+        # non final relation field
+        if entity.has_eid() or entity.relation_cached(attr, field.role):
+            value = [r[0] for r in entity.related(attr, field.role)]
+        else:
+            value = self._form_field_default_value(field, load_bytes)
+        return value
+
+    def form_field_format(self, field):
+        """return MIME type used for the given (text or bytes) field"""
+        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.edited_entity.attr_metadata(field.name, 'format')
+        return self.req.property_value('ui.default-text-format')
+
+    def form_field_encoding(self, field):
+        """return encoding used for the given (text) field"""
+        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.edited_entity.attr_metadata(field.name, 'encoding')
+        return super(EntityFieldsForm, self).form_field_encoding(field)
+
+    def form_field_name(self, field):
+        """return qualified name for the given field"""
+        if field.eidparam:
+            return eid_param(field.name, self.edited_entity.eid)
+        return field.name
+
+    def form_field_id(self, field):
+        """return dom id for the given field"""
+        if field.eidparam:
+            return eid_param(field.id, self.edited_entity.eid)
+        return field.id
+
+    def form_field_vocabulary(self, field, limit=None):
+        """return vocabulary for the given field"""
+        role, rtype = field.role, field.name
+        method = '%s_%s_vocabulary' % (role, rtype)
+        try:
+            vocabfunc = getattr(self, method)
+        except AttributeError:
+            try:
+                # XXX bw compat, <role>_<rtype>_vocabulary on the entity
+                vocabfunc = getattr(self.edited_entity, method)
+            except AttributeError:
+                vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
+            else:
+                warn('found %s on %s, should be set on a specific form'
+                     % (method, 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
+        #       couples (label, None) which act as separators. In these
+        #       cases, it doesn't make sense to sort results afterwards.
+        return vocabfunc(rtype, limit)
+
+    def subject_relation_vocabulary(self, rtype, limit=None):
+        """defaut vocabulary method for the given relation, looking for
+        relation's object entities (i.e. self is the subject)
+        """
+        entity = self.edited_entity
+        if isinstance(rtype, basestring):
+            rtype = entity.schema.rschema(rtype)
+        done = None
+        assert not rtype.is_final(), rtype
+        if entity.has_eid():
+            done = set(e.eid for e in getattr(entity, str(rtype)))
+        result = []
+        rsetsize = None
+        for objtype in rtype.objects(entity.e_schema):
+            if limit is not None:
+                rsetsize = limit - len(result)
+            result += self._relation_vocabulary(rtype, objtype, 'subject',
+                                                rsetsize, done)
+            if limit is not None and len(result) >= limit:
+                break
+        return result
+
+    def object_relation_vocabulary(self, rtype, limit=None):
+        """defaut vocabulary method for the given relation, looking for
+        relation's subject entities (i.e. self is the object)
+        """
+        entity = self.edited_entity
+        if isinstance(rtype, basestring):
+            rtype = entity.schema.rschema(rtype)
+        done = None
+        if entity.has_eid():
+            done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype))
+        result = []
+        rsetsize = None
+        for subjtype in rtype.subjects(entity.e_schema):
+            if limit is not None:
+                rsetsize = limit - len(result)
+            result += self._relation_vocabulary(rtype, subjtype, 'object',
+                                                rsetsize, done)
+            if limit is not None and len(result) >= limit:
+                break
+        return result
+
+    def subject_in_state_vocabulary(self, rtype, limit=None):
+        """vocabulary method for the in_state relation, looking for relation's
+        object entities (i.e. self is the subject) according to initial_state,
+        state_of and next_state relation
+        """
+        entity = self.edited_entity
+        if not entity.has_eid() or not entity.in_state:
+            # get the initial state
+            rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S'
+            rset = self.req.execute(rql, {'etype': str(entity.e_schema)})
+            if rset:
+                return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])]
+            return []
+        results = []
+        for tr in entity.in_state[0].transitions(entity):
+            state = tr.destination_state[0]
+            results.append((state.view('combobox'), state.eid))
+        return sorted(results)
+
+
+class CompositeForm(FieldsForm):
+    """form composed for sub-forms"""
+    id = 'composite'
+    form_renderer_id = id
+
+    def __init__(self, *args, **kwargs):
+        super(CompositeForm, 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)
--- a/web/views/management.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/management.py	Fri May 29 14:19:30 2009 +0200
@@ -15,13 +15,12 @@
 from cubicweb.view import AnyRsetView, StartupView, EntityView
 from cubicweb.common.uilib import html_traceback, rest_traceback
 from cubicweb.web import formwidgets
-from cubicweb.web.form import FieldsForm, EntityFieldsForm
 from cubicweb.web.formfields import guess_field
-from cubicweb.web.views.formrenderers import HTableFormRenderer
 
 SUBMIT_MSGID = _('Submit bug report')
 MAIL_SUBMIT_MSGID = _('Submit bug report by mail')
 
+
 class SecurityViewMixIn(object):
     """display security information for a given schema """
 
@@ -106,11 +105,13 @@
     def owned_by_edit_form(self, entity):
         self.w('<h3>%s</h3>' % self.req._('ownership'))
         msg = self.req._('ownerships have been changed')
-        form = EntityFieldsForm(self.req, None, entity=entity, submitmsg=msg,
-                                form_buttons=[formwidgets.SubmitButton()],
-                                domid='ownership%s' % entity.eid,
-                                __redirectvid='security',
-                                __redirectpath=entity.rest_path())
+        form = self.vreg.select_object('forms', 'base', self.req, entity=entity,
+                                       form_renderer_id='base',
+                                  submitmsg=msg,
+                                  form_buttons=[formwidgets.SubmitButton()],
+                                  domid='ownership%s' % entity.eid,
+                                  __redirectvid='security',
+                                  __redirectpath=entity.rest_path())
         field = guess_field(entity.e_schema, self.schema.rschema('owned_by'))
         form.append_field(field)
         self.w(form.form_render(display_progress_div=False))
@@ -163,11 +164,11 @@
         newperm = self.vreg.etype_class('CWPermission')(self.req, None)
         newperm.eid = self.req.varmaker.next()
         w(u'<p>%s</p>' % _('add a new permission'))
-        form = EntityFieldsForm(self.req, None, entity=newperm,
-                                form_buttons=[formwidgets.SubmitButton()],
-                                domid='reqperm%s' % entity.eid,
-                                __redirectvid='security',
-                                __redirectpath=entity.rest_path())
+        form = self.vreg.select_object('forms', 'base', self.req, entity=newperm,
+                                       form_buttons=[formwidgets.SubmitButton()],
+                                       domid='reqperm%s' % entity.eid,
+                                       __redirectvid='security',
+                                       __redirectpath=entity.rest_path())
         form.form_add_hidden('require_permission', entity.eid, role='object',
                              eidparam=True)
         permnames = getattr(entity, '__permissions__', None)
@@ -183,7 +184,8 @@
         form.append_field(field)
         field = guess_field(cwpermschema, self.schema.rschema('require_group'))
         form.append_field(field)
-        renderer = HTableFormRenderer(self.req, display_progress_div=False)
+        renderer = self.select_object('formrenderers', 'htable', self.req,
+                                      display_progress_div=False)
         self.w(form.form_render(renderer=renderer))
 
 
@@ -240,7 +242,7 @@
         submiturl = self.config['submit-url']
         submitmail = self.config['submit-mail']
         if submiturl or submitmail:
-            form = FieldsForm(self.req, set_error_url=False)
+            form = self.select_object('forms', 'base', self.req, set_error_url=False)
             binfo = text_error_description(ex, excinfo, req, eversion, cversions)
             form.form_add_hidden('description', binfo)
             form.form_add_hidden('__bugreporting', '1')
--- a/web/views/massmailing.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/massmailing.py	Fri May 29 14:19:30 2009 +0200
@@ -15,10 +15,10 @@
 from cubicweb.view import EntityView
 from cubicweb.web import stdmsgs
 from cubicweb.web.action import Action
-from cubicweb.web.form import FieldsForm, FormViewMixIn
+from cubicweb.web.form import FormViewMixIn
 from cubicweb.web.formfields import StringField
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
-from cubicweb.web.views import formrenderers
+from cubicweb.web.views import forms, formrenderers
 
 
 class SendEmailAction(Action):
@@ -37,12 +37,12 @@
                               **params)
 
 
-class MassMailingForm(FieldsForm):
+class MassMailingForm(forms.FieldsForm):
     id = 'massmailing'
 
     sender = StringField(widget=TextInput({'disabled': 'disabled'}), label=_('From:'))
     recipient = StringField(widget=CheckBox(), label=_('Recipients:'))
-    subject = StringField(label=_('Subject:'))
+    subject = StringField(label=_('Subject:'), max_length=256)
     mailbody = StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
                                              inputid='mailbody'))
 
--- a/web/views/workflow.py	Fri May 29 14:07:42 2009 +0200
+++ b/web/views/workflow.py	Fri May 29 14:19:30 2009 +0200
@@ -22,13 +22,12 @@
 from cubicweb.web.form import FormViewMixIn
 from cubicweb.web.formfields import StringField,  RichTextField
 from cubicweb.web.formwidgets import HiddenInput, SubmitButton, Button
-from cubicweb.web.views import TmpFileViewMixin
-from cubicweb.web.views.boxes import EditBox
+from cubicweb.web.views import TmpFileViewMixin, forms
 
 
 # IWorkflowable views #########################################################
 
-class ChangeStateForm(form.EntityFieldsForm):
+class ChangeStateForm(forms.EntityFieldsForm):
     id = 'changestate'
 
     form_renderer_id = 'base' # don't want EntityFormRenderer