    Multi+Object Widget
    -------------------

    The multi widget allows you to add and edit one or more values.

      >>> from pyramid.testing import setUp, tearDown
      >>> config = setUp(hook_zca=True)

      >>> from cornice import includeme as include_cornice
      >>> include_cornice(config)
      >>> from pyams_utils import includeme as include_utils
      >>> include_utils(config)
      >>> from pyams_site import includeme as include_site
      >>> include_site(config)
      >>> from pyams_i18n import includeme as include_i18n
      >>> include_i18n(config)
      >>> from pyams_form import includeme as include_form
      >>> include_form(config)

    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 pyams_form import util, testing
      >>> testing.setup_form_defaults(config.registry)

    As for all widgets, the multi widget must provide the new ``IWidget``
    interface:

      >>> from pyams_form import interfaces
      >>> from pyams_form.browser import multi

      >>> interfaces.widget.IWidget.implementedBy(multi.MultiWidget)
      True

    The widget can be instantiated only using the request:

      >>> from pyams_utils.testing import format_html
      >>> from pyams_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 can now render the widget:

      >>> widget.update()
      >>> print(format_html(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. Now let's define a field
    for this widget and check it again:

      >>> from pyams_form.widget import FieldWidget

      >>> from pyams_form.testing import IMySubObjectMulti
      >>> from pyams_form.testing import MySubObjectMulti

      >>> from pyams_form.object import register_factory_adapter
      >>> register_factory_adapter(IMySubObjectMulti, MySubObjectMulti)

      >>> import zope.schema
      >>> field = zope.schema.List(
      ...     __name__='foo',
      ...     value_type=zope.schema.Object(title='my object widget',
      ...                                   schema=IMySubObjectMulti),
      ...     )

      >>> widget = FieldWidget(field, widget)
      >>> widget.update()
      >>> print(format_html(widget.render()))
As you can see, there is still no input value. Let's provide some values for this widget. It must not fail if we assign values that do not meet the constraints, just cry about it in the HTML: >>> from pyams_form import object >>> widget.value = [object.ObjectWidgetValue( ... {'foofield': '', 'barfield': '666'})] >>> widget.update() >>> print(format_html(widget.render()))
my object widget *
My foo field *
Required input is missing.
My dear bar
    Let's set acceptable values:

      >>> widget.value = [
      ...     object.ObjectWidgetValue(dict(foofield='42', barfield='666')),
      ...     object.ObjectWidgetValue(dict(foofield='789', barfield='321'))]

      >>> print(format_html(widget.render()))
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
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(params={'foo.count': '2',
      ...                                      'foo.0.widgets.foofield': '42',
      ...                                      'foo.0.widgets.barfield': '666',
      ...                                      'foo.0-empty-marker': '1',
      ...                                      'foo.1.widgets.foofield': '789',
      ...                                      'foo.1.widgets.barfield': '321',
      ...                                      'foo.1-empty-marker': '1',
      ...                                      'foo.buttons.add': 'Add'})
      >>> widget.update()
      >>> print(format_html(widget.render()))
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
    Let's see what we get on value extraction:

      >>> from pprint import pprint
      >>> value = widget.extract()
      >>> pprint(value)
      [{'barfield': '666', 'foofield': '42'}, {'barfield': '321', 'foofield': '789'}]
      >>> converter = interfaces.IDataConverter(widget)

      >>> value = converter.to_field_value(value)
      >>> value
      [
,
    ]

      >>> value[0].foofield
      42
      >>> value[0].barfield
      666

    Now let's store the new value:

      >>> widget.request = TestRequest(params={'foo.count': '3',
      ...                                      'foo.0.widgets.foofield': '42',
      ...                                      'foo.0.widgets.barfield': '666',
      ...                                      'foo.0-empty-marker': '1',
      ...                                      'foo.1.widgets.foofield': '789',
      ...                                      'foo.1.widgets.barfield': '321',
      ...                                      'foo.1-empty-marker': '1',
      ...                                      'foo.2.widgets.foofield': '46',
      ...                                      'foo.2.widgets.barfield': '98',
      ...                                      'foo.2-empty-marker': '1',
      ...                                    })
      >>> widget.update()
      >>> print(format_html(widget.render()))
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
    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.to_field_value(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(params={'foo.count':'3',
      ...                                      'foo.0.widgets.foofield':'42',
      ...                                      'foo.0.widgets.barfield':'666',
      ...                                      'foo.0-empty-marker':'1',
      ...                                      'foo.1.widgets.foofield':'789',
      ...                                      'foo.1.widgets.barfield':'321',
      ...                                      'foo.1-empty-marker':'1',
      ...                                      'foo.2.widgets.foofield':'46',
      ...                                      'foo.2.widgets.barfield':'98',
      ...                                      'foo.2-empty-marker':'1',
      ...                                      'foo.1.remove':'1',
      ...                                      'foo.buttons.remove':'Remove selected'})
      >>> widget.update()
      >>> print(format_html(widget.render()))
my object widget *
My foo field *
My dear bar
my object widget *
My foo field *
My dear bar
    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.to_field_value(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.

      >>> widget.request = TestRequest(params={'foo.count':'2',
      ...                                      'foo.0.widgets.foofield':'42',
      ...                                      'foo.0.widgets.barfield':'666',
      ...                                      'foo.0-empty-marker':'1',
      ...                                      'foo.1.widgets.foofield':'bad',
      ...                                      'foo.1.widgets.barfield':'98',
      ...                                      'foo.1-empty-marker':'1',
      ...                                      })

      >>> widget.update()
      >>> print(format_html(widget.render()))
my object widget *
My foo field *
My dear bar
my object widget *
The entered value is not a valid integer literal.
My foo field *
The entered value is not a valid integer literal.
My dear bar
    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 `show_label` 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 = [
      ...     object.ObjectWidgetValue(dict(foofield='42', barfield='666')),
      ...     object.ObjectWidgetValue(dict(foofield='789', barfield='321'))]
      >>> widget.show_label = False
      >>> widget.update()
      >>> print(format_html(widget.render()))
My foo field *
My dear bar
My foo field *
My dear bar
    In a form
    #########

    Let's try a simple example in a form.

    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)
      >>> _ = config.add_subscriber(logEvent, zope.lifecycleevent.interfaces.IObjectCreatedEvent)
      >>> _ = config.add_subscriber(logEvent, zope.lifecycleevent.interfaces.IObjectModifiedEvent)

      >>> 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 define an interface containing a subobject, and an addform for it:

      >>> from pyams_form import form, field
      >>> from pyams_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:

      >>> from zope.container.folder import Folder
      >>> root = Folder()
      >>> request = TestRequest()
      >>> myaddform =  MyAddForm(root, request)

      >>> myaddform.update()

    As usual, the form contains a widget manager with the expected widget

      >>> list(myaddform.widgets.keys())
      ['list_of_objects', 'name']
      >>> list(myaddform.widgets.values())
      [
,
    ]

    If we want to render the addform, we must give it a template:

      >>> import os
      >>> from pyams_template.interfaces import IContentTemplate
      >>> from pyams_template.template import TemplateFactory
      >>> from pyams_layer.interfaces import IFormLayer
      >>> from pyams_form import tests
      >>> factory = TemplateFactory(os.path.join(os.path.dirname(tests.__file__),
      ...                           'templates', 'simple-edit.pt'), 'text/html')
      >>> config.registry.registerAdapter(factory, (None, IFormLayer, MyAddForm), IContentTemplate)

    Now rendering the addform renders no items yet:

      >>> print(format_html(myaddform.render()))
My list field
name
    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(params={
      ...     'form.widgets.list_of_objects.count':'0',
      ...     'form.widgets.list_of_objects.buttons.add':'Add'})
      >>> myaddform.request = request

    Update with the request:

      >>> myaddform.update()

    Render the form:

      >>> print(format_html(myaddform.render()))
My list field
my object widget *
My foo field *
My dear bar
name
    Now we can fill in some values to the object, and a name to the whole schema:

      >>> request = TestRequest(params={
      ...     'form.widgets.list_of_objects.count':'1',
      ...     'form.widgets.list_of_objects.0.widgets.foofield':'66',
      ...     'form.widgets.list_of_objects.0.widgets.barfield':'99',
      ...     'form.widgets.list_of_objects.0-empty-marker':'1',
      ...     'form.widgets.name':'first',
      ...     'form.buttons.add':'Add'})
      >>> myaddform.request = request

    Update the form with the request:

      >>> myaddform.update()
      MyAddForm.create
      {'list_of_objects': [
>> root['first'] >>> root['first'].list_of_objects [
    ]

    Field values need to be right:

      >>> root['first'].list_of_objects[0].foofield
      66
      >>> root['first'].list_of_objects[0].barfield
      99

    Let's see our event log:

      >>> len(eventlog)
      5

    ((why is IMySubObjectMulti created twice???))

      >>> printEvents()
pyams_form.testing.IMySubObjectMulti ['barfield', 'foofield'] pyams_form.testing.IMySubObjectMulti ['barfield', 'foofield']
    # TODO: look for missing ContainerModifiedEvent!!!

      >>> eventlog = []

    Let's try to edit that newly added object:

      >>> class MyEditForm(form.EditForm):
      ...     fields = field.Fields(IMyMultiObject)

      >>> editform = MyEditForm(root['first'], TestRequest())
      >>> config.registry.registerAdapter(factory, (None, IFormLayer, MyEditForm), IContentTemplate)
      >>> editform.update()

    Watch for the widget values in the HTML:

      >>> print(format_html(editform.render()))
My list field
my object widget *
My foo field *
My dear bar
name
    Let's modify the values:

      >>> request = TestRequest(params={
      ...     'form.widgets.list_of_objects.count':'1',
      ...     'form.widgets.list_of_objects.0.widgets.foofield':'43',
      ...     'form.widgets.list_of_objects.0.widgets.barfield':'55',
      ...     'form.widgets.list_of_objects.0-empty-marker':'1',
      ...     'form.widgets.name':'first',
      ...     'form.buttons.apply':'Apply'})

    They are still the same:

      >>> root['first'].list_of_objects[0].foofield
      66
      >>> root['first'].list_of_objects[0].barfield
      99

      >>> editform.request = request
      >>> editform.update()

    Until we have updated the form:

      >>> root['first'].list_of_objects[0].foofield
      43
      >>> root['first'].list_of_objects[0].barfield
      55

    Let's see our event log:

      >>> len(eventlog)
      5

    ((TODO: now this is real crap here, why is IMySubObjectMulti created 3 times???))

      >>> printEvents()
pyams_form.testing.IMySubObjectMulti ['barfield', 'foofield'] pyams_form.testing.IMySubObjectMulti ['barfield', 'foofield'] pyams_form.testing.IMyMultiObject ['list_of_objects'] >>> eventlog=[] After the update the form says that the values got updated and renders the new values: >>> print(format_html(editform.render())) Data successfully updated.
My list field
my object widget *
My foo field *
My dear bar
name
    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'].list_of_objects[0].__marker__ = "ThisMustStayTheSame"

      >>> root['first'].list_of_objects[0].foofield
      43
      >>> root['first'].list_of_objects[0].barfield
      55

    Let's modify the values:

      >>> request = TestRequest(params={
      ...     'form.widgets.list_of_objects.count':'1',
      ...     'form.widgets.list_of_objects.0.widgets.foofield':'666',
      ...     'form.widgets.list_of_objects.0.widgets.barfield':'999',
      ...     'form.widgets.list_of_objects.0-empty-marker':'1',
      ...     'form.widgets.name':'first',
      ...     'form.buttons.apply':'Apply'})

      >>> editform.request = request

      >>> editform.update()

    Let's check what are ther esults of the update:

      >>> root['first'].list_of_objects[0].foofield
      666
      >>> root['first'].list_of_objects[0].barfield
      999

    ((TODO: bummer... we can't keep the old object))

      #>>> root['first'].list_of_objects[0].__marker__
      #'ThisMustStayTheSame'

    Let's make a nasty error, by typing 'bad' instead of an integer:

      >>> request = TestRequest(params={
      ...     'form.widgets.list_of_objects.count':'1',
      ...     'form.widgets.list_of_objects.0.widgets.foofield':'99',
      ...     'form.widgets.list_of_objects.0.widgets.barfield':'bad',
      ...     'form.widgets.list_of_objects.0-empty-marker':'1',
      ...     'form.widgets.name':'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(format_html(editform.render())) There were some errors.

- My list field
  The entered value is not a valid integer literal.

The entered value is not a valid integer literal.
My list field
my object widget *
The entered value is not a valid integer literal.
My foo field *
My dear bar
The entered value is not a valid integer literal.
name
    The object values must stay at the old ones:

      >>> root['first'].list_of_objects[0].foofield
      666
      >>> root['first'].list_of_objects[0].barfield
      999

    Simple but often used use-case is the display form:

      >>> editform = MyEditForm(root['first'], TestRequest())
      >>> editform.mode = interfaces.DISPLAY_MODE
      >>> editform.update()
      >>> print(format_html(editform.render()))
My list field
my object widget *
My foo field *
666
My dear bar
999
name first
Tests cleanup: >>> tearDown()