===================
Multi+Object Widget
===================
The multi widget allows you to add and edit one or more values.
In order to not overwhelm you with our set of well-chosen defaults,
all the default component registrations have been made prior to doing those
examples:
>>> from z3c.form import testing
>>> testing.setupFormDefaults()
As for all widgets, the multi widget must provide the new ``IWidget``
interface:
>>> from zope.interface.verify import verifyClass
>>> from z3c.form import interfaces
>>> from z3c.form.browser import multi
>>> verifyClass(interfaces.IWidget, multi.MultiWidget)
True
The widget can be instantiated only using the request:
>>> from z3c.form.testing import TestRequest
>>> request = TestRequest()
>>> widget = multi.MultiWidget(request)
Before rendering the widget, one has to set the name and id of the widget:
>>> widget.id = 'widget-id'
>>> widget.name = 'widget.name'
We also need to register the template for at least the widget and request:
>>> import zope.component
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> from z3c.form.testing import getPath
>>> from z3c.form.widget import WidgetTemplateFactory
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('multi_input.pt'), 'text/html'),
... (None, None, None, None, interfaces.IMultiWidget),
... IPageTemplate, name=interfaces.INPUT_MODE)
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('multi_display.pt'), 'text/html'),
... (None, None, None, None, interfaces.IMultiWidget),
... IPageTemplate, name=interfaces.DISPLAY_MODE)
For the next test, we need to setup our button handler adapters.
>>> from z3c.form import button
>>> zope.component.provideAdapter(button.ButtonActions)
>>> zope.component.provideAdapter(button.ButtonActionHandler)
>>> zope.component.provideAdapter(button.ButtonAction,
... provides=interfaces.IButtonAction)
Our submit buttons will need a template as well:
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('submit_input.pt'), 'text/html'),
... (None, None, None, None, interfaces.ISubmitWidget),
... IPageTemplate, name=interfaces.INPUT_MODE)
We can now render the widget:
>>> widget.update()
>>> print(widget.render())
As you can see the widget is empty and doesn't provide values. This is because
the widget does not know what sub-widgets to display. So let's register a
`IFieldWidget` adapter and a template for our `IInt` field:
>>> import z3c.form.interfaces
>>> from z3c.form.browser.text import TextFieldWidget
>>> zope.component.provideAdapter(TextFieldWidget,
... (zope.schema.interfaces.IInt, z3c.form.interfaces.IFormLayer))
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('text_input.pt'), 'text/html'),
... (None, None, None, None, interfaces.ITextWidget),
... IPageTemplate, name=interfaces.INPUT_MODE)
Let's now update the widget and check it again.
>>> widget.update()
>>> print(widget.render())
It's still the same. Since the widget doesn't provide a field nothing useful
gets rendered. Now let's define a field for this widget and check it again:
>>> from z3c.form.widget import FieldWidget
>>> from z3c.form.testing import IMySubObjectMulti
>>> from z3c.form.testing import MySubObjectMulti
>>> from z3c.form.object import registerFactoryAdapter
>>> registerFactoryAdapter(IMySubObjectMulti, MySubObjectMulti)
>>> field = zope.schema.List(
... __name__='foo',
... value_type=zope.schema.Object(title=u'my object widget',
... schema=IMySubObjectMulti),
... )
>>> widget = FieldWidget(field, widget)
>>> widget.update()
>>> print(widget.render())
As you can see, there is still no input value. Let's provide some values for
this widget. Before we can do that, we will need to register a data converter
for our multi widget and the data converter dispatcher adapter:
>>> from z3c.form.converter import IntegerDataConverter
>>> from z3c.form.converter import FieldWidgetDataConverter
>>> from z3c.form.converter import MultiConverter
>>> from z3c.form.validator import SimpleFieldValidator
>>> zope.component.provideAdapter(IntegerDataConverter)
>>> zope.component.provideAdapter(FieldWidgetDataConverter)
>>> zope.component.provideAdapter(SimpleFieldValidator)
>>> zope.component.provideAdapter(MultiConverter)
Bunch of adapters to get objectwidget work:
>>> from z3c.form import datamanager
>>> zope.component.provideAdapter(datamanager.DictionaryField)
>>> import z3c.form.browser.object
>>> zope.component.provideAdapter(z3c.form.browser.object.ObjectFieldWidget)
>>> import z3c.form.object
>>> zope.component.provideAdapter(z3c.form.object.ObjectConverter)
>>> import z3c.form.error
>>> zope.component.provideAdapter(z3c.form.error.ValueErrorViewSnippet)
>>> from z3c.form.object import SubformAdapter
>>> zope.component.provideAdapter(SubformAdapter)
>>> from zope.pagetemplate.interfaces import IPageTemplate
>>> from z3c.form.testing import getPath
>>> from z3c.form.widget import WidgetTemplateFactory
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('object_input.pt'), 'text/html'),
... (None, None, None, None, interfaces.IObjectWidget),
... IPageTemplate, name=interfaces.INPUT_MODE)
>>> zope.component.provideAdapter(
... WidgetTemplateFactory(getPath('object_display.pt'), 'text/html'),
... (None, None, None, None, interfaces.IObjectWidget),
... IPageTemplate, name=interfaces.DISPLAY_MODE)
>>> widget.update()
It must not fail if we assign values that do not meet the constraints,
just cry about it in the HTML:
>>> widget.value = [{'foofield': None, 'barfield': 666}]
>>> widget.update()
>>> print(widget.render())
Let's see what we get on value extraction:
>>> widget.extract()
If we now click on the ``Add`` button, we will get a new input field for enter
a new value:
>>> widget.request = TestRequest(form={'foo.count':u'2',
... 'foo.0.widgets.foofield':u'42',
... 'foo.0.widgets.barfield':u'666',
... 'foo.0-empty-marker':u'1',
... 'foo.1.widgets.foofield':u'789',
... 'foo.1.widgets.barfield':u'321',
... 'foo.1-empty-marker':u'1',
... 'foo.buttons.add':'Add'})
>>> widget.update()
>>> print(widget.render())
Let's see what we get on value extraction:
>>> value = widget.extract()
>>> pprint(value)
[{'barfield': 666, 'foofield': 42}, {'barfield': 321, 'foofield': 789}]
>>> converter = interfaces.IDataConverter(widget)
>>> value = converter.toFieldValue(value)
>>> value
[,
]
>>> value[0].foofield
42
>>> value[0].barfield
666
Now let's store the new value:
>>> widget.request = TestRequest(form={'foo.count':u'3',
... 'foo.0.widgets.foofield':u'42',
... 'foo.0.widgets.barfield':u'666',
... 'foo.0-empty-marker':u'1',
... 'foo.1.widgets.foofield':u'789',
... 'foo.1.widgets.barfield':u'321',
... 'foo.1-empty-marker':u'1',
... 'foo.2.widgets.foofield':u'46',
... 'foo.2.widgets.barfield':u'98',
... 'foo.2-empty-marker':u'1',
... })
>>> widget.update()
>>> print(widget.render())
Let's see what we get on value extraction:
>>> value = widget.extract()
>>> pprint(value)
[{'barfield': 666, 'foofield': 42},
{'barfield': 321, 'foofield': 789},
{'barfield': 98, 'foofield': 46}]
>>> converter = interfaces.IDataConverter(widget)
>>> value = converter.toFieldValue(value)
>>> value
[,
]
>>> value[0].foofield
42
>>> value[0].barfield
666
As you can see in the above sample, the new stored value gets rendered as a
real value and the new adding value input field is gone. Now let's try to
remove an existing value:
>>> widget.request = TestRequest(form={'foo.count':u'3',
... 'foo.0.widgets.foofield':u'42',
... 'foo.0.widgets.barfield':u'666',
... 'foo.0-empty-marker':u'1',
... 'foo.1.widgets.foofield':u'789',
... 'foo.1.widgets.barfield':u'321',
... 'foo.1-empty-marker':u'1',
... 'foo.2.widgets.foofield':u'46',
... 'foo.2.widgets.barfield':u'98',
... 'foo.2-empty-marker':u'1',
... 'foo.1.remove':u'1',
... 'foo.buttons.remove':'Remove selected'})
>>> widget.update()
>>> print(widget.render())
Let's see what we get on value extraction:
(this is good so, because Remove selected is a widget-internal submit)
>>> value = widget.extract()
>>> pprint(value)
[{'barfield': 666, 'foofield': 42},
{'barfield': 321, 'foofield': 789},
{'barfield': 98, 'foofield': 46}]
>>> converter = interfaces.IDataConverter(widget)
>>> value = converter.toFieldValue(value)
>>> value
[,
]
>>> value[0].foofield
42
>>> value[0].barfield
666
Error handling is next. Let's use the value "bad" (an invalid integer literal)
as input for our internal (sub) widget.
>>> from z3c.form.error import ErrorViewSnippet
>>> from z3c.form.error import StandardErrorViewTemplate
>>> zope.component.provideAdapter(ErrorViewSnippet)
>>> zope.component.provideAdapter(StandardErrorViewTemplate)
>>> widget.request = TestRequest(form={'foo.count':u'2',
... 'foo.0.widgets.foofield':u'42',
... 'foo.0.widgets.barfield':u'666',
... 'foo.0-empty-marker':u'1',
... 'foo.1.widgets.foofield':u'bad',
... 'foo.1.widgets.barfield':u'98',
... 'foo.1-empty-marker':u'1',
... })
>>> widget.update()
>>> print(widget.render())
Object is of wrong type.
The entered value is not a valid integer literal.
Let's see what we get on value extraction:
>>> value = widget.extract()
>>> pprint(value)
[{'barfield': 666, 'foofield': 42},
{'barfield': '98', 'foofield': 'bad'}]
Label
-----
There is an option which allows to disable the label for the (sub) widgets.
You can set the `showLabel` option to `False` which will skip rendering the
labels. Alternatively you can also register your own template for your layer
if you like to skip the label rendering for all widgets.
>>> field = zope.schema.List(
... __name__='foo',
... value_type=zope.schema.Object(title=u'ignored_title',
... schema=IMySubObjectMulti),
... )
>>> request = TestRequest()
>>> widget = multi.MultiWidget(request)
>>> widget = FieldWidget(field, widget)
>>> widget.value = [dict(foofield=42, barfield=666),
... dict(foofield=789, barfield=321)]
>>> widget.showLabel = False
>>> widget.update()
>>> print(widget.render())
In a form
---------
Let's try a simple example in a form.
We have to provide an adapter first:
>>> import z3c.form.browser.object
>>> zope.component.provideAdapter(z3c.form.browser.object.ObjectFieldWidget)
Forms and our objectwidget fire events on add and edit, setup a subscriber
for those:
>>> eventlog = []
>>> import zope.lifecycleevent
>>> @zope.component.adapter(zope.lifecycleevent.ObjectModifiedEvent)
... def logEvent(event):
... eventlog.append(event)
>>> zope.component.provideHandler(logEvent)
>>> @zope.component.adapter(zope.lifecycleevent.ObjectCreatedEvent)
... def logEvent2(event):
... eventlog.append(event)
>>> zope.component.provideHandler(logEvent2)
>>> def printEvents():
... for event in eventlog:
... print(event)
... if isinstance(event, zope.lifecycleevent.ObjectModifiedEvent):
... for attr in event.descriptions:
... print(attr.interface)
... print(sorted(attr.attributes))
We need to provide the widgets for the List
>>> from z3c.form.browser.multi import multiFieldWidgetFactory
>>> zope.component.provideAdapter(multiFieldWidgetFactory,
... (zope.schema.interfaces.IList, z3c.form.interfaces.IFormLayer))
>>> zope.component.provideAdapter(multiFieldWidgetFactory,
... (zope.schema.interfaces.ITuple, z3c.form.interfaces.IFormLayer))
>>> zope.component.provideAdapter(multiFieldWidgetFactory,
... (zope.schema.interfaces.IDict, z3c.form.interfaces.IFormLayer))
We define an interface containing a subobject, and an addform for it:
>>> from z3c.form import form, field
>>> from z3c.form.testing import MyMultiObject, IMyMultiObject
Note, that creating an object will print some information about it:
>>> class MyAddForm(form.AddForm):
... fields = field.Fields(IMyMultiObject)
... def create(self, data):
... print("MyAddForm.create")
... pprint(data)
... return MyMultiObject(**data)
... def add(self, obj):
... self.context[obj.name] = obj
... def nextURL(self):
... pass
We create the form and try to update it:
>>> request = TestRequest()
>>> myaddform = MyAddForm(root, request)
>>> myaddform.update()
As usual, the form contains a widget manager with the expected widget
>>> myaddform.widgets.keys()
['listOfObject', 'name']
>>> myaddform.widgets.values()
[, ]
If we want to render the addform, we must give it a template:
>>> import os
>>> from zope.browserpage.viewpagetemplatefile import ViewPageTemplateFile
>>> from zope.browserpage.viewpagetemplatefile import BoundPageTemplate
>>> from z3c.form import tests
>>> def addTemplate(form):
... form.template = BoundPageTemplate(
... ViewPageTemplateFile(
... 'simple_edit.pt', os.path.dirname(tests.__file__)), form)
>>> addTemplate(myaddform)
Now rendering the addform renders no items yet:
>>> print(myaddform.render())
We don't have the object (yet) in the root:
>>> root['first']
Traceback (most recent call last):
...
KeyError: 'first'
Add a row to the multi widget:
>>> request = TestRequest(form={
... 'form.widgets.listOfObject.count':u'0',
... 'form.widgets.listOfObject.buttons.add':'Add'})
>>> myaddform.request = request
Update with the request:
>>> myaddform.update()
Render the form:
>>> print(myaddform.render())
Now we can fill in some values to the object, and a name to the whole schema:
>>> request = TestRequest(form={
... 'form.widgets.listOfObject.count':u'1',
... 'form.widgets.listOfObject.0.widgets.foofield':u'66',
... 'form.widgets.listOfObject.0.widgets.barfield':u'99',
... 'form.widgets.listOfObject.0-empty-marker':u'1',
... 'form.widgets.name':u'first',
... 'form.buttons.add':'Add'})
>>> myaddform.request = request
Update the form with the request:
>>> myaddform.update()
MyAddForm.create
{'listOfObject': [>> root['first']
>>> root['first'].listOfObject
[]
Field values need to be right:
>>> root['first'].listOfObject[0].foofield
66
>>> root['first'].listOfObject[0].barfield
99
Let's see our event log:
>>> len(eventlog)
6
((why is IMySubObjectMulti created twice???))
>>> printEvents()
['barfield', 'foofield']
['barfield', 'foofield']
>>> eventlog=[]
Let's try to edit that newly added object:
>>> class MyEditForm(form.EditForm):
... fields = field.Fields(IMyMultiObject)
>>> editform = MyEditForm(root['first'], TestRequest())
>>> addTemplate(editform)
>>> editform.update()
Watch for the widget values in the HTML:
>>> print(editform.render())
Let's modify the values:
>>> request = TestRequest(form={
... 'form.widgets.listOfObject.count':u'1',
... 'form.widgets.listOfObject.0.widgets.foofield':u'43',
... 'form.widgets.listOfObject.0.widgets.barfield':u'55',
... 'form.widgets.listOfObject.0-empty-marker':u'1',
... 'form.widgets.name':u'first',
... 'form.buttons.apply':'Apply'})
They are still the same:
>>> root['first'].listOfObject[0].foofield
66
>>> root['first'].listOfObject[0].barfield
99
>>> editform.request = request
>>> editform.update()
Until we have updated the form:
>>> root['first'].listOfObject[0].foofield
43
>>> root['first'].listOfObject[0].barfield
55
Let's see our event log:
>>> len(eventlog)
7
((TODO: now this is real crap here, why is IMySubObjectMulti created 3 times???))
>>> printEvents()
['barfield', 'foofield']
['barfield', 'foofield']
['barfield', 'foofield']
['listOfObject']
>>> eventlog=[]
After the update the form says that the values got updated and renders the new
values:
>>> print(editform.render())
Data successfully updated.
Let's see if the widget keeps the old object on editing:
We add a special property to keep track of the object:
>>> root['first'].listOfObject[0].__marker__ = "ThisMustStayTheSame"
>>> root['first'].listOfObject[0].foofield
43
>>> root['first'].listOfObject[0].barfield
55
Let's modify the values:
>>> request = TestRequest(form={
... 'form.widgets.listOfObject.count':u'1',
... 'form.widgets.listOfObject.0.widgets.foofield':u'666',
... 'form.widgets.listOfObject.0.widgets.barfield':u'999',
... 'form.widgets.listOfObject.0-empty-marker':u'1',
... 'form.widgets.name':u'first',
... 'form.buttons.apply':'Apply'})
>>> editform.request = request
>>> editform.update()
Let's check what are ther esults of the update:
>>> root['first'].listOfObject[0].foofield
666
>>> root['first'].listOfObject[0].barfield
999
((TODO: bummer... we can't keep the old object))
#>>> root['first'].listOfObject[0].__marker__
#'ThisMustStayTheSame'
Let's make a nasty error, by typing 'bad' instead of an integer:
>>> request = TestRequest(form={
... 'form.widgets.listOfObject.count':u'1',
... 'form.widgets.listOfObject.0.widgets.foofield':u'99',
... 'form.widgets.listOfObject.0.widgets.barfield':u'bad',
... 'form.widgets.listOfObject.0-empty-marker':u'1',
... 'form.widgets.name':u'first',
... 'form.buttons.apply':'Apply'})
>>> editform.request = request
>>> eventlog=[]
>>> editform.update()
Eventlog must be clean:
>>> len(eventlog)
2
((TODO: bummer... who creates those 2 objects???))
>>> printEvents()
Watch for the error message in the HTML:
it has to appear at the field itself and at the top of the form:
((not nice: at the top ``Object is of wrong type.`` appears))
>>> print(editform.render())
There were some errors.
My list field:
Object is of wrong type.
The object values must stay at the old ones:
>>> root['first'].listOfObject[0].foofield
666
>>> root['first'].listOfObject[0].barfield
999
Simple but often used use-case is the display form:
>>> editform = MyEditForm(root['first'], TestRequest())
>>> addTemplate(editform)
>>> editform.mode = interfaces.DISPLAY_MODE
>>> editform.update()
>>> print(editform.render())