[form, controller] closes #1787233: form should provide a method to process posted content
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 29 Jun 2011 18:57:23 +0200
changeset 7584 e1881933f366
parent 7583 6632c762cd63
child 7586 b3688b15d7f5
[form, controller] closes #1787233: form should provide a method to process posted content
web/form.py
web/test/unittest_application.py
web/test/unittest_form.py
web/test/unittest_reledit.py
web/test/unittest_views_basecontrollers.py
web/views/editcontroller.py
web/views/forms.py
--- a/web/form.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/form.py	Wed Jun 29 18:57:23 2011 +0200
@@ -82,6 +82,9 @@
     force_session_key = None
     domid = 'form'
     copy_nav_params = False
+    control_fields = set( ('__form_id', '__errorurl', '__domid',
+                           '__redirectpath', '_cwmsgid', '__message',
+                           ) )
 
     def __init__(self, req, rset=None, row=None, col=None,
                  submitmsg=None, mainform=True, **kwargs):
--- a/web/test/unittest_application.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/test/unittest_application.py	Wed Jun 29 18:57:23 2011 +0200
@@ -196,7 +196,7 @@
         eid = unicode(user.eid)
         req.form = {
             'eid':       eid,
-            '__type:'+eid:    'CWUser', '_cw_edited_fields:'+eid: 'login-subject',
+            '__type:'+eid:    'CWUser', '_cw_entity_fields:'+eid: 'login-subject',
             'login-subject:'+eid:     '', # ERROR: no login specified
              # just a sample, missing some necessary information for real life
             '__errorurl': 'view?vid=edition...'
@@ -221,11 +221,11 @@
         req = self.request()
         # set Y before X to ensure both entities are edited, not only X
         req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
-                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject',
+                    '__type:X': 'CWUser', '_cw_entity_fields:X': 'login-subject',
                     # missing required field
                     'login-subject:X': u'',
                     # but email address is set
-                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    '__type:Y': 'EmailAddress', '_cw_entity_fields:Y': 'address-subject',
                     'address-subject:Y': u'bougloup@logilab.fr',
                     'use_email-object:Y': 'X',
                     # necessary to get validation error handling
@@ -250,11 +250,11 @@
         req = self.request()
         # set Y before X to ensure both entities are edited, not only X
         req.form = {'eid': ['Y', 'X'], '__maineid': 'X',
-                    '__type:X': 'CWUser', '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    '__type:X': 'CWUser', '_cw_entity_fields:X': 'login-subject,upassword-subject',
                     # already existent user
                     'login-subject:X': u'admin',
                     'upassword-subject:X': u'admin', 'upassword-subject-confirm:X': u'admin',
-                    '__type:Y': 'EmailAddress', '_cw_edited_fields:Y': 'address-subject',
+                    '__type:Y': 'EmailAddress', '_cw_entity_fields:Y': 'address-subject',
                     'address-subject:Y': u'bougloup@logilab.fr',
                     'use_email-object:Y': 'X',
                     # necessary to get validation error handling
--- a/web/test/unittest_form.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/test/unittest_form.py	Wed Jun 29 18:57:23 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -21,7 +21,7 @@
 from logilab.common.testlib import unittest_main, mock_object
 from logilab.common.compat import any
 
-from cubicweb import Binary
+from cubicweb import Binary, ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.web.formfields import (IntField, StringField, RichTextField,
                                      PasswordField, DateTimeField,
@@ -42,6 +42,16 @@
         self.assertEqual(StringField().format(form), 'text/rest')
 
 
+    def test_process_posted(self):
+        class AForm(FieldsForm):
+            anint = IntField()
+            astring = StringField()
+        form = AForm(self.request(anint='1', astring='2', _cw_fields='anint,astring'))
+        self.assertEqual(form.process_posted(), {'anint': 1, 'astring': '2'})
+        form = AForm(self.request(anint='1a', astring='2b', _cw_fields='anint,astring'))
+        self.assertRaises(ValidationError, form.process_posted)
+
+
 class EntityFieldsFormTC(CubicWebTC):
 
     def setUp(self):
--- a/web/test/unittest_reledit.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/test/unittest_reledit.py	Wed Jun 29 18:57:23 2011 +0200
@@ -64,7 +64,7 @@
 <input name="__reledit|reload" type="hidden" value="false" />
 <input name="__reledit|role" type="hidden" value="subject" />
 <input name="__reledit|eid" type="hidden" value="%(eid)s" />
-<input name="_cw_edited_fields:%(eid)s" type="hidden" value="title-subject,__type" />
+<input name="_cw_entity_fields:%(eid)s" type="hidden" value="title-subject,__type" />
 <fieldset class="default">
 <table class="">
 <tr class="title_subject_row">
@@ -97,7 +97,7 @@
 <input name="__reledit|reload" type="hidden" value="false" />
 <input name="__reledit|role" type="hidden" value="subject" />
 <input name="__reledit|eid" type="hidden" value="%(eid)s" />
-<input name="_cw_edited_fields:A" type="hidden" value="title-subject,rss_url-subject,__type,description-subject" />
+<input name="_cw_entity_fields:A" type="hidden" value="title-subject,rss_url-subject,__type,description-subject" />
 <fieldset class="default">
 <table class="attributeForm">
 <tr class="title_subject_row">
@@ -141,7 +141,7 @@
 <input name="__reledit|reload" type="hidden" value="false" />
 <input name="__reledit|role" type="hidden" value="subject" />
 <input name="__reledit|eid" type="hidden" value="%(eid)s" />
-<input name="_cw_edited_fields:%(eid)s" type="hidden" value="manager-subject,__type" />
+<input name="_cw_entity_fields:%(eid)s" type="hidden" value="manager-subject,__type" />
 <fieldset class="default">
 <table class="">
 <tr class="manager_subject_row">
--- a/web/test/unittest_views_basecontrollers.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Wed Jun 29 18:57:23 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -33,7 +33,7 @@
 
 def req_form(user):
     return {'eid': [str(user.eid)],
-            '_cw_edited_fields:%s' % user.eid: '_cw_generic_field',
+            '_cw_entity_fields:%s' % user.eid: '_cw_generic_field',
             '__type:%s' % user.eid: user.__regid__
             }
 
@@ -59,7 +59,7 @@
         user = self.user()
         req = self.request()
         req.form = {'eid': 'X', '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject',
                     'login-subject:X': u'admin',
                     'upassword-subject:X': u'toto',
                     'upassword-subject-confirm:X': u'toto',
@@ -79,7 +79,7 @@
         eid = u(user.eid)
         req.form = {
             'eid': eid, '__type:'+eid: 'CWUser',
-            '_cw_edited_fields:'+eid: 'login-subject,firstname-subject,surname-subject,in_group-subject',
+            '_cw_entity_fields:'+eid: 'login-subject,firstname-subject,surname-subject,in_group-subject',
             'login-subject:'+eid:     u(user.login),
             'surname-subject:'+eid: u'Th\xe9nault',
             'firstname-subject:'+eid:   u'Sylvain',
@@ -100,7 +100,7 @@
         req.form = {
             'eid': eid, '__maineid' : eid,
             '__type:'+eid: 'CWUser',
-            '_cw_edited_fields:'+eid: 'upassword-subject',
+            '_cw_entity_fields:'+eid: 'upassword-subject',
             'upassword-subject:'+eid: 'tournicoton',
             'upassword-subject-confirm:'+eid: 'tournicoton',
             }
@@ -120,7 +120,7 @@
         req.form = {
             'eid':       eid,
             '__type:'+eid:    'CWUser',
-            '_cw_edited_fields:'+eid: 'login-subject,firstname-subject,surname-subject',
+            '_cw_entity_fields:'+eid: 'login-subject,firstname-subject,surname-subject',
             'login-subject:'+eid:     u(user.login),
             'firstname-subject:'+eid: u'Th\xe9nault',
             'surname-subject:'+eid:   u'Sylvain',
@@ -140,14 +140,14 @@
         req.form = {'eid': ['X', 'Y'], '__maineid' : 'X',
 
                     '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject,surname-subject,in_group-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject,surname-subject,in_group-subject',
                     'login-subject:X': u'adim',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     'surname-subject:X': u'Di Mascio',
                     'in_group-subject:X': u(gueid),
 
                     '__type:Y': 'EmailAddress',
-                    '_cw_edited_fields:Y': 'address-subject,use_email-object',
+                    '_cw_entity_fields:Y': 'address-subject,use_email-object',
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': 'X',
                     }
@@ -165,11 +165,11 @@
         req.form = {'eid': [peid, 'Y'], '__maineid': peid,
 
                     '__type:'+peid: u'CWUser',
-                    '_cw_edited_fields:'+peid: u'surname-subject',
+                    '_cw_entity_fields:'+peid: u'surname-subject',
                     'surname-subject:'+peid: u'Di Masci',
 
                     '__type:Y': u'EmailAddress',
-                    '_cw_edited_fields:Y': u'address-subject,use_email-object',
+                    '_cw_entity_fields:Y': u'address-subject,use_email-object',
                     'address-subject:Y': u'dima@logilab.fr',
                     'use_email-object:Y': peid,
                     }
@@ -185,11 +185,11 @@
         req.form = {'eid': [peid, emaileid],
 
                     '__type:'+peid: u'CWUser',
-                    '_cw_edited_fields:'+peid: u'surname-subject',
+                    '_cw_entity_fields:'+peid: u'surname-subject',
                     'surname-subject:'+peid: u'Di Masci',
 
                     '__type:'+emaileid: u'EmailAddress',
-                    '_cw_edited_fields:'+emaileid: u'address-subject,use_email-object',
+                    '_cw_entity_fields:'+emaileid: u'address-subject,use_email-object',
                     'address-subject:'+emaileid: u'adim@logilab.fr',
                     'use_email-object:'+emaileid: peid,
                     }
@@ -205,7 +205,7 @@
         req = self.request()
         req.form = {'eid': 'X',
                     '__cloned_eid:X': u(user.eid), '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject',
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto',
                     }
@@ -215,7 +215,7 @@
         req = self.request()
         req.form = {'__cloned_eid:X': u(user.eid),
                     'eid': 'X', '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject',
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto',
                     'upassword-subject-confirm:X': u'tutu',
@@ -232,7 +232,7 @@
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
-                    '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
+                    '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                     'amount-subject:X': u'-10',
                     'described_by_test-subject:X': u(feid),
                 }
@@ -242,7 +242,7 @@
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
-                    '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
+                    '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                     'amount-subject:X': u'110',
                     'described_by_test-subject:X': u(feid),
                     }
@@ -252,7 +252,7 @@
         req = self.request(rollbackfirst=True)
         req.form = {'eid': ['X'],
                     '__type:X': 'Salesterm',
-                    '_cw_edited_fields:X': 'amount-subject,described_by_test-subject',
+                    '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                     'amount-subject:X': u'10',
                     'described_by_test-subject:X': u(feid),
                     }
@@ -298,7 +298,7 @@
         req = self.request()
         req.form = {
             'eid': 'A', '__maineid' : 'A',
-            '__type:A': 'BlogEntry', '_cw_edited_fields:A': 'content-subject,title-subject',
+            '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'content-subject,title-subject',
             'content-subject:A': u'"13:03:43"',
             'title-subject:A': u'huuu',
             '__redirectrql': redirectrql,
@@ -321,7 +321,7 @@
         req = self.request()
         req.form = {
             'eid': 'A', '__maineid' : 'A',
-            '__type:A': 'BlogEntry', '_cw_edited_fields:A': 'content-subject,title-subject',
+            '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'content-subject,title-subject',
             'content-subject:A': u'"13:03:43"',
             'title-subject:A': u'huuu',
             '__redirectrql': redirectrql,
@@ -377,7 +377,7 @@
         req.form = {
             'eid':      cwetypeeid,
             '__type:'+cwetypeeid:  'CWEType',
-            '_cw_edited_fields:'+cwetypeeid: 'name-subject,final-subject,description-subject,read_permission-subject',
+            '_cw_entity_fields:'+cwetypeeid: 'name-subject,final-subject,description-subject,read_permission-subject',
             'name-subject:'+cwetypeeid:     u'CWEType',
             'final-subject:'+cwetypeeid:    '',
             'description-subject:'+cwetypeeid:     u'users group',
@@ -401,7 +401,7 @@
         req = self.request()
         req.form = {
             'eid': 'A', '__maineid' : 'A',
-            '__type:A': 'BlogEntry', '_cw_edited_fields:A': 'title-subject,content-subject',
+            '__type:A': 'BlogEntry', '_cw_entity_fields:A': 'title-subject,content-subject',
             'title-subject:A': u'"13:03:40"',
             'content-subject:A': u'"13:03:43"',}
         path, params = self.expect_redirect_publish(req, 'edit')
@@ -418,13 +418,13 @@
         req.form = {'eid': ['X', 'Y'],
 
                     '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject,in_group-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject,in_group-subject',
                     'login-subject:X': u'adim',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     'in_group-subject:X': `gueid`,
 
                     '__type:Y': 'EmailAddress',
-                    '_cw_edited_fields:Y': 'address-subject,alias-subject,use_email-object',
+                    '_cw_entity_fields:Y': 'address-subject,alias-subject,use_email-object',
                     'address-subject:Y': u'',
                     'alias-subject:Y': u'',
                     'use_email-object:Y': 'X',
@@ -438,7 +438,7 @@
         req = self.request()
         req.form = {'__maineid' : 'X', 'eid': 'X',
                     '__cloned_eid:X': user.eid, '__type:X': 'CWUser',
-                    '_cw_edited_fields:X': 'login-subject,upassword-subject',
+                    '_cw_entity_fields:X': 'login-subject,upassword-subject',
                     'login-subject:X': u'toto',
                     'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                     }
@@ -462,7 +462,7 @@
             req = self.request()
             req.form = {'eid': 'X',
                         '__cloned_eid:X': p.eid, '__type:X': 'CWUser',
-                        '_cw_edited_fields:X': 'login-subject,surname-subject',
+                        '_cw_entity_fields:X': 'login-subject,surname-subject',
                         'login-subject': u'dodo',
                         'surname-subject:X': u'Boom',
                         '__errorurl' : "whatever but required",
--- a/web/views/editcontroller.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/views/editcontroller.py	Wed Jun 29 18:57:23 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -23,8 +23,6 @@
 
 from rql.utils import rqlvar_maker
 
-from logilab.common.textutils import splitstrip
-
 from cubicweb import Binary, ValidationError, typed_eid
 from cubicweb.view import EntityAdapter, implements_adapter_compat
 from cubicweb.selectors import is_instance
@@ -190,23 +188,18 @@
             formid = 'edition'
         form = self._cw.vreg['forms'].select(formid, self._cw, entity=entity)
         eid = form.actual_eid(entity.eid)
-        form.formvalues = {} # init fields value cache
         try:
-            editedfields = formparams['_cw_edited_fields']
+            editedfields = formparams['_cw_entity_fields']
         except KeyError:
-            raise RequestError(self._cw._('no edited fields specified for entity %s' % entity.eid))
-        for editedfield in splitstrip(editedfields):
             try:
-                name, role = editedfield.split('-')
-            except:
-                name = editedfield
-                role = None
-            if form.field_by_name.im_func.func_code.co_argcount == 4: # XXX
-                field = form.field_by_name(name, role, eschema=entity.e_schema)
-            else:
-                field = form.field_by_name(name, role)
-            if field.has_been_modified(form):
-                self.handle_formfield(form, field, rqlquery)
+                editedfields = formparams['_cw_edited_fields']
+                warn('[3.13] _cw_edited_fields has been renamed _cw_entity_fields',
+                     DeprecationWarning)
+            except KeyError:
+                raise RequestError(self._cw._('no edited fields specified for entity %s' % entity.eid))
+        form.formvalues = {} # init fields value cache
+        for field in form.iter_modified_fields(editedfields, entity):
+            self.handle_formfield(form, field, rqlquery)
         if self.errors:
             errors = dict((f.role_name(), unicode(ex)) for f, ex in self.errors)
             raise ValidationError(valerror_eid(entity.eid), errors)
--- a/web/views/forms.py	Wed Jun 29 18:43:33 2011 +0200
+++ b/web/views/forms.py	Wed Jun 29 18:57:23 2011 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -45,14 +45,16 @@
 
 from warnings import warn
 
-from logilab.common import dictattr
+from logilab.common import dictattr, tempattr
 from logilab.common.decorators import iclassmethod
 from logilab.common.compat import any
+from logilab.common.textutils import splitstrip
 from logilab.common.deprecation import deprecated
 
-from cubicweb import typed_eid
+from cubicweb import ValidationError, typed_eid
 from cubicweb.utils import support_args
 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
+from cubicweb.web import RequestError, ProcessFormError
 from cubicweb.web import uicfg, form, formwidgets as fwdgs
 from cubicweb.web.formfields import relvoc_unrelated, guess_field
 
@@ -125,6 +127,23 @@
 
     .. automethod:: cubicweb.web.views.forms.FieldsForm.render
 
+    **Form posting methods**
+
+    Once a form is posted, you can retrieve the form on the controller side and
+    use the following methods to ease processing. For "simple" forms, this
+    should looks like :
+
+    .. sourcecode :: python
+
+        form = self._cw.vreg['forms'].select('myformid', self._cw)
+        posted = form.process_posted()
+        # do something with the returned dictionary
+
+    Notice that form related to entity edition should usually use the
+    `edit` controller which will handle all the logic for you.
+
+    .. automethod:: cubicweb.web.views.forms.FieldsForm.process_content
+    .. automethod:: cubicweb.web.views.forms.FieldsForm.iter_modified_fields
     """
     __regid__ = 'base'
 
@@ -218,6 +237,19 @@
         for field in self.fields[:]:
             for field in field.actual_fields(self):
                 field.form_init(self)
+        # store used field in an hidden input for later usage by a controller
+        fields = set()
+        eidfields = set()
+        for field in self.fields:
+            if field.eidparam:
+                eidfields.add(field.role_name())
+            elif field.name not in self.control_fields:
+                fields.add(field.role_name())
+        if fields:
+            self.add_hidden('_cw_fields', u','.join(fields))
+        if eidfields:
+            self.add_hidden('_cw_entity_fields', u','.join(eidfields),
+                            eidparam=True)
 
     _default_form_action_path = 'edit'
     def form_action(self):
@@ -229,6 +261,50 @@
             return self._cw.build_url(self._default_form_action_path)
         return action
 
+    # controller form processing methods #######################################
+
+    def iter_modified_fields(self, editedfields=None, entity=None):
+        """return a generator on field that has been modified by the posted
+        form.
+        """
+        if editedfields is None:
+            try:
+                editedfields = self._cw.form['_cw_fields']
+            except KeyError:
+                raise RequestError(self._cw._('no edited fields specified'))
+        entityform = entity and self.field_by_name.im_func.func_code.co_argcount == 4 # XXX
+        for editedfield in splitstrip(editedfields):
+            try:
+                name, role = editedfield.split('-')
+            except:
+                name = editedfield
+                role = None
+            if entityform:
+                field = self.field_by_name(name, role, eschema=entity.e_schema)
+            else:
+                field = self.field_by_name(name, role)
+            if field.has_been_modified(self):
+                yield field
+
+    def process_posted(self):
+        """use this method to process the content posted by a simple form.  it
+        will return a dictionary with field names as key and typed value as
+        associated value.
+        """
+        with tempattr(self, 'formvalues', {}): # init fields value cache
+            errors = []
+            processed = {}
+            for field in self.iter_modified_fields():
+                try:
+                    for field, value in field.process_posted(self):
+                        processed[field.role_name()] = value
+                except ProcessFormError, exc:
+                    errors.append((field, exc))
+            if errors:
+                errors = dict((f.role_name(), unicode(ex)) for f, ex in errors)
+                raise ValidationError(None, errors)
+            return processed
+
     @deprecated('[3.6] use .add_hidden(name, value, **kwargs)')
     def form_add_hidden(self, name, value=None, **kwargs):
         return self.add_hidden(name, value, **kwargs)
@@ -323,16 +399,6 @@
         # different url after a validation error
         return '%s#%s' % (self._cw.url(), self.domid)
 
-    def build_context(self, formvalues=None):
-        if self.formvalues is not None:
-            return # already built
-        super(EntityFieldsForm, self).build_context(formvalues)
-        edited = set()
-        for field in self.fields:
-            if field.eidparam:
-                edited.add(field.role_name())
-        self.add_hidden('_cw_edited_fields', u','.join(edited), eidparam=True)
-
     def default_renderer(self):
         return self._cw.vreg['formrenderers'].select(
             self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row,