[Zope3-Users] subform support for z3c.form
garz
garz at gmx.net
Sun Jul 13 17:38:33 EDT 2008
hi,
after i didnt like the way z3c.formdemo-package solved the subform problem
by using sessions, i decided to write a package that supports subforms that
are able to extract the needed information from the request. since all the
needed information is in the request, a session is not needed. if the
community is interested, id like to share it with the rest, because i also
benefit from packages wrote by others. here is the doctest that show you how
this package works:
========
Subforms
========
Add Forms
~~~~~~~~~
Subforms allow you to include forms defined by other objects. Usually you
have
some basic classes like, lets say a person:
>>> import zope.interface
>>> class IPerson(zope.interface.Interface):
... """ Simple Person. """
...
... name = zope.schema.TextLine(title=u"Name")
... age = zope.schema.Int(title=u"Age", min=0)
And you have some classes, that wants to have the basic ones as an
attribute:
>>> class ICar(zope.interface.Interface):
... """ Simple Car. """
...
... type = zope.schema.TextLine(title=u"Type")
... owner = zope.schema.Object(title=u"Owner", schema=IPerson)
Lets get those interfaces implemented by some classes:
>>> class Person(object):
... zope.interface.implements(IPerson)
...
... def __init__(self, name=u"", age=0):
... self.name = name
... self.age = age
>>> class Car(object):
... zope.interface.implements(ICar)
...
... def __init__(self, type=u"", owner=None):
... self.type = type
... self.owner = owner
It's very important that your classes are new style python classes by
deriving
them from 'object'. In the SubForms.update-method we need introspection to
obtain the sub-contexts by their name. This is done by using the
__getattribute__-method.
Lets start the interesting stuff by defining the add forms:
>>> from z3c.form import field
>>> from z3c.subform import form, subform
>>> class PersonAddForm(form.AddForm):
... fields = field.Fields(IPerson)
... prefix = 'person'
...
... def create(self, data):
... return Person(**data)
...
... def add(self, obj):
... self.context[str(obj.name)] = obj
...
... def nextUrl(self):
... return 'index.html'
>>> class CarAddForm(form.AddForm):
... fields = field.Fields(ICar).select('type')
... subFields = subform.SubFields(ICar).select('owner') # 1st step
... subFields['owner'].viewFactory = PersonAddForm # 2nd step
... subFields['owner'].logicalUnit = True
... subFormAdds = ['owner'] # 3rd step
... prefix = 'car'
...
... def create(self, data):
... return Car(**data)
...
... def add(self, obj):
... self.context[obj.type] = obj
...
... def nextUrl(self):
... return 'index.html'
Here you can see the definition of the 'owner'-field as subform.
To achieve this, you first you have to tell the SubFields-Manager, which
Object-schemas we want to have as subforms. It is a manager just like the
Fields-Manager, except that it will only take care of Object-Fields. If you
give in an Interface, it will go through the fields an add those that
provide
zope.schema.interfaces.IObject.
The next step is to assign a view to the SubField. This is done via the
SubField.viewFactory attribute. Simply give the class of the AddForm you
want to use. Note that it is important that this Form is derived from one
of the forms provided by this package. Because these forms have a special
attribute 'logicalUnit' that marks them as a ... ehm ... logical unit, like
the one described in the subform.txt from the z3c.form-package. You can
alter
this flag by setting the SubField.logicalUnit attribute, which is by default
True. So we could have left its definition out in the CarAddForm example.
It's only there for demonstration.
The third and last step is to tell the form, which subforms 'add'-actions
should be executed, when pressing the add-button in the parent form. This is
done by filling the 'subFormAdds'-list with the names of the subforms.
Thats all, there is nothing more to it.
The rest simply stays the same way as usual, with one difference. The
create-methods 'data'-dictionary now includes a new entry with the key
'owner' and the newly created Owner-instance as value! This rocks! No
messing
around with sessions like in the formdemo-example. This approach is much
cleaner, because it directly uses the data that is coming with the request,
not going a detour by using sessions. (!_imho_!)
The next advantage is, that every AddForm or EditForm you create with this
package is always subform-capable. This means you are writing forms once and
you don't have to care about neither you want to use your form as a subform
nor
as a regular one. To achieve this, every form in this package applies to the
Pagelet-pattern. This is necessary, so that the form's render-method only
renders the content, not the form as a whole (including layout). It means
that
you will always have to supply a layout- and a content-templates. You can
find more information on this in the z3c.pagelet-package.
This has the following consequences. Let's assume, we want to extend our
application with, let's say a garage:
>>> class IGarage(zope.interface.Interface):
... """ Simple garage. """
...
... size = zope.schema.Int(title=u"Size")
... parking = zope.schema.Object(title=u"Parking", schema=ICar)
>>> class Garage(object):
... zope.interface.implements(IGarage)
...
... def __init__(self, size=0, parking=None):
... self.size = size
... self.parking = parking
>>> class GarageAddForm(form.AddForm):
... fields = field.Fields(IGarage).select('size')
... subFields = subform.SubFields(IGarage).select('parking')
... subFields['parking'].viewFactory = CarAddForm
... subFormAdds = ['parking']
... prefix = 'garage'
...
... def create(self, data):
... return Garage(**data)
...
... def add(self, obj):
... self.context[str(obj.size)] = obj
...
... def nextUrl(self):
... return 'index.html'
Now we have a garage thats has a parking car, which in turn has an
owner. Note that the more subobjects you encapsulate, the longer the widgets
names get. So maybe you shouldn't encapsulate too much.
Edit Forms
~~~~~~~~~~
For EditForms, its exactly the same thing, with one difference, the
'subFormAdds'-list there names 'subFormEdits'.
>>> class PersonEditForm(form.EditForm):
... fields = field.Fields(IPerson)
... prefix = 'person'
>>> class CarEditForm(form.EditForm):
... fields = field.Fields(ICar).select('type')
... subFields = subform.SubFields(ICar).select('owner')
... subFields['owner'].viewFactory = PersonEditForm
... subFields['owner'].logicalUnit = True
... subFormEdits = ['owner']
... prefix = 'car'
>>> class GarageEditForm(form.EditForm):
... fields = field.Fields(IGarage).select('size')
... subFields = subform.SubFields(IGarage).select('parking')
... subFields['parking'].viewFactory = CarEditForm
... subFormEdits = ['parking']
... prefix = 'garage'
Finished.
Developing subforms with custom buttons
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you want to develop a form that doesn't follow the AddForm or EditForm
pattern, this package supports you with a method provided by the
'SubForms' manager. Is is called 'executeButton'. You give it a subFormName,
a buttonName and the action of the handler you are in and it will call
the handler of the subforms button. This is not difficult and if you need
an example, just look at the AddForm and EditForm implementations of this
package.
There you also will see, that you can use the 'logicalUnit'-flag to
make your form differ from wether it is a logical unit or not. That way
the AddForm and EditForm were made both kinds, "logical unit"-subforms and
"stand alone"-forms.
The next thing you will notice there, is that the button
has a simple condition assigned, that checks if the 'logicalUnit'-flag is
False. This means, that the button only gets showed, if the form is not a
logical unit.
With these 3 things, you should be able to implement you custom
executing-subform-actions-and-beeing-both-kinds forms. Or any variation.
The testing
~~~~~~~~~~~
Now that everything is explained, lets prove that this stuff works.
First let's init all those z3c.form registrations:
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
And the new one brought to you by this package. It's an adapter for the
SubForms-manager:
>>> from z3c.subform.interfaces import ISubForms
>>> import zope.component
>>> zope.component.provideAdapter(
... subform.SubForms,
... (None, None, None),
... ISubForms)
Let's start the action with the PersonAddForm. It is possible to add Person-
objects to a container with it:
>>> request = testing.TestRequest(form={
... 'person.widgets.name': u'batman',
... 'person.widgets.age': u'23',
... 'person.buttons.add': u'Add'}
... )
>>> personAddForm = PersonAddForm(root, request)
>>> personAddForm.update()
>>> sorted(root)
[u'batman']
>>> batman = root[u'batman']
>>> batman.name
u'batman'
>>> batman.age
23
>>> del root[u'batman']
Fine, now it gets more exciting. Let's test the CarAddForm:
>>> request = testing.TestRequest()
>>> carAddForm = CarAddForm(None, request)
After updating it, a SubForms-manager should be available:
>>> carAddForm.update()
>>> carAddForm.subForms
<z3c.subform.subform.SubForms object at ...>
This one now should manage the PersonAddForm, tagged as a logical unit:
>>> subForms = carAddForm.subForms
>>> subForms.keys()
['owner']
>>> subForms['owner']
<PersonAddForm object at ...>
>>> subForms['owner'].logicalUnit
True
And because it is a logical unit, it shouldn't have the add-action, because
the button condition should have filtered it.
>>> subForms['owner'].actions.keys()
[]
Now let's add a car. As you can see, in the request, all subforms are
collected under the prefix 'subforms', followed by the fields name. This way
you can combine as many forms of the same kind as you like.
>>> request = testing.TestRequest(form={
... 'car.widgets.type': u'batmobile',
... 'car.subforms.owner.widgets.name': u'batman',
... 'car.subforms.owner.widgets.age': u'23',
... 'car.buttons.add': u'Add'}
... )
>>> carAddForm = CarAddForm(root, request)
>>> carAddForm.update()
>>> sorted(root)
[u'batmobile']
>>> batmobile = root[u'batmobile']
>>> batmobile.type
u'batmobile'
>>> batmobile.owner
<Person object at ...>
>>> batmobile.owner.name
u'batman'
>>> batmobile.owner.age
23
>>> del root[u'batmobile']
As promised, the owner-field now has a Person-instance. But what is, if we
gave incorrect information. Let's try this:
>>> request = testing.TestRequest(form={
... 'car.widgets.type': u'batmobile',
... 'car.subforms.owner.widgets.name': u'batman',
... 'car.subforms.owner.widgets.age': u'-5',
... 'car.buttons.add': u'Add'}
... )
>>> carAddForm = CarAddForm(root, request)
>>> carAddForm.update()
>>> sorted(root)
[]
Nothing was added and the form's _finishedAdd-attribute should be False:
>>> carAddForm._finishedAdd
False
Now that this works, let's have a short check on the GarageAddForm:
>>> request = testing.TestRequest(form={
... 'garage.widgets.size': u'100',
... 'garage.subforms.parking.widgets.type': u'batmobile',
... 'garage.subforms.parking.subforms.owner.widgets.name': u'batman',
... 'garage.subforms.parking.subforms.owner.widgets.age': u'23',
... 'garage.buttons.add': u'Add'}
... )
>>> garageAddForm = GarageAddForm(root, request)
>>> garageAddForm.update()
>>> sorted(root)
[u'100']
>>> hundred = root[u'100']
>>> hundred.size
100
>>> hundred.parking
<Car object at ...>
>>> hundred.parking.type
u'batmobile'
>>> hundred.parking.owner
<Person object at ...>
>>> hundred.parking.owner.name
u'batman'
>>> hundred.parking.owner.age
23
>>> del root[u'100']
This is fine.
Let's assume, that if error handling works for CarAddForm, it
will work for any deepness of subforming.
Now the EditForms. First lets edit the created person:
>>> request = testing.TestRequest(form={
... 'person.widgets.name': u'robin',
... 'person.widgets.age': u'14',
... 'person.buttons.apply': u'Apply'}
... )
>>> robin = Person()
>>> personEditForm = PersonEditForm(robin, request)
>>> personEditForm.update()
>>> robin.name
u'robin'
>>> robin.age
14
Now the car:
>>> request = testing.TestRequest(form={
... 'car.widgets.type': u'robmobile',
... 'car.subforms.owner.widgets.name': u'robben',
... 'car.subforms.owner.widgets.age': u'17',
... 'car.buttons.apply': u'Apply'}
... )
>>> robmobile = Car()
>>> robmobile.owner = robin
>>> carEditForm = CarEditForm(robmobile, request)
>>> carEditForm.update()
>>> robmobile.type
u'robmobile'
>>> robmobile.owner.name
u'robben'
>>> robmobile.owner.age
17
And the garage:
>>> request = testing.TestRequest(form={
... 'garage.widgets.size': u'99',
... 'garage.subforms.parking.widgets.type': u'robinmobile',
... 'garage.subforms.parking.subforms.owner.widgets.name': u'rob',
... 'garage.subforms.parking.subforms.owner.widgets.age': u'15',
... 'garage.buttons.apply': u'Apply'}
... )
>>> twenty = Garage()
>>> twenty.parking = robmobile
>>> garageEditForm = GarageEditForm(twenty, request)
>>> garageEditForm.update()
>>> twenty.size
99
>>> twenty.parking.type
u'robinmobile'
>>> twenty.parking.owner.name
u'rob'
>>> twenty.parking.owner.age
15
And Errorhandling:
>>> request = testing.TestRequest(form={
... 'garage.widgets.size': u'77',
... 'garage.subforms.parking.widgets.type': u'robmobile',
... 'garage.subforms.parking.subforms.owner.widgets.name': u'robin
rob',
... 'garage.subforms.parking.subforms.owner.widgets.age': u'-15',
... 'garage.buttons.apply': u'Apply'}
... )
>>> garageEditForm = GarageEditForm(twenty, request)
>>> garageEditForm.update()
>>> twenty.size
99
>>> twenty.parking.type
u'robinmobile'
>>> twenty.parking.owner.name
u'rob'
>>> twenty.parking.owner.age
15
You can see here, that if there is any error, no changes will be applied at
all.
The End
--
View this message in context: http://www.nabble.com/subform-support-for-z3c.form-tp18434209p18434209.html
Sent from the Zope3 - users mailing list archive at Nabble.com.
More information about the Zope3-users
mailing list