# HG changeset patch # User Sylvain Thénault # Date 1309366643 -7200 # Node ID e1881933f366c9753caab7aa66ec97ec4006ced4 # Parent 6632c762cd63bcf43e5af29e364ed25d438c3dd4 [form, controller] closes #1787233: form should provide a method to process posted content diff -r 6632c762cd63 -r e1881933f366 web/form.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): diff -r 6632c762cd63 -r e1881933f366 web/test/unittest_application.py --- 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 diff -r 6632c762cd63 -r e1881933f366 web/test/unittest_form.py --- 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): diff -r 6632c762cd63 -r e1881933f366 web/test/unittest_reledit.py --- 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 @@ - +
@@ -97,7 +97,7 @@ - +
@@ -141,7 +141,7 @@ - +
diff -r 6632c762cd63 -r e1881933f366 web/test/unittest_views_basecontrollers.py --- 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", diff -r 6632c762cd63 -r e1881933f366 web/views/editcontroller.py --- 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) diff -r 6632c762cd63 -r e1881933f366 web/views/forms.py --- 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,