========= Sub-Forms ========= Traditionally, the Zope community talks about sub-forms in a generic manner without defining their purpose, restrictions and assumptions. When we initially talked about sub-forms for this package, we quickly noticed that there are several classes of sub-forms with different goals. Of course, we need to setup our defaults for this demonstration as well: >>> from z3c.form import testing >>> testing.setupFormDefaults() Class I: The form within the form --------------------------------- This class of sub-forms provides a complete form within a form, including its own actions. When an action of the sub-form is submitted, the parent form usually does not interact with that action at all. The same is true for the reverse; when an action of the parent form is submitted, the sub-form does not react. A classic example for this type of sub-form is uploading an image. The subform allows uploading a file and once the file is uploaded the image is shown as well as a "Delete"/"Clear" button. The sub-form will store the image in the session and when the main form is submitted it looks in the session for the image. This scenario was well supported in ``zope.formlib`` and also does not require special support in ``z3c.form``. Let me show you, how this can be done. In this example, we would like to describe a car and its owner: >>> import zope.interface >>> import zope.schema >>> class IOwner(zope.interface.Interface): ... name = zope.schema.TextLine(title=u'Name') ... license = zope.schema.TextLine(title=u'License') >>> class ICar(zope.interface.Interface): ... model = zope.schema.TextLine(title=u'Model') ... make = zope.schema.TextLine(title=u'Make') ... owner = zope.schema.Object(title=u'Owner', schema=IOwner) Let's now implement the two interfaces and create instances, so that we can create edit forms for it: >>> @zope.interface.implementer(IOwner) ... class Owner(object): ... def __init__(self, name, license): ... self.name = name ... self.license = license >>> @zope.interface.implementer(ICar) ... class Car(object): ... def __init__(self, model, make, owner): ... self.model = model ... self.make = make ... self.owner = owner >>> me = Owner(u'Stephan Richter', u'MA-1231FW97') >>> mycar = Car(u'Nissan', u'Sentra', me) We define the owner sub-form as we would any other form. The only difference is the template, which should not render a form-tag: >>> import os >>> from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile >>> from z3c.form import form, field, tests >>> templatePath = os.path.dirname(tests.__file__) >>> class OwnerForm(form.EditForm): ... template = ViewPageTemplateFile( ... 'simple_owneredit.pt', templatePath) ... fields = field.Fields(IOwner) ... prefix = 'owner' Next we define the car form, which has the owner form as a sub-form. The car form also needs a special template, since it needs to render the sub-form at some point. For the simplicity of this example, I have duplicated a lot of template code here, but you can use your favorite template techniques, such as METAL macros, viewlets, or pagelets to make better reuse of some code. >>> class CarForm(form.EditForm): ... fields = field.Fields(ICar).select('model', 'make') ... template = ViewPageTemplateFile( ... 'simple_caredit.pt', templatePath) ... prefix = 'car' ... def update(self): ... self.owner = OwnerForm(self.context.owner, self.request) ... self.owner.update() ... super(CarForm, self).update() Let's now instantiate the form and render it: >>> from z3c.form.testing import TestRequest >>> request = TestRequest() >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> print(carForm.render())
I can now submit the owner form, which should not submit any car changes I might have made in the form: >>> request = TestRequest(form={ ... 'car.widgets.model': u'BMW', ... 'car.widgets.make': u'325', ... 'owner.widgets.name': u'Stephan Richter', ... 'owner.widgets.license': u'MA-97097A87', ... 'owner.buttons.apply': u'Apply' ... }) >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> mycar.model u'Nissan' >>> mycar.make u'Sentra' >>> me.name u'Stephan Richter' >>> me.license u'MA-97097A87' Also, the form should say that the data of the owner has changed: >>> print(carForm.render()) The same is true the other way around as well. Submitting the overall form does not submit the owner form: >>> request = TestRequest(form={ ... 'car.widgets.model': u'BMW', ... 'car.widgets.make': u'325', ... 'car.buttons.apply': u'Apply', ... 'owner.widgets.name': u'Claudia Richter', ... 'owner.widgets.license': u'MA-123403S2', ... }) >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> mycar.model u'BMW' >>> mycar.make u'325' >>> me.name u'Stephan Richter' >>> me.license u'MA-97097A87' Class II: The logical unit -------------------------- In this class of sub-forms, a sub-form is often just a collection of widgets without any actions. Instead, the sub-form must be able to react to the actions of the parent form. A good example of those types of sub-forms is actually the example I chose above. So let's redevelop our example above in a way that the owner sub-form is just a logical unit that shares the action with its parent form. Initially, the example does not look very different, except that we use ``EditSubForm`` as a base class: >>> from z3c.form import subform >>> class OwnerForm(subform.EditSubForm): ... template = ViewPageTemplateFile( ... 'simple_subedit.pt', templatePath) ... fields = field.Fields(IOwner) ... prefix = 'owner' The main form also is pretty much the same, except that a subform takes three constructor arguments, the last one being the parent form: >>> class CarForm(form.EditForm): ... fields = field.Fields(ICar).select('model', 'make') ... template = ViewPageTemplateFile( ... 'simple_caredit.pt', templatePath) ... prefix = 'car' ... ... def update(self): ... super(CarForm, self).update() ... self.owner = OwnerForm(self.context.owner, self.request, self) ... self.owner.update() Rendering the form works as before: >>> request = TestRequest() >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> print(carForm.render()) The interesting part of this setup is that the "Apply" button calls the action handlers for both, the main and the sub-form: >>> request = TestRequest(form={ ... 'car.widgets.model': u'Ford', ... 'car.widgets.make': u'F150', ... 'car.buttons.apply': u'Apply', ... 'owner.widgets.name': u'Claudia Richter', ... 'owner.widgets.license': u'MA-991723FDG', ... }) >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> mycar.model u'Ford' >>> mycar.make u'F150' >>> me.name u'Claudia Richter' >>> me.license u'MA-991723FDG' Let's now have a look at cases where an error happens. If an error occurs in the parent form, the sub-form is still submitted: >>> request = TestRequest(form={ ... 'car.widgets.model': u'Volvo\n~', ... 'car.widgets.make': u'450', ... 'car.buttons.apply': u'Apply', ... 'owner.widgets.name': u'Stephan Richter', ... 'owner.widgets.license': u'MA-991723FDG', ... }) >>> carForm = CarForm(mycar, request) >>> carForm.update() >>> mycar.model u'Ford' >>> mycar.make u'F150' >>> me.name u'Stephan Richter' >>> me.license u'MA-991723FDG' Let's look at the rendered form: >>> print(carForm.render()) There were some errors.