[Checkins] SVN: z3c.form/trunk/ merge of branch adamg-objectwidget,

Adam Groszer agroszer at gmail.com
Fri Nov 21 04:14:52 EST 2008


Log message for revision 93230:
  merge of branch adamg-objectwidget,
  there is one test error, related to z3c.pt
  

Changed:
  U   z3c.form/trunk/CHANGES.txt
  U   z3c.form/trunk/buildout.cfg
  U   z3c.form/trunk/src/z3c/form/README.txt
  U   z3c.form/trunk/src/z3c/form/browser/configure.zcml
  U   z3c.form/trunk/src/z3c/form/browser/image.py
  A   z3c.form/trunk/src/z3c/form/browser/object.py
  A   z3c.form/trunk/src/z3c/form/browser/object.txt
  A   z3c.form/trunk/src/z3c/form/browser/object.zcml
  A   z3c.form/trunk/src/z3c/form/browser/object_display.pt
  A   z3c.form/trunk/src/z3c/form/browser/object_input.pt
  U   z3c.form/trunk/src/z3c/form/browser/tests.py
  U   z3c.form/trunk/src/z3c/form/browser/widget.py
  U   z3c.form/trunk/src/z3c/form/configure.zcml
  U   z3c.form/trunk/src/z3c/form/converter.py
  U   z3c.form/trunk/src/z3c/form/error.py
  U   z3c.form/trunk/src/z3c/form/field.py
  U   z3c.form/trunk/src/z3c/form/field.txt
  U   z3c.form/trunk/src/z3c/form/form.py
  U   z3c.form/trunk/src/z3c/form/hint.py
  U   z3c.form/trunk/src/z3c/form/hint.txt
  U   z3c.form/trunk/src/z3c/form/interfaces.py
  A   z3c.form/trunk/src/z3c/form/object-caveat.txt
  A   z3c.form/trunk/src/z3c/form/object.py
  A   z3c.form/trunk/src/z3c/form/object.zcml
  U   z3c.form/trunk/src/z3c/form/testing.py
  U   z3c.form/trunk/src/z3c/form/widget.py
  U   z3c.form/trunk/src/z3c/form/widget.txt
  U   z3c.form/trunk/src/z3c/form/zcml.txt

-=-
Modified: z3c.form/trunk/CHANGES.txt
===================================================================
--- z3c.form/trunk/CHANGES.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/CHANGES.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -8,6 +8,16 @@
 - Feature: When no file is specified in the file upload widget, instead of
   overwriting the value with a missing one, the old data is retained.
 
+- Feature: Added a new flag ``ignoreContext`` to the form field, so that one
+  can individually select which fields should and which ones should not ignore
+  the context.
+
+- Feature: Allow raw request values of sequence widgets to be non-sequence
+  values, which makes integration with Javascript libraries easier.
+
+- Feature: Preliminary support for widgets of schema.IObject.
+  There is a big caveat, read: object-caveat.txt
+
 - Feature: Added support in the file upload widget's testing flavor to specify
   'base64'-encoded strings in the hidden text area, so that binary data can be
   uploaded as well.

Modified: z3c.form/trunk/buildout.cfg
===================================================================
--- z3c.form/trunk/buildout.cfg	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/buildout.cfg	2008-11-21 09:14:51 UTC (rev 93230)
@@ -7,6 +7,8 @@
 [versions]
 lxml = 2.1.1
 zope.app.locales = 3.4.5
+z3c.pt >= 1.0b1
+z3c.pt.compat = 0.3
 
 [test-environment]
 CHAMELEON_DEBUG = False

Modified: z3c.form/trunk/src/z3c/form/README.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/README.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/README.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -92,7 +92,11 @@
   form-based code in unit tests. It also provides components that simplify
   testing in testbrowser and Selenium.
 
+- ``object-caveat.txt`` [informative]
 
+  Explains the current problems of ObjectWidget.
+
+
 Browser Documentation
 ---------------------
 
@@ -107,4 +111,3 @@
 
   Each field name documentation file comprehensively explains the widget and
   how it is ensured to work properly.
-

Modified: z3c.form/trunk/src/z3c/form/browser/configure.zcml
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/configure.zcml	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/browser/configure.zcml	2008-11-21 09:14:51 UTC (rev 93230)
@@ -16,5 +16,6 @@
   <include file="text.zcml" />
   <include file="textarea.zcml" />
   <include file="textlines.zcml" />
+  <include file="object.zcml" />
 
 </configure>

Modified: z3c.form/trunk/src/z3c/form/browser/image.py
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/image.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/browser/image.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -34,7 +34,7 @@
     src = FieldProperty(IHTMLImageWidget['src'])
     klass = u'image-widget'
 
-    def extract(self, default=interfaces.NOVALUE):
+    def extract(self, default=interfaces.NOVALUE, setErrors=True):
         """See z3c.form.interfaces.IWidget."""
         if self.name + '.x' not in self.request:
             return default

Copied: z3c.form/trunk/src/z3c/form/browser/object.py (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/browser/object.py)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/object.py	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/object.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,37 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ObjectWidget browser related classes
+
+$Id$
+"""
+
+__docformat__ = "reStructuredText"
+import zope.component
+import zope.interface
+import zope.schema.interfaces
+
+from z3c.form import interfaces, object
+from z3c.form.widget import FieldWidget
+from z3c.form.browser import widget
+
+class ObjectWidget(widget.HTMLFormElement, object.ObjectWidget):
+    zope.interface.implements(interfaces.IObjectWidget)
+
+    klass = u'object-widget'
+
+ at zope.component.adapter(zope.schema.interfaces.IObject, interfaces.IFormLayer)
+ at zope.interface.implementer(interfaces.IFieldWidget)
+def ObjectFieldWidget(field, request):
+    """IFieldWidget factory for IObjectWidget."""
+    return FieldWidget(field, ObjectWidget(request))

Copied: z3c.form/trunk/src/z3c/form/browser/object.txt (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/browser/object.txt)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/object.txt	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/object.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,1169 @@
+============
+ObjectWidget
+============
+
+The ObjectWidget is about rendering a schema's fields as a single widget.
+
+
+There are way too many preconditions to exercise the ObjectWidget as it
+relies heavily on the from and widget framework.
+It renders the sub-widgets in a sub-form.
+
+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()
+
+  >>> import zope.component
+
+  >>> from z3c.form import datamanager
+  >>> zope.component.provideAdapter(datamanager.DictionaryField)
+
+
+  >>> from z3c.form.testing import IMySubObject
+  >>> from z3c.form.testing import IMySecond
+  >>> from z3c.form.testing import MySubObject
+  >>> from z3c.form.testing import MySecond
+
+  >>> from z3c.form.object import registerFactoryAdapter
+  >>> registerFactoryAdapter(IMySubObject, MySubObject)
+  >>> registerFactoryAdapter(IMySecond, MySecond)
+
+  >>> from z3c.form.object import SubformAdapter
+  >>> zope.component.provideAdapter(SubformAdapter)
+
+
+  >>> from z3c.form.error import MultipleErrorViewSnippet
+  >>> zope.component.provideAdapter(MultipleErrorViewSnippet)
+
+
+As for all widgets, the objectwidget must provide the new ``IWidget``
+interface:
+
+  >>> from zope.interface.verify import verifyClass
+  >>> from z3c.form import interfaces
+  >>> from z3c.form.browser import object
+
+  >>> verifyClass(interfaces.IWidget, object.ObjectWidget)
+  True
+
+The widget can be instantiated only using the request:
+
+  >>> from z3c.form.testing import TestRequest
+  >>> request = TestRequest()
+  >>> widget = object.ObjectWidget(request)
+
+Before rendering the widget, one has to set the name and id of the widget:
+
+  >>> widget.id = 'widget-id'
+  >>> widget.name = 'widget.name'
+
+Also, stand-alone widgets need to ignore the context:
+
+  >>> widget.ignoreContext = True
+
+There is no life for ObjectWidget without a schema to render:
+
+  >>> widget.update()
+  Traceback (most recent call last):
+  ...
+  ValueError: <ObjectWidget 'widget.name'> .field is None, that's a blocking point
+
+This schema is specified by the field:
+
+  >>> from z3c.form.widget import FieldWidget
+  >>> from z3c.form.testing import IMySubObject
+  >>> import zope.schema
+  >>> field = zope.schema.Object(
+  ...     __name__='subobject',
+  ...     title=u'my object widget',
+  ...     schema=IMySubObject)
+
+Apply the field nicely with the helper:
+
+  >>> widget = FieldWidget(field, widget)
+
+We also need to register the templates for 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('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)
+
+We can now render the widget:
+
+  >>> widget.update()
+  >>> print widget.render()
+  <html>
+    <body>
+      <div class="object-widget required">
+        <div class="label">
+          <label for="subobject-widgets-foofield">
+            <span>My foo field</span>
+            <span class="required">*</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget required int-field"
+          id="subobject-widgets-foofield"
+          name="subobject.widgets.foofield"
+          type="text" value="1,111">
+        </div>
+        <div class="label">
+          <label for="subobject-widgets-barfield">
+            <span>My dear bar</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget int-field"
+          id="subobject-widgets-barfield"
+          name="subobject.widgets.barfield"
+          type="text" value="2,222">
+        </div>
+        <input name="subobject-empty-marker" type="hidden" value="1">
+      </div>
+    </body>
+  </html>
+
+
+As you see all sort of default values are rendered.
+
+Let's provide a more meaningful value:
+
+  >>> from z3c.form.testing import MySubObject
+  >>> v = MySubObject()
+  >>> v.foofield = 42
+  >>> v.barfield = 666
+  >>> v.__marker__ = "ThisMustStayTheSame"
+
+
+  >>> widget.ignoreContext = False
+  >>> widget.value = v
+
+  >>> widget.update()
+
+  >>> print widget.render()
+  <html>
+    <body>
+      <div class="object-widget required">
+        <div class="label">
+          <label for="subobject-widgets-foofield">
+            <span>My foo field</span>
+            <span class="required">*</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget required int-field"
+          id="subobject-widgets-foofield"
+          name="subobject.widgets.foofield"
+          type="text" value="42">
+        </div>
+        <div class="label">
+          <label for="subobject-widgets-barfield">
+            <span>My dear bar</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget int-field"
+          id="subobject-widgets-barfield"
+          name="subobject.widgets.barfield"
+          type="text" value="666">
+        </div>
+        <input name="subobject-empty-marker" type="hidden" value="1">
+      </div>
+    </body>
+  </html>
+
+The widget's value is NOVALUE until it gets a request:
+
+  >>> widget.value
+  <NOVALUE>
+
+Let's fill in some values via the request:
+
+  >>> widget.request = TestRequest(form={'subobject.widgets.foofield':u'2',
+  ...                                    'subobject.widgets.barfield':u'999',
+  ...                                    'subobject-empty-marker':u'1'})
+  >>> widget.update()
+  >>> print widget.render()
+  <html>
+    <body>
+      <div class="object-widget required">
+        <div class="label">
+          <label for="subobject-widgets-foofield">
+            <span>My foo field</span>
+            <span class="required">*</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget required int-field"
+          id="subobject-widgets-foofield"
+          name="subobject.widgets.foofield"
+          type="text" value="2">
+        </div>
+        <div class="label">
+          <label for="subobject-widgets-barfield">
+            <span>My dear bar</span>
+          </label>
+        </div>
+        <div class="widget">
+          <input class="text-widget int-field"
+          id="subobject-widgets-barfield"
+          name="subobject.widgets.barfield"
+          type="text" value="999">
+        </div>
+        <input name="subobject-empty-marker" type="hidden" value="1">
+      </div>
+    </body>
+  </html>
+
+Widget value comes from the request:
+
+  >>> wv = widget.value
+  >>> wv
+  {'foofield': 2, 'barfield': 999}
+
+But our object will not be modified, since there was no "apply"-like control.
+
+  >>> v
+  <z3c.form.testing.MySubObject object at ...>
+  >>> v.foofield
+  42
+  >>> v.barfield
+  666
+
+The marker must stay (we have to modify the same object):
+
+  >>> v.__marker__
+  'ThisMustStayTheSame'
+
+
+HMMMM.... do we to test error handling here?
+I'm tempted to leave it out as no widgets seem to do this.
+
+#Error handling is next. Let's use the value "bad" (an invalid integer literal)
+#as input for our internal (sub) widget.
+#
+#  >>> widget.request = TestRequest(form={'subobject.widgets.foofield':u'55',
+#  ...                                    'subobject.widgets.barfield':u'bad',
+#  ...                                    'subobject-empty-marker':u'1'})
+#
+#
+#  >>> widget.update()
+#  >>> print widget.render()
+#  <html>
+#    <body>
+#      <div class="object-widget required">
+#        <div class="label">
+#          <label for="subobject-widgets-foofield">
+#            <span>My foo field</span>
+#            <span class="required">*</span>
+#          </label>
+#        </div>
+#        <div class="widget">
+#          <input class="text-widget required int-field"
+#          id="subobject-widgets-foofield"
+#          name="subobject.widgets.foofield"
+#          type="text" value="55">
+#        </div>
+#        <div class="label">
+#          <label for="subobject-widgets-barfield">
+#            <span>My dear bar</span>
+#          </label>
+#        </div>
+#        <div class="error">
+#          <div class="error">The entered value is not a valid integer literal.</div>
+#        </div>
+#        <div class="widget">
+#          <input class="text-widget int-field"
+#          id="subobject-widgets-barfield"
+#          name="subobject.widgets.barfield"
+#          type="text" value="bad">
+#        </div>
+#        <input name="subobject-empty-marker" type="hidden" value="1">
+#      </div>
+#    </body>
+#  </html>
+#
+#Getting the widget value raises the widget errors:
+#
+#  >>> widget.value
+#  Traceback (most recent call last):
+#  ...
+#  MultipleErrors
+#
+#Our object still must retain the old values:
+#
+#  >>> v
+#  <z3c.form.testing.MySubObject object at ...>
+#  >>> v.foofield
+#  42
+#  >>> v.barfield
+#  666
+#  >>> v.__marker__
+#  'ThisMustStayTheSame'
+
+
+
+In forms
+========
+
+Do all that fun in add and edit forms too:
+
+
+We have to provide an adapter first:
+
+  >>> zope.component.provideAdapter(object.ObjectFieldWidget)
+  >>> import z3c.form.object
+  >>> zope.component.provideAdapter(z3c.form.object.ObjectConverter)
+
+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 str(event)
+  ...         if isinstance(event, zope.lifecycleevent.ObjectModifiedEvent):
+  ...             for attr in event.descriptions:
+  ...                 print attr.interface
+  ...                 print attr.attributes
+
+We define an interface containing a subobject, and an addform for it:
+
+  >>> from z3c.form import form, field
+  >>> from z3c.form.testing import MyObject, IMyObject
+
+Note, that creating an object will print some information about it:
+
+  >>> class MyAddForm(form.AddForm):
+  ...     fields = field.Fields(IMyObject)
+  ...     def create(self, data):
+  ...         print "MyAddForm.create", str(data)
+  ...         return MyObject(**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()
+  ['subobject', 'name']
+  >>> myaddform.widgets.values()
+  [<ObjectWidget 'form.widgets.subobject'>, <TextWidget 'form.widgets.name'>]
+
+The addform has our ObjectWidget which in turn contains a subform:
+
+  >>> myaddform.widgets['subobject'].subform
+  <z3c.form.object.ObjectSubForm object at ...>
+
+Which in turn contains the sub-widgets:
+
+  >>> myaddform.widgets['subobject'].subform.widgets.keys()
+  ['foofield', 'barfield']
+
+If we want to render the addform, we must give it a template:
+
+  >>> import os
+  >>> from zope.app.pagetemplate import viewpagetemplatefile
+  >>> from z3c.form import tests
+  >>> def addTemplate(form):
+  ...     form.template = viewpagetemplatefile.BoundPageTemplate(
+  ...         viewpagetemplatefile.ViewPageTemplateFile(
+  ...             'simple_edit.pt', os.path.dirname(tests.__file__)), form)
+  >>> addTemplate(myaddform)
+
+Now rendering the addform renders the subform as well:
+
+  >>> print myaddform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="1,111">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="2,222">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name" name="form.widgets.name" type="text" value="">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-add"
+          name="form.buttons.add" type="submit" value="Add">
+        </div>
+      </form>
+    </body>
+  </html>
+
+
+We don't have the object (yet) in the root:
+
+  >>> root['first']
+  Traceback (most recent call last):
+  ...
+  KeyError: 'first'
+
+Let's try to add an object:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'66',
+  ...     'form.widgets.subobject.widgets.barfield':u'99',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.add':'Add'})
+  >>> myaddform.request = request
+
+  >>> myaddform.update()
+  MyAddForm.create {'subobject': <z3c.form.testing.MySubObject object at ...>,
+  'name': u'first'}
+
+Wow, it got added:
+
+  >>> root['first']
+  <z3c.form.testing.MyObject object at ...>
+
+  >>> root['first'].subobject
+  <z3c.form.testing.MySubObject object at ...>
+
+Field values need to be right:
+
+  >>> root['first'].subobject.foofield
+  66
+  >>> root['first'].subobject.barfield
+  99
+
+Let's see our event log:
+
+  >>> len(eventlog)
+  4
+
+  >>> printEvents()
+  <zope.app.event.objectevent.ObjectCreatedEvent object at ...>
+  <zope.app.event.objectevent.ObjectModifiedEvent object at ...>
+  <InterfaceClass z3c.form.testing.IMySubObject>
+  ('foofield', 'barfield')
+  <zope.app.event.objectevent.ObjectCreatedEvent object at ...>
+  <zope.app.container.contained.ContainerModifiedEvent object at ...>
+
+  >>> eventlog=[]
+
+Let's try to edit that newly added object:
+
+  >>> class MyEditForm(form.EditForm):
+  ...     fields = field.Fields(IMyObject)
+
+  >>> editform = MyEditForm(root['first'], TestRequest())
+  >>> addTemplate(editform)
+  >>> editform.update()
+
+Watch for the widget values in the HTML:
+
+  >>> print editform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="66">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="99">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name"
+          name="form.widgets.name" type="text" value="first">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-apply"
+          name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+Let's modify the values:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'43',
+  ...     'form.widgets.subobject.widgets.barfield':u'55',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.apply':'Apply'})
+
+They are still the same:
+
+  >>> root['first'].subobject.foofield
+  66
+  >>> root['first'].subobject.barfield
+  99
+
+  >>> editform.request = request
+  >>> editform.update()
+
+Until we have updated the form:
+
+  >>> root['first'].subobject.foofield
+  43
+  >>> root['first'].subobject.barfield
+  55
+
+Let's see our event log:
+
+  >>> len(eventlog)
+  2
+
+  >>> printEvents()
+  <zope.app.event.objectevent.ObjectModifiedEvent object at ...>
+  <InterfaceClass z3c.form.testing.IMySubObject>
+  ('foofield', 'barfield')
+  <zope.app.event.objectevent.ObjectModifiedEvent object at ...>
+  <InterfaceClass z3c.form.testing.IMyObject>
+  ('subobject',)
+
+  >>> eventlog=[]
+
+
+After the update the form says that the values got updated and renders the new
+values:
+
+  >>> print editform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <i>Data successfully updated.</i>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="43">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="55">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name"
+          name="form.widgets.name" type="text" value="first">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-apply"
+          name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+
+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'].__marker__ = "ThisMustStayTheSame"
+
+  >>> root['first'].subobject.foofield
+  43
+  >>> root['first'].subobject.barfield
+  55
+
+Let's modify the values:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'666',
+  ...     'form.widgets.subobject.widgets.barfield':u'999',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.apply':'Apply'})
+
+  >>> editform.request = request
+
+  >>> editform.update()
+
+Until we have updated the form:
+
+  >>> root['first'].subobject.foofield
+  666
+  >>> root['first'].subobject.barfield
+  999
+  >>> root['first'].__marker__
+  'ThisMustStayTheSame'
+
+
+Let's make a nasty error, by typing 'bad' instead of an integer:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'99',
+  ...     'form.widgets.subobject.widgets.barfield':u'bad',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.apply':'Apply'})
+
+  >>> editform.request = request
+  >>> eventlog=[]
+  >>> editform.update()
+
+Eventlog must be clean:
+
+  >>> len(eventlog)
+  0
+
+Watch for the error message in the HTML:
+it has to appear at the field itself and at the top of the form:
+
+  >>> print editform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <i>There were some errors.</i>
+      <ul>
+        <li>
+        my object:
+          <div class="error">The entered value is not a valid integer literal.</div>
+        </li>
+      </ul>
+      <form action=".">
+        <div class="row">
+          <b>
+            <div class="error">The entered value is not a valid integer literal.</div>
+          </b>
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="99">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="error">
+              <div class="error">The entered value is not a valid integer literal.</div>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="bad">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name"
+          name="form.widgets.name" type="text" value="first">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-apply"
+          name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+The object values must stay at the old ones:
+
+  >>> root['first'].subobject.foofield
+  666
+  >>> root['first'].subobject.barfield
+  999
+
+Let's make more errors:
+Now we enter 'bad' and '999999', where '999999' hits the upper limit of the field.
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'999999',
+  ...     'form.widgets.subobject.widgets.barfield':u'bad',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.apply':'Apply'})
+
+  >>> editform.request = request
+  >>> editform.update()
+
+Both errors must appear at the top of the form:
+
+  >>> print editform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <i>There were some errors.</i>
+      <ul>
+        <li>
+        my object:
+          <div class="error">Value is too big</div>
+          <div class="error">The entered value is not a valid integer literal.</div>
+        </li>
+      </ul>
+      <form action=".">
+        <div class="row">
+          <b>
+            <div class="error">Value is too big</div>
+            <div class="error">The entered value is not a valid integer literal.</div>
+          </b>
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="error">
+              <div class="error">Value is too big</div>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="999999">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="error">
+              <div class="error">The entered value is not a valid integer literal.</div>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="bad">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name"
+          name="form.widgets.name" type="text" value="first">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-apply"
+          name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+And of course, the object values do not get modified:
+
+  >>> root['first'].subobject.foofield
+  666
+  >>> root['first'].subobject.barfield
+  999
+
+
+#Let's make an other error, by typing a newline into name:
+#
+#  >>> request = TestRequest(form={
+#  ...     'form.widgets.subobject.widgets.foofield':u'1',
+#  ...     'form.widgets.subobject.widgets.barfield':u'2',
+#  ...     'form.widgets.name':u'first\nline',
+#  ...     'form.widgets.subobject-empty-marker':u'1',
+#  ...     'form.buttons.apply':'Apply'})
+#
+#  >>> editform.request = request
+#  >>> eventlog=[]
+#  >>> editform.update()
+
+THIS IS BAD:
+
+  #>>> len(eventlog)
+  #0
+  #>>> printEvents()
+  #<zope.app.event.objectevent.ObjectModifiedEvent object at 0x97f688c>
+  #<InterfaceClass z3c.form.testing.IMySubObject>
+  #('foofield', 'barfield')
+
+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()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <span class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield">666</span>
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <span class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield">999</span>
+            </div>
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <span class="text-widget required textline-field"
+          id="form-widgets-name">first</span>
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field"
+          id="form-buttons-apply" name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+
+
+
+Editforms might use dicts as context:
+
+  >>> newsub = MySubObject()
+  >>> newsub.foofield = 78
+  >>> newsub.barfield = 87
+
+  >>> class MyEditFormDict(form.EditForm):
+  ...     fields = field.Fields(IMyObject)
+  ...     def getContent(self):
+  ...         return {'subobject': newsub, 'name': u'blooki'}
+
+  >>> editform = MyEditFormDict(None, TestRequest())
+  >>> addTemplate(editform)
+  >>> editform.update()
+
+Watch for the widget values in the HTML:
+
+  >>> print editform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+              id="form-widgets-subobject-widgets-foofield"
+              name="form.widgets.subobject.widgets.foofield"
+              type="text" value="78">
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+              id="form-widgets-subobject-widgets-barfield"
+              name="form.widgets.subobject.widgets.barfield"
+              type="text" value="87">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden"
+            value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field" id="form-widgets-name"
+          name="form.widgets.name" type="text" value="blooki">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-apply"
+          name="form.buttons.apply" type="submit" value="Apply">
+        </div>
+      </form>
+    </body>
+  </html>
+
+Let's modify the values:
+
+  >>> request = TestRequest(form={
+  ...     'form.widgets.subobject.widgets.foofield':u'43',
+  ...     'form.widgets.subobject.widgets.barfield':u'55',
+  ...     'form.widgets.name':u'first',
+  ...     'form.widgets.subobject-empty-marker':u'1',
+  ...     'form.buttons.apply':'Apply'})
+
+They are still the same:
+
+  >>> newsub.foofield
+  78
+  >>> newsub.barfield
+  87
+
+Until updating the form:
+
+  >>> editform.request = request
+  >>> eventlog=[]
+  >>> editform.update()
+
+  >>> newsub.foofield
+  43
+  >>> newsub.barfield
+  55
+
+  >>> len(eventlog)
+  2
+  >>> printEvents()
+  <zope.app.event.objectevent.ObjectModifiedEvent object at ...>
+  <InterfaceClass z3c.form.testing.IMySubObject>
+  ('foofield', 'barfield')
+  <zope.app.event.objectevent.ObjectModifiedEvent object at ...>
+  <InterfaceClass z3c.form.testing.IMyObject>
+  ('subobject', 'name')
+
+
+
+Object in an Object situation
+=============================
+
+
+We define an interface containing a subobject, and an addform for it:
+
+  >>> from z3c.form import form, field
+  >>> from z3c.form.testing import MyComplexObject, IMyComplexObject
+
+Note, that creating an object will print some information about it:
+
+  >>> class MyAddForm(form.AddForm):
+  ...     fields = field.Fields(IMyComplexObject)
+  ...     def create(self, data):
+  ...         print "MyAddForm.create", str(data)
+  ...         return MyObject(**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()
+
+#>>> from pub.dbgpclient import brk; brk('192.168.32.1')
+
+  >>> myaddform =  MyAddForm(root, request)
+
+#>>> from pub.dbgpclient import brk; brk('192.168.32.1')
+
+
+  >>> myaddform.update()
+
+As usual, the form contains a widget manager with the expected widget
+
+  >>> myaddform.widgets.keys()
+  ['subobject', 'name']
+  >>> myaddform.widgets.values()
+  [<ObjectWidget 'form.widgets.subobject'>, <TextWidget 'form.widgets.name'>]
+
+The addform has our ObjectWidget which in turn contains a subform:
+
+  >>> myaddform.widgets['subobject'].subform
+  <z3c.form.object.ObjectSubForm object at ...>
+
+Which in turn contains the sub-widgets:
+
+  >>> myaddform.widgets['subobject'].subform.widgets.keys()
+  ['subfield', 'moofield']
+
+  >>> myaddform.widgets['subobject'].subform.widgets['subfield'].subform.widgets.keys()
+  ['foofield', 'barfield']
+
+If we want to render the addform, we must give it a template:
+
+  >>> addTemplate(myaddform)
+
+Now rendering the addform renders the subform as well:
+
+  >>> print myaddform.render()
+  <html xmlns="http://www.w3.org/1999/xhtml">
+    <body>
+      <form action=".">
+        <div class="row">
+          <label for="form-widgets-subobject">my object</label>
+          <div class="object-widget required">
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-subfield">
+                <span>Second-subobject</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <div class="object-widget required">
+                <div class="label">
+                  <label for="form-widgets-subobject-widgets-subfield-widgets-foofield">
+                    <span>My foo field</span>
+                    <span class="required">*</span>
+                  </label>
+                </div>
+                <div class="widget">
+                  <input class="text-widget required int-field"
+                  id="form-widgets-subobject-widgets-subfield-widgets-foofield"
+                  name="form.widgets.subobject.widgets.subfield.widgets.foofield"
+                  type="text" value="1,111">
+                </div>
+                <div class="label">
+                  <label for="form-widgets-subobject-widgets-subfield-widgets-barfield">
+                    <span>My dear bar</span>
+                  </label>
+                </div>
+                <div class="widget">
+                  <input class="text-widget int-field"
+                  id="form-widgets-subobject-widgets-subfield-widgets-barfield"
+                  name="form.widgets.subobject.widgets.subfield.widgets.barfield"
+                  type="text" value="2,222">
+                </div>
+                <input name="form.widgets.subobject.widgets.subfield-empty-marker" type="hidden" value="1">
+              </div>
+            </div>
+            <div class="label">
+              <label for="form-widgets-subobject-widgets-moofield">
+                <span>Something</span>
+                <span class="required">*</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget required textline-field"
+              id="form-widgets-subobject-widgets-moofield"
+              name="form.widgets.subobject.widgets.moofield" type="text" value="">
+            </div>
+            <input name="form.widgets.subobject-empty-marker" type="hidden" value="1">
+          </div>
+        </div>
+        <div class="row">
+          <label for="form-widgets-name">name</label>
+          <input class="text-widget required textline-field"
+          id="form-widgets-name" name="form.widgets.name" type="text" value="">
+        </div>
+        <div class="action">
+          <input class="submit-widget button-field" id="form-buttons-add" name="form.buttons.add" type="submit" value="Add">
+        </div>
+      </form>
+    </body>
+  </html>

Copied: z3c.form/trunk/src/z3c/form/browser/object.zcml (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/browser/object.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/object.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/object.zcml	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,38 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:z3c="http://namespaces.zope.org/z3c"
+    i18n_domain="z3c.form">
+
+  <class class=".object.ObjectWidget">
+    <require
+        permission="zope.Public"
+        interface="z3c.form.interfaces.IObjectWidget"
+        />
+  </class>
+
+<adapter
+      factory=".object.ObjectFieldWidget"
+      for="zope.schema.interfaces.IObject
+           z3c.form.interfaces.IFormLayer"
+      />
+
+<z3c:widgetTemplate
+      mode="input"
+      widget="z3c.form.interfaces.IObjectWidget"
+      layer="z3c.form.interfaces.IFormLayer"
+      template="object_input.pt"
+      />
+<z3c:widgetTemplate
+      mode="hidden"
+      widget="z3c.form.interfaces.IObjectWidget"
+      layer="z3c.form.interfaces.IFormLayer"
+      template="object_input.pt"
+      />
+<z3c:widgetTemplate
+      mode="display"
+      widget="z3c.form.interfaces.IObjectWidget"
+      layer="z3c.form.interfaces.IFormLayer"
+      template="object_display.pt"
+      />
+
+</configure>

Copied: z3c.form/trunk/src/z3c/form/browser/object_display.pt (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/browser/object_display.pt)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/object_display.pt	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/object_display.pt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,16 @@
+<div class="object-widget"
+     tal:attributes="class view/klass">
+    <tal:block repeat="widget view/subform/widgets/values">
+        <div class="label">
+          <label tal:attributes="for widget/id">
+            <span i18n:translate=""
+                tal:content="widget/label">label</span>
+            <span class="required"
+                  tal:condition="widget/required">*</span>
+          </label>
+        </div>
+        <div class="widget" tal:content="structure widget/render">
+          <input type="text" size="24" value="" />
+        </div>
+    </tal:block>
+</div>
\ No newline at end of file

Copied: z3c.form/trunk/src/z3c/form/browser/object_input.pt (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/browser/object_input.pt)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/object_input.pt	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/object_input.pt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,22 @@
+<div class="object-widget"
+     tal:attributes="class view/klass">
+    <tal:block repeat="widget view/subform/widgets/values">
+        <div class="label">
+          <label tal:attributes="for widget/id">
+            <span i18n:translate=""
+                tal:content="widget/label">label</span>
+            <span class="required"
+                  tal:condition="widget/required">*</span>
+          </label>
+        </div>
+        <div class="error"
+             tal:condition="widget/error">
+          <span tal:replace="structure widget/error/render">error</span>
+        </div>
+        <div class="widget" tal:content="structure widget/render">
+          <input type="text" size="24" value="" />
+        </div>
+    </tal:block>
+    <input name="field-empty-marker" type="hidden" value="1"
+           tal:attributes="name string:${view/name}-empty-marker" />
+</div>
\ No newline at end of file

Modified: z3c.form/trunk/src/z3c/form/browser/tests.py
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/tests.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/browser/tests.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -91,6 +91,11 @@
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
                      checker=checker,
                      ),
+        DocFileSuite('object.txt',
+                     setUp=setUp, tearDown=testing.tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     checker=checker,
+                     ),
         DocFileSuite('multi.txt',
                      setUp=setUp, tearDown=testing.tearDown,
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,

Modified: z3c.form/trunk/src/z3c/form/browser/widget.py
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/widget.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/browser/widget.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -54,7 +54,16 @@
         if not self.klass:
             self.klass = unicode(klass)
         else:
-            self.klass += u' ' + unicode(klass)
+            #make sure items are not repeated
+            parts = self.klass.split()+[unicode(klass)]
+            seen = {}
+            unique = []
+            for item in parts:
+                if item in seen:
+                    continue
+                seen[item]=1
+                unique.append(item)
+            self.klass = u' '.join(unique)
 
     def update(self):
         """See z3c.form.interfaces.IWidget"""

Modified: z3c.form/trunk/src/z3c/form/configure.zcml
===================================================================
--- z3c.form/trunk/src/z3c/form/configure.zcml	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/configure.zcml	2008-11-21 09:14:51 UTC (rev 93230)
@@ -122,6 +122,11 @@
       permission="zope.Public"
       />
   <adapter
+      factory=".error.MultipleErrorViewSnippet"
+      trusted="True"
+      permission="zope.Public"
+      />
+  <adapter
       factory=".error.StandardErrorViewTemplate"
       />
 
@@ -204,6 +209,8 @@
         />
   </configure>
 
+  <include file="object.zcml" />
+
   <include package=".browser" />
 
   <i18n:registerTranslations directory="locales" />

Modified: z3c.form/trunk/src/z3c/form/converter.py
===================================================================
--- z3c.form/trunk/src/z3c/form/converter.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/converter.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -341,7 +341,7 @@
         """Just dispatch it."""
         if value is self.field.missing_value:
             return []
-        # We relay on the default registered widget, this is probably a 
+        # We relay on the default registered widget, this is probably a
         # restriction for custom widgets. If so use your own MultiWidget and
         # register your own converter which will get the right widget for the
         # used value_type.
@@ -361,7 +361,7 @@
             return self.field.missing_value
         valueType = self.field.value_type._type
         values = [valueType(v) for v in value]
-        # convert the field values to a tuple or list 
+        # convert the field values to a tuple or list
         return collectionType(values)
 
 

Modified: z3c.form/trunk/src/z3c/form/error.py
===================================================================
--- z3c.form/trunk/src/z3c/form/error.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/error.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -101,6 +101,26 @@
         return self.error.args[0]
 
 
+class MultipleErrorViewSnippet(ErrorViewSnippet):
+    """Error view snippet for multiple errors."""
+    zope.component.adapts(
+        interfaces.IMultipleErrors, None, None, None, None, None)
+
+    def update(self):
+        pass
+
+    def render(self):
+        return ''.join([view.render() for view in self.error.errors])
+
+
+class MultipleErrors(Exception):
+    zope.interface.implements(interfaces.IMultipleErrors)
+    """An error that contains many errors"""
+
+    def __init__(self, errors):
+        self.errors = errors
+
+
 class ErrorViewTemplateFactory(object):
     """Error view template factory."""
 

Modified: z3c.form/trunk/src/z3c/form/field.py
===================================================================
--- z3c.form/trunk/src/z3c/form/field.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/field.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -21,7 +21,8 @@
 import zope.location
 import zope.schema.interfaces
 
-from z3c.form import interfaces, util
+from z3c.form import interfaces, util, error
+from z3c.form.error import MultipleErrors
 from z3c.form.widget import AfterWidgetUpdateEvent
 
 
@@ -65,7 +66,8 @@
 
     widgetFactory = WidgetFactoryProperty()
 
-    def __init__(self, field, name=None, prefix='', mode=None, interface=None):
+    def __init__(self, field, name=None, prefix='', mode=None, interface=None,
+                 ignoreContext=None):
         self.field = field
         if name is None:
             name = field.__name__
@@ -76,6 +78,7 @@
         if interface is None:
             interface = field.interface
         self.interface = interface
+        self.ignoreContext = ignoreContext
 
     def __repr__(self):
         return '<%s %r>' % (self.__class__.__name__, self.__name__)
@@ -220,13 +223,17 @@
         prefix += util.expandPrefix(self.prefix)
         # Walk through each field, making a widget out of it.
         for field in self.form.fields.values():
+            # Step 0. Determine whether the context should be ignored.
+            ignoreContext = self.ignoreContext
+            if field.ignoreContext is not None:
+                ignoreContext = field.ignoreContext
             # Step 1: Determine the mode of the widget.
             mode = self.mode
             if field.mode is not None:
                 mode = field.mode
             elif field.field.readonly and not self.ignoreReadonly:
                     mode = interfaces.DISPLAY_MODE
-            elif not self.ignoreContext:
+            elif not ignoreContext:
                 # If we do not have enough permissions to write to the
                 # attribute, then switch to display mode.
                 dm = zope.component.getMultiAdapter(
@@ -253,7 +260,7 @@
             zope.interface.alsoProvides(
                 widget, interfaces.IContextAware, interfaces.IFormAware)
             # Step 6: Set some variables
-            widget.ignoreContext = self.ignoreContext
+            widget.ignoreContext = ignoreContext
             widget.ignoreRequest = self.ignoreRequest
             # Step 7: Set the mode of the widget
             widget.mode = mode
@@ -273,9 +280,9 @@
         for name, widget in self.items():
             if widget.mode == interfaces.DISPLAY_MODE:
                 continue
-            raw = widget.extract()
             value = widget.field.missing_value
             try:
+                raw = widget.extract(setErrors=setErrors)
                 if raw is not interfaces.NOVALUE:
                     value = interfaces.IDataConverter(widget).toFieldValue(raw)
                 zope.component.getMultiAdapter(
@@ -285,7 +292,8 @@
                      getattr(widget, 'field', None),
                      widget),
                     interfaces.IValidator).validate(value)
-            except (zope.schema.ValidationError, ValueError), error:
+            except (zope.schema.ValidationError,
+                    ValueError, MultipleErrors), error:
                 view = zope.component.getMultiAdapter(
                     (error, self.request, widget, widget.field,
                      self.form, self.content), interfaces.IErrorViewSnippet)

Modified: z3c.form/trunk/src/z3c/form/field.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/field.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/field.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -288,7 +288,23 @@
     >>> manager['country'].mode
     'display'
 
+* ``ignoreContext``
 
+  While the ``ignoreContext`` flag is usually set on the form, it is sometimes
+  desirable to set the flag for a particular field.
+
+    >>> manager = field.Fields(IPerson)
+    >>> manager['country'].ignoreContext
+
+    >>> manager = field.Fields(IPerson, ignoreContext=True)
+    >>> manager['country'].ignoreContext
+    True
+
+    >>> manager = field.Fields(IPerson, ignoreContext=False)
+    >>> manager['country'].ignoreContext
+    False
+
+
 Fields Widget Manager
 ---------------------
 
@@ -730,7 +746,31 @@
   >>> manager['lastName'].error is None
   True
 
+Customization of Ignoring the Context
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+Note that you can also manually control ignoring the context per field.
+
+  >>> class CustomPersonForm(object):
+  ...     prefix = 'form.'
+  ...     fields = field.Fields(IPerson).select('id')
+  ...     fields += field.Fields(IPerson, ignoreContext=True).select(
+  ...                   'firstName', 'lastName')
+  >>> customPersonForm = CustomPersonForm()
+
+Let's now create a manager and update it:
+
+  >>> customManager = field.FieldWidgets(customPersonForm, request, context)
+  >>> customManager.update()
+
+  >>> customManager['id'].ignoreContext
+  False
+  >>> customManager['firstName'].ignoreContext
+  True
+  >>> customManager['lastName'].ignoreContext
+  True
+
+
 Fields -- Custom Widget Factories
 ---------------------------------
 

Modified: z3c.form/trunk/src/z3c/form/form.py
===================================================================
--- z3c.form/trunk/src/z3c/form/form.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/form.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -44,7 +44,9 @@
         dm = zope.component.getMultiAdapter(
             (content, field.field), interfaces.IDataManager)
         # Only update the data, if it is different
-        if dm.get() != data[name]:
+        # Or it is an Object, in case we'll never know
+        if (dm.get() != data[name]
+            or zope.schema.interfaces.IObject.providedBy(field.field)):
             dm.set(data[name])
             # Record the change using information required later
             changes.setdefault(dm.field.interface, []).append(name)
@@ -128,9 +130,9 @@
         self.widgets.ignoreReadonly = self.ignoreReadonly
         self.widgets.update()
 
-    def extractData(self):
+    def extractData(self, setErrors=True):
         '''See interfaces.IForm'''
-        return self.widgets.extract()
+        return self.widgets.extract(setErrors=setErrors)
 
     def update(self):
         '''See interfaces.IForm'''

Modified: z3c.form/trunk/src/z3c/form/hint.py
===================================================================
--- z3c.form/trunk/src/z3c/form/hint.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/hint.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -29,7 +29,7 @@
 
     zope.interface.implements(interfaces.IValue)
     zope.component.adapts(zope.interface.Interface, interfaces.IFormLayer,
-        interfaces.IForm, zope.schema.interfaces.IField, interfaces.IWidget) 
+        interfaces.IForm, zope.schema.interfaces.IField, interfaces.IWidget)
 
     def __init__(self, context, request, form, field, widget):
         self.context = context

Modified: z3c.form/trunk/src/z3c/form/hint.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/hint.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/hint.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -1,16 +1,17 @@
-========================
+=====================
 Hint (title) Adapters
-========================
+=====================
 
-A widget can provide a hint. Hints are not a standard concept, the 
-implementations can be very different in each project. Hints are most the time
-implemented with JavaScript since the default ``input title`` hint in browsers
-ar almost unusable.
+A widget can provide a hint. Hints are not a standard concept, the
+implementations can be very different in each project. Hints are most
+of the time implemented with JavaScript since the default ``input
+title`` hint in browsers are almost unusable.
 
-Our hint support is limited and only offers some helpers. Which means we will
-offer an adapter for show the schema field description as title. Since this
-is very specific we only provide a ``FieldDescriptionAsHint`` adapter which
-you can configure as named IValue adapter.
+Our hint support is limited and only offers some helpers. Which means
+we will offer an adapter that shows the schema field description as
+the title. Since this is very specific we only provide a
+``FieldDescriptionAsHint`` adapter which you can configure as a named
+IValue adapter.
 
   >>> import zope.interface
   >>> import zope.component
@@ -26,16 +27,16 @@
 Let's create a couple of simple widgets and forms first:
 
   >>> class IContent(zope.interface.Interface):
-  ... 
+  ...
   ...     textLine = zope.schema.TextLine(
   ...         title=u'Title',
   ...         description=u'A TextLine description')
 
   >>> class Content(object):
   ...     zope.interface.implements(IContent)
-  ... 
+  ...
   ...     textLine = None
-  ... 
+  ...
   >>> content = Content()
 
   >>> from z3c.form.testing import TestRequest
@@ -75,14 +76,15 @@
 Check all fields
 ----------------
 
-Just make sure that all widgets get correct handled. This sample can be useful 
-if you need to implement a JavaScript based hint concept:
+Just to make sure that all the widgets are handled correctly, we will
+go trhough each of them. This sample can be useful if you need to
+implement a JavaScript based hint concept:
 
   >>> import datetime
   >>> import decimal
   >>> from zope.schema import vocabulary
 
-Let's setup a sample vocabulary:
+Let's setup a simple vocabulary:
 
   >>> vocab = vocabulary.SimpleVocabulary([
   ...     vocabulary.SimpleVocabulary.createTerm(1, '1', u'One'),
@@ -93,145 +95,145 @@
   ...     ])
 
   >>> class IAllInOne(zope.interface.Interface):
-  ... 
+  ...
   ...     asciiField = zope.schema.ASCII(
   ...         title=u'ASCII',
   ...         description=u'This is an ASCII field.',
   ...         default='This is\n ASCII.')
-  ... 
+  ...
   ...     asciiLineField = zope.schema.ASCIILine(
   ...         title=u'ASCII Line',
   ...         description=u'This is an ASCII-Line field.',
   ...         default='An ASCII line.')
-  ... 
+  ...
   ...     boolField = zope.schema.Bool(
   ...         title=u'Boolean',
   ...         description=u'This is a Bool field.',
   ...         default=True)
-  ... 
+  ...
   ...     checkboxBoolField = zope.schema.Bool(
   ...         title=u'Boolean (Checkbox)',
   ...         description=u'This is a Bool field displayed suing a checkbox.',
   ...         default=True)
-  ... 
+  ...
   ...     bytesLineField = zope.schema.BytesLine(
   ...         title=u'Bytes Line',
   ...         description=u'This is a bytes line field.',
   ...         default='A Bytes line.')
-  ... 
+  ...
   ...     choiceField = zope.schema.Choice(
   ...         title=u'Choice',
   ...         description=u'This is a choice field.',
   ...         default=3,
   ...         vocabulary=vocab)
-  ... 
+  ...
   ...     optionalChoiceField = zope.schema.Choice(
   ...         title=u'Choice (Not Required)',
   ...         description=u'This is a non-required choice field.',
   ...         vocabulary=vocab,
   ...         required=False)
-  ... 
+  ...
   ...     promptChoiceField = zope.schema.Choice(
   ...         title=u'Choice (Explicit Prompt)',
   ...         description=u'This is a choice field with an explicit prompt.',
   ...         vocabulary=vocab,
   ...         required=False)
-  ... 
+  ...
   ...     dateField = zope.schema.Date(
   ...         title=u'Date',
   ...         description=u'This is a Date field.',
   ...         default=datetime.date(2007, 4, 1))
-  ... 
+  ...
   ...     datetimeField = zope.schema.Datetime(
   ...         title=u'Date/Time',
   ...         description=u'This is a Datetime field.',
   ...         default=datetime.datetime(2007, 4, 1, 12))
-  ... 
+  ...
   ...     decimalField = zope.schema.Decimal(
   ...         title=u'Decimal',
   ...         description=u'This is a Decimal field.',
   ...         default=decimal.Decimal('12.87'))
-  ... 
+  ...
   ...     dottedNameField = zope.schema.DottedName(
   ...         title=u'Dotted Name',
   ...         description=u'This is a DottedName field.',
   ...         default='z3c.form')
-  ... 
+  ...
   ...     floatField = zope.schema.Float(
   ...         title=u'Float',
   ...         description=u'This is a Float field.',
   ...         default=12.8)
-  ... 
+  ...
   ...     frozenSetField = zope.schema.FrozenSet(
   ...         title=u'Frozen Set',
   ...         description=u'This is a FrozenSet field.',
   ...         value_type=choiceField,
   ...         default=frozenset([1, 3]) )
-  ... 
+  ...
   ...     idField = zope.schema.Id(
   ...         title=u'Id',
   ...         description=u'This is a Id field.',
   ...         default='z3c.form')
-  ... 
+  ...
   ...     intField = zope.schema.Int(
   ...         title=u'Integer',
   ...         description=u'This is a Int field.',
   ...         default=12345)
-  ... 
+  ...
   ...     listField = zope.schema.List(
   ...         title=u'List',
   ...         description=u'This is a List field.',
   ...         value_type=choiceField,
   ...         default=[1, 3])
-  ... 
+  ...
   ...     passwordField = zope.schema.Password(
   ...         title=u'Password',
   ...         description=u'This is a Password field.',
   ...         default=u'mypwd',
   ...         required=False)
-  ... 
+  ...
   ...     setField = zope.schema.Set(
   ...         title=u'Set',
   ...         description=u'This is a Set field.',
   ...         value_type=choiceField,
   ...         default=set([1, 3]) )
-  ... 
+  ...
   ...     sourceTextField = zope.schema.SourceText(
   ...         title=u'Source Text',
   ...         description=u'This is a SourceText field.',
   ...         default=u'<source />')
-  ... 
+  ...
   ...     textField = zope.schema.Text(
   ...         title=u'Text',
   ...         description=u'This is a Text field.',
   ...         default=u'Some\n Text.')
-  ... 
+  ...
   ...     textLineField = zope.schema.TextLine(
   ...         title=u'Text Line',
   ...         description=u'This is a TextLine field.',
   ...         default=u'Some Text line.')
-  ... 
+  ...
   ...     timeField = zope.schema.Time(
   ...         title=u'Time',
   ...         description=u'This is a Time field.',
   ...         default=datetime.time(12, 0))
-  ... 
+  ...
   ...     timedeltaField = zope.schema.Timedelta(
   ...         title=u'Time Delta',
   ...         description=u'This is a Timedelta field.',
   ...         default=datetime.timedelta(days=3))
-  ... 
+  ...
   ...     tupleField = zope.schema.Tuple(
   ...         title=u'Tuple',
   ...         description=u'This is a Tuple field.',
   ...         value_type=choiceField,
   ...         default=(1, 3))
-  ... 
+  ...
   ...     uriField = zope.schema.URI(
   ...         title=u'URI',
   ...         description=u'This is a URI field.',
   ...         default='http://zope.org')
-  ... 
+  ...
   ...     hiddenField = zope.schema.TextLine(
   ...         title=u'Hidden Text Line',
   ...         description=u'This is a hidden TextLine field.',
@@ -239,7 +241,7 @@
 
   >>> class AllInOne(object):
   ...     zope.interface.implements(IAllInOne)
-  ... 
+  ...
   ...     asciiField = None
   ...     asciiLineField = None
   ...     boolField = None
@@ -561,7 +563,7 @@
          class="text-widget required time-field"
          title="This is a Time field." value="12:00"
          type="text" />
- 
+
   >>> print allInOneForm.widgets['timedeltaField'].render()
   <input id="form-widgets-timedeltaField"
          name="form.widgets.timedeltaField"

Modified: z3c.form/trunk/src/z3c/form/interfaces.py
===================================================================
--- z3c.form/trunk/src/z3c/form/interfaces.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/interfaces.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -158,7 +158,11 @@
     def render():
         """Render view."""
 
+class IMultipleErrors(zope.interface.Interface):
+    """An error that contains many errors"""
 
+    errors = zope.interface.Attribute("List of errors")
+
 # ----[ Fields ]--------------------------------------------------------------
 
 class IField(zope.interface.Interface):
@@ -189,11 +193,11 @@
         description=_('The interface from which the field is coming.'),
         required=True)
 
-    dataProvider = zope.schema.Field(
-        title=_('Data Provider'),
-        description=_('The component providing the data of the field for '
-                      'the widget.'),
-        required=True)
+    ignoreContext = zope.schema.Bool(
+        title=_('Ignore Context'),
+        description=_('A flag, when set, forces the widget not to look at '
+                      'the context for a value.'),
+        required=False)
 
     widgetFactory = zope.schema.Field(
         title=_('Widget Factory'),
@@ -325,6 +329,30 @@
         required=False)
 
 
+# ----[ Object factory ]-----------------------------------------------------
+
+class IObjectFactory(zope.interface.Interface):
+    """Factory that will instatiate our objects for ObjectWidget
+    It could also pre-populate properties as it gets the values extracted
+    from the form passed in ``value``
+    """
+
+    def __call__(value):
+        """return a default object created to be populated
+        """
+
+
+# ----[ Subform factory ]-----------------------------------------------------
+
+class ISubformFactory(zope.interface.Interface):
+    """Factory that will instatiate our subforms for ObjectWidget
+    """
+
+    def __call__():
+        """return a default object created to be populated
+        """
+
+
 # ----[ Widgets ]------------------------------------------------------------
 
 class IWidget(ILocation):
@@ -380,7 +408,7 @@
         default=False,
         required=False)
 
-    def extract(default=NOVALUE):
+    def extract(default=NOVALUE, setErrors=True):
         """Extract the string value(s) of the widget from the form.
 
         The return value may be any Python construct, but is typically a
@@ -390,10 +418,12 @@
 
         If an error occurs during the extraction, the default value should be
         returned. Since this should never happen, if the widget is properly
-        designed and used, it is okay to not raise an error here, since we do
+        designed and used, it is okay to NOT raise an error here, since we do
         not want to crash the system during an inproper request.
 
         If there is no value to extract, the default is to be returned.
+
+        setErrors: needs to be passed on to possible sub-widgets
         """
 
     def update():
@@ -512,6 +542,8 @@
 class IPasswordWidget(ITextWidget):
     """Password widget."""
 
+class IObjectWidget(IWidget):
+    """Object widget."""
 
 class IWidgets(IManager):
     """A widget manager"""
@@ -556,8 +588,11 @@
     def update():
         """Setup widgets."""
 
-    def extract():
+    def extract(setErrors=True):
         """Extract the values from the widgets and validate them.
+
+        setErrors: decides whether to set errors on self and on the widgets
+                   also needs to be passed on to sub-widgets
         """
 
 
@@ -817,9 +852,11 @@
         mainly meant to be a hook for subclasses.
         '''
 
-    def extractData():
-        '''Extract the data of the form.'''
+    def extractData(setErrors=True):
+        '''Extract the data of the form.
 
+        setErrors: needs to be passed to extract() and to sub-widgets'''
+
     def update():
         '''Update the form.'''
 

Copied: z3c.form/trunk/src/z3c/form/object-caveat.txt (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/object-caveat.txt)
===================================================================
--- z3c.form/trunk/src/z3c/form/object-caveat.txt	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/object-caveat.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,70 @@
+===================
+ObjectWidget caveat
+===================
+
+ObjectWidget itself seems to be fine, but we discovered a fundamental problem
+in z3.form.
+
+The meat is that widget value
+* validation
+* extraction
+* applying values
+need to be separated and made recursive-aware.
+
+Currently
+---------
+* There is a loop that extracts and validates each widgets value.
+  Then it moves on to the next widget in the same loop.
+* The problem is that the ObjectWidget MUST keep it's values in the object itself,
+  not in any dict or helper structure.
+  That means in case of a validation failure later in the loop the ObjectWidget's
+  values are already applied and cannot be reverted.
+* Also on a single level of widgets this loop might be OK,
+  because the loop is just flat.
+
+We need
+-------
+* To do a loop to validate ALL widget values.
+* To do a loop to extract ALL values. (maybe apply too, let's think about it...)
+* Then in a different loop apply those extracted (and validated) values.
+* Problem is that the current API does not support separate methods for that.
+* One more point is to take into account that with the ObjectWidget forms and
+  widgets can be _recursive_, that means there can be a
+  form-widgets-subform-widgets-subform-widgets level of widgets.
+
+
+An example:
+
+> The situation is the following:
+> - schema is like this:
+> class IMySubObject(zope.interface.Interface):
+>     foofield = zope.schema.Int(
+>         title=u"My foo field",
+>         default=1111,
+>         max=9999)
+>     barfield = zope.schema.Int(
+>         title=u"My dear bar",
+>         default=2222,
+>         required=False)
+> class IMyObject(zope.interface.Interface):
+>     subobject = zope.schema.Object(title=u'my object',
+>                                    schema=IMySubObject)
+>     name = zope.schema.TextLine(title=u'name')
+>
+> - on object editing
+> - we need to keep the (**old**) (IMySubObject) object in place
+>   -- do not create a new one
+> - value setting is done in the editform handleApply
+>   - extractData, extract needs to extract recursively
+>   - return assignable values
+>   - it has no idea about subobjects
+> - let's say the IMySubObject data is validated OK, but there's an
+>   error in IMyObject (with name)
+> - now the problem is:
+>   - IMyObject.subobject extract gets called first
+>     it sets the values on the existing object (and fires
+>     ObjectModifiedEvent)
+>   - IMyObject.name detects the error
+>     it does not set the value
+>   BUT IMyObject.subobject sticks to the extracted value that should be
+>   discarded, because the whole form did not validate?!?!?!

Copied: z3c.form/trunk/src/z3c/form/object.py (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/object.py)
===================================================================
--- z3c.form/trunk/src/z3c/form/object.py	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/object.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,316 @@
+##############################################################################
+#
+# Copyright (c) 2007 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""ObjectWidget related classes
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+import zope.i18n.format
+import zope.interface
+import zope.component
+import zope.schema
+import zope.event
+import zope.lifecycleevent
+from zope.security.proxy import removeSecurityProxy
+from z3c.form.converter import BaseDataConverter
+
+from z3c.form import form, interfaces, util, widget
+from z3c.form.field import Fields
+from z3c.form.error import MultipleErrors
+from z3c.form.i18n import MessageFactory as _
+
+def getIfName(iface):
+    return iface.__module__+'.'+iface.__name__
+
+class ObjectSubForm(form.BaseForm):
+    zope.interface.implements(interfaces.ISubForm)
+
+    def __init__(self, context, request, parentWidget):
+        self.context = context
+        self.request = request
+        self.__parent__ = parentWidget
+        self.parentForm = parentWidget.form
+
+    def _validate(self):
+        for widget in self.widgets.values():
+            try:
+                # convert widget value to field value
+                converter = interfaces.IDataConverter(widget)
+                value = converter.toFieldValue(widget.value)
+                # validate field value
+                zope.component.getMultiAdapter(
+                    (self.context,
+                     self.request,
+                     self.parentForm,
+                     getattr(widget, 'field', None),
+                     widget),
+                    interfaces.IValidator).validate(value)
+            except (zope.schema.ValidationError, ValueError), error:
+                # on exception, setup the widget error message
+                view = zope.component.getMultiAdapter(
+                    (error, self.request, widget, widget.field,
+                     self.parentForm, self.context),
+                    interfaces.IErrorViewSnippet)
+                view.update()
+                widget.error = view
+
+    def setupFields(self):
+        self.fields = Fields(self.__parent__.field.schema)
+
+    def update(self, ignoreContext=None, setErrors=True):
+        if self.__parent__.field is None:
+            raise ValueError("%r .field is None, that's a blocking point" % self.__parent__)
+        #update stuff from parent to be sure
+        self.mode = self.__parent__.mode
+        if ignoreContext is not None:
+            self.ignoreContext = ignoreContext
+        else:
+            self.ignoreContext = self.__parent__.ignoreContext
+        self.ignoreRequest = self.__parent__.ignoreRequest
+        if interfaces.IFormAware.providedBy(self.__parent__):
+            self.ignoreReadonly = self.parentForm.ignoreReadonly
+
+        prefix = ''
+        if self.parentForm:
+            prefix = util.expandPrefix(self.parentForm.prefix) + \
+                util.expandPrefix(self.parentForm.widgets.prefix)
+
+        self.prefix = prefix+self.__parent__.field.__name__
+
+        self.setupFields()
+
+        super(ObjectSubForm, self).update()
+
+        if setErrors:
+            #hmmm, do we need this here? seems to be over-validated
+            self._validate()
+
+    def getContent(self):
+        return self.__parent__._value
+
+class ObjectConverter(BaseDataConverter):
+    """Data converter for IObjectWidget."""
+
+    zope.component.adapts(
+        zope.schema.interfaces.IObject, interfaces.IObjectWidget)
+
+    def toWidgetValue(self, value):
+        """Just dispatch it."""
+        if value is self.field.missing_value:
+            return interfaces.NOVALUE
+
+        retval = {}
+        for name in zope.schema.getFieldNames(self.field.schema):
+            dm = zope.component.getMultiAdapter(
+                (value, self.field.schema[name]), interfaces.IDataManager)
+            retval[name] = dm.query()
+
+        return retval
+
+    def createObject(self, value):
+        #keep value passed, maybe some subclasses want it
+        #value here is the raw extracted from the widget's subform
+        #in the form of a dict key:fieldname, value:fieldvalue
+
+        name = getIfName(self.field.schema)
+        creator = zope.component.queryMultiAdapter(
+            (self.widget.context, self.widget.request,
+             self.widget.form, self.widget),
+            interfaces.IObjectFactory,
+            name=name)
+        if creator:
+            obj = creator(value)
+        else:
+            raise ValueError("No IObjectFactory adapter registered for %s" %
+                             name)
+
+        return obj
+
+    def toFieldValue(self, value):
+        """See interfaces.IDataConverter"""
+        if value is interfaces.NOVALUE:
+            return self.field.missing_value
+
+        if self.widget.subform.ignoreContext:
+            obj = self.createObject(value)
+        else:
+            dm = zope.component.getMultiAdapter(
+                (self.widget.context, self.field), interfaces.IDataManager)
+            try:
+                obj = dm.get()
+            except KeyError:
+                obj = self.createObject(value)
+
+        obj = self.field.schema(obj)
+
+        names = []
+        for name in zope.schema.getFieldNames(self.field.schema):
+            try:
+                dm = zope.component.getMultiAdapter(
+                    (obj, self.field.schema[name]), interfaces.IDataManager)
+                oldval = dm.query()
+                if (oldval != value[name]
+                    or zope.schema.interfaces.IObject.providedBy(
+                        self.field.schema[name])
+                    ):
+                    dm.set(value[name])
+                    names.append(name)
+            except KeyError:
+                pass
+
+        if names:
+            zope.event.notify(
+                zope.lifecycleevent.ObjectModifiedEvent(obj,
+                    zope.lifecycleevent.Attributes(self.field.schema, *names)))
+
+        # Commonly the widget context is security proxied. This method,
+        # however, should return a bare object, so let's remove the
+        # security proxy now that all fields have been set using the security
+        # mechanism.
+        return removeSecurityProxy(obj)
+
+
+class ObjectWidget(widget.Widget):
+    zope.interface.implements(interfaces.IObjectWidget)
+
+    subform = None
+    _value = interfaces.NOVALUE
+    _updating = False
+
+    def _getForm(self, content):
+        form = getattr(self, 'form', None)
+        schema = getattr(self.field, 'schema', None)
+
+        self.subform = zope.component.getMultiAdapter(
+            (content, self.request, self.context,
+             form, self, self.field, schema),
+            interfaces.ISubformFactory)()
+
+    def updateWidgets(self, setErrors=True):
+        if self._value is not interfaces.NOVALUE:
+            self._getForm(self._value)
+            ignore = None
+        else:
+            self._getForm(None)
+            ignore = True
+
+        self.subform.update(ignore, setErrors=setErrors)
+
+    def update(self):
+        #very-very-nasty: skip raising exceptions in extract while we're updating
+        self._updating = True
+        try:
+            super(ObjectWidget, self).update()
+            self.updateWidgets(setErrors=False)
+        finally:
+            self._updating = False
+
+    @apply
+    def value():
+        """This invokes updateWidgets on any value change e.g. update/extract."""
+        def get(self):
+            return self.extract(setErrors=True)
+        def set(self, value):
+            self._value = value
+            # ensure that we apply our new values to the widgets
+            self.updateWidgets()
+        return property(get, set)
+
+
+    def extract(self, default=interfaces.NOVALUE, setErrors=True):
+        if self.name+'-empty-marker' in self.request:
+            self.updateWidgets(setErrors=False)
+
+            value, errors = self.subform.extractData(setErrors=setErrors)
+
+            if errors:
+                #very-very-nasty: skip raising exceptions in extract
+                #while we're updating
+                if self._updating:
+                    return default
+                raise MultipleErrors(errors)
+
+            return value
+
+        else:
+            return default
+
+
+######## default adapters
+
+class SubformAdapter(object):
+    """Most basic-default subform factory adapter"""
+
+    zope.interface.implements(interfaces.ISubformFactory)
+    zope.component.adapts(zope.interface.Interface, #widget value
+                          interfaces.IFormLayer,    #request
+                          zope.interface.Interface, #widget context
+                          zope.interface.Interface, #form
+                          interfaces.IObjectWidget, #widget
+                          zope.interface.Interface, #field
+                          zope.interface.Interface) #field.schema
+
+    factory = ObjectSubForm
+
+    def __init__(self, context, request, widgetContext, form,
+                 widget, field, schema):
+        self.context = context
+        self.request = request
+        self.widgetContext = widgetContext
+        self.form = form
+        self.widget = widget
+        self.field = field
+        self.schema = schema
+
+    def __call__(self):
+        #value is the extracted data from the form
+        obj = self.factory(self.context, self.request, self.widget)
+        return obj
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.__name__)
+
+class FactoryAdapter(object):
+    """Most basic-default object factory adapter"""
+
+    zope.interface.implements(interfaces.IObjectFactory)
+    zope.component.adapts(zope.interface.Interface, interfaces.IFormLayer,
+        interfaces.IForm, interfaces.IWidget)
+
+    factory = None
+
+    def __init__(self, context, request, form, widget):
+        self.context = context
+        self.request = request
+        self.form = form
+        self.widget = widget
+
+    def __call__(self, value):
+        #value is the extracted data from the form
+        obj = self.factory()
+        zope.event.notify(zope.lifecycleevent.ObjectCreatedEvent(obj))
+        return obj
+
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.__name__)
+
+# XXX: Probably we should offer an register factrory method which allows to
+# use all discriminators e.g. context, request, form, widget as optional
+# arguments. But can probably do that later in a ZCML directive
+def registerFactoryAdapter(for_, klass):
+    """register the basic FactoryAdapter for a given interface and class"""
+    name = getIfName(for_)
+    class temp(FactoryAdapter):
+        factory = klass
+    zope.component.provideAdapter(temp, name=name)

Copied: z3c.form/trunk/src/z3c/form/object.zcml (from rev 93221, z3c.form/branches/adamg-objectwidget/src/z3c/form/object.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/object.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/object.zcml	2008-11-21 09:14:51 UTC (rev 93230)
@@ -0,0 +1,16 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:i18n="http://namespaces.zope.org/i18n"
+    i18n_domain="z3c.form">
+
+  <!-- Data Converters -->
+  <adapter
+      factory=".object.ObjectConverter"
+      />
+
+  <!-- Subform default factory -->
+  <adapter
+      factory=".object.SubformAdapter"
+      />
+
+</configure>

Modified: z3c.form/trunk/src/z3c/form/testing.py
===================================================================
--- z3c.form/trunk/src/z3c/form/testing.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/testing.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -25,6 +25,7 @@
 import zope.component
 import zope.interface
 import zope.schema
+from zope.schema.fieldproperty import FieldProperty
 import zope.configuration.xmlconfig
 
 from zope.pagetemplate.interfaces import IPageTemplate
@@ -104,6 +105,65 @@
 def getPath(filename):
     return os.path.join(os.path.dirname(browser.__file__), filename)
 
+
+#############################
+# classes required by ObjectWidget tests
+#
+
+class IMySubObject(zope.interface.Interface):
+    foofield = zope.schema.Int(
+        title=u"My foo field",
+        default=1111,
+        max=9999)
+    barfield = zope.schema.Int(
+        title=u"My dear bar",
+        default=2222,
+        required=False)
+
+class MySubObject(object):
+    zope.interface.implements(IMySubObject)
+
+    foofield = FieldProperty(IMySubObject['foofield'])
+    barfield = FieldProperty(IMySubObject['barfield'])
+
+class IMySecond(zope.interface.Interface):
+    subfield = zope.schema.Object(
+        title=u"Second-subobject",
+        schema=IMySubObject)
+    moofield = zope.schema.TextLine(title=u"Something")
+
+class MySecond(object):
+    zope.interface.implements(IMySecond)
+
+    subfield = FieldProperty(IMySecond['subfield'])
+    moofield = FieldProperty(IMySecond['moofield'])
+
+
+class IMyObject(zope.interface.Interface):
+    subobject = zope.schema.Object(title=u'my object', schema=IMySubObject)
+    name = zope.schema.TextLine(title=u'name')
+
+class MyObject(object):
+    zope.interface.implements(IMyObject)
+    def __init__(self, name=u'', subobject=None):
+        self.subobject=subobject
+        self.name=name
+
+
+class IMyComplexObject(zope.interface.Interface):
+    subobject = zope.schema.Object(title=u'my object', schema=IMySecond)
+    name = zope.schema.TextLine(title=u'name')
+
+class MyComplexObject(object):
+    zope.interface.implements(IMyComplexObject)
+    def __init__(self, name=u'', subobject=None):
+        self.subobject=subobject
+        self.name=name
+
+#
+#
+#############################
+
 def setUp(test):
     test.globs = {'root': setup.placefulSetUp(True)}
 

Modified: z3c.form/trunk/src/z3c/form/widget.py
===================================================================
--- z3c.form/trunk/src/z3c/form/widget.py	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/widget.py	2008-11-21 09:14:51 UTC (rev 93230)
@@ -77,7 +77,9 @@
         lookForDefault = False
         # Step 1.1: If possible, get a value from the request
         if not self.ignoreRequest:
-            widget_value = self.extract()
+            #at this turn we do not need errors to be set on widgets
+            #errors will be set when extract gets called from form.extractData
+            widget_value = self.extract(setErrors=False)
             if widget_value is not interfaces.NOVALUE:
                 # Once we found the value in the request, it takes precendence
                 # over everything and nothing else has to be done.
@@ -136,7 +138,7 @@
                 IPageTemplate, name=self.mode)
         return template(self)
 
-    def extract(self, default=interfaces.NOVALUE):
+    def extract(self, default=interfaces.NOVALUE, setErrors=True):
         """See z3c.form.interfaces.IWidget."""
         return self.request.get(self.name, default)
 
@@ -195,13 +197,15 @@
         self.updateTerms()
         super(SequenceWidget, self).update()
 
-    def extract(self, default=interfaces.NOVALUE):
+    def extract(self, default=interfaces.NOVALUE, setErrors=True):
         """See z3c.form.interfaces.IWidget."""
         if (self.name not in self.request and
             self.name+'-empty-marker' in self.request):
             return []
         value = self.request.get(self.name, default)
         if value != default:
+            if not isinstance(value, (tuple, list)):
+                value = (value,)
             # do some kind of validation, at least only use existing values
             for token in value:
                 if token == self.noValueToken:
@@ -338,7 +342,7 @@
             self.updateWidgets()
         return property(get, set)
 
-    def extract(self, default=interfaces.NOVALUE):
+    def extract(self, default=interfaces.NOVALUE, setErrors=True):
         # This method is responsible to get the widgets value based on the
         # request and nothing else.
         # We have to setup the widgets for extract their values, because we

Modified: z3c.form/trunk/src/z3c/form/widget.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/widget.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/widget.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -447,6 +447,15 @@
   >>> seqWidget.extract()
   []
 
+Note that we also support single values being returned outside a sequence. The
+extracted value is then wrapped by a tuple. This feature is useful when
+integrating with third-party client frameworks that do not know about the Zope
+naming conventions.
+
+  >>> seqWidget.request = TestRequest(form={'seq': 'v1'})
+  >>> seqWidget.extract()
+  ('v1',)
+
 If the no-value token has been selected, it is returned without further
 verification:
 

Modified: z3c.form/trunk/src/z3c/form/zcml.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/zcml.txt	2008-11-21 08:55:28 UTC (rev 93229)
+++ z3c.form/trunk/src/z3c/form/zcml.txt	2008-11-21 09:14:51 UTC (rev 93230)
@@ -75,7 +75,7 @@
 and check it:
 
   >>> template
-  <z3c.pt.compat.ViewPageTemplateFile object at ...>
+  <ViewPageTemplateFile ...widget.pt>
 
 Let's use the template within the widget.
 
@@ -117,4 +117,3 @@
 
   >>> import shutil
   >>> shutil.rmtree(temp_dir)
-



More information about the Checkins mailing list