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

Adam Groszer cvs-admin at zope.org
Wed Sep 12 14:57:58 UTC 2012


Log message for revision 127835:
  merge of branch adamg-missing-terms,
  - Missing terms in vocabularies: this was a pain until now.
    Now it's possible to have the same (missing) value unchanged on the object
    with an EditForm after save as it was before editing.
    That brings some changes with it:
    - *MAJOR*: unchanged values/fields do not get validated anymore
      (unless they are empty or are FileUploads)
    - A temporary ``SimpleTerm`` gets created for the missing value
      Title is by default "Missing: ${value}". See MissingTermsMixin.
  
  - Split ``configure.zcml``
  

Changed:
  U   z3c.form/trunk/CHANGES.txt
  A   z3c.form/trunk/src/z3c/form/apidoc.zcml
  U   z3c.form/trunk/src/z3c/form/browser/objectmulti.txt
  A   z3c.form/trunk/src/z3c/form/browser/select-missing-terms.txt
  U   z3c.form/trunk/src/z3c/form/browser/select.py
  U   z3c.form/trunk/src/z3c/form/browser/tests.py
  A   z3c.form/trunk/src/z3c/form/button.zcml
  U   z3c.form/trunk/src/z3c/form/configure.zcml
  A   z3c.form/trunk/src/z3c/form/converter.zcml
  A   z3c.form/trunk/src/z3c/form/datamanager.zcml
  A   z3c.form/trunk/src/z3c/form/error.zcml
  U   z3c.form/trunk/src/z3c/form/field.py
  U   z3c.form/trunk/src/z3c/form/form.py
  U   z3c.form/trunk/src/z3c/form/interfaces.py
  U   z3c.form/trunk/src/z3c/form/object.py
  U   z3c.form/trunk/src/z3c/form/term.py
  U   z3c.form/trunk/src/z3c/form/term.txt
  A   z3c.form/trunk/src/z3c/form/term.zcml
  U   z3c.form/trunk/src/z3c/form/util.py
  U   z3c.form/trunk/src/z3c/form/util.txt
  U   z3c.form/trunk/src/z3c/form/validator.py
  U   z3c.form/trunk/src/z3c/form/validator.txt
  A   z3c.form/trunk/src/z3c/form/validator.zcml

-=-
Modified: z3c.form/trunk/CHANGES.txt
===================================================================
--- z3c.form/trunk/CHANGES.txt	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/CHANGES.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -2,8 +2,20 @@
 CHANGES
 =======
 
-In next release ...
+2.9.0 (unreleased)
+------------------
 
+- Missing terms in vocabularies: this was a pain until now.
+  Now it's possible to have the same (missing) value unchanged on the object
+  with an EditForm after save as it was before editing.
+  That brings some changes with it:
+  - *MAJOR*: unchanged values/fields do not get validated anymore
+    (unless they are empty or are FileUploads)
+  - A temporary ``SimpleTerm`` gets created for the missing value
+    Title is by default "Missing: ${value}". See MissingTermsMixin.
+
+- Split ``configure.zcml``
+
 - Initialize widgets in ``update`` step. The ``updateWidgets`` method
   is now responsible only for actually updating the widgets.
 
@@ -11,14 +23,9 @@
   widgets are updated, useful for situations where neither a form, nor
   a widgets prefix is desired.
 
-
-2.8.3 (unreleased)
-------------------
-
 - ``SequenceWidget`` DISPLAY_MODE: silently ignore missing tokens,
   because INPUT_MODE and HIDDEN_MODE does that too
 
-
 2.8.2 (2012-08-17)
 ------------------
 

Copied: z3c.form/trunk/src/z3c/form/apidoc.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/apidoc.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/apidoc.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/apidoc.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,84 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- APIDoc documentation -->
+  <configure
+      xmlns:zcml="http://namespaces.zope.org/zcml"
+      xmlns:apidoc="http://namespaces.zope.org/apidoc"
+      zcml:condition="have apidoc">
+
+    <apidoc:bookchapter
+        id="z3c-form"
+        title="z3c.form - Widgets and Forms"
+        doc_path="README.txt"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-form"
+        title="Forms"
+        doc_path="form.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-subform"
+        title="Sub-Forms"
+        doc_path="subform.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-field"
+        title="Fields"
+        doc_path="field.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-button"
+        title="Buttons"
+        doc_path="button.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-validator"
+        title="Validators"
+        doc_path="validator.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-zcml"
+        title="ZCML Directives"
+        doc_path="zcml.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-widget"
+        title="Widgets"
+        doc_path="widget.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-action"
+        title="Actions"
+        doc_path="action.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-value"
+        title="Attribute Values"
+        doc_path="value.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-datamanager"
+        title="Data Managers"
+        doc_path="datamanager.txt"
+        parent="z3c-form"
+        />
+    <apidoc:bookchapter
+        id="z3c-form-converter"
+        title="Data Converters"
+        doc_path="converter.txt"
+        parent="z3c-form"
+        />
+  </configure>
+
+</configure>

Modified: z3c.form/trunk/src/z3c/form/browser/objectmulti.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/objectmulti.txt	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/browser/objectmulti.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -189,54 +189,59 @@
   >>> widget.update()
   >>> print widget.render()
   <div class="multi-widget required">
-      <div id="foo-0-row" class="row">
-          <div class="label">
-            <label for="foo-0">
-              <span>my object widget</span>
-              <span class="required">*</span>
-            </label>
-          </div>
-          <div class="error">Wrong contained type</div>
-          <div class="widget">
-            <div class="multi-widget-checkbox">
-              <input id="foo-0-remove" name="foo.0.remove"
-                     class="multi-widget-checkbox checkbox-widget"
-                     type="checkbox" value="1" />
+    <div class="row" id="foo-0-row">
+      <div class="label">
+        <label for="foo-0">
+          <span>my object widget</span>
+          <span class="required">*</span>
+        </label>
+      </div>
+      <div class="error">Wrong contained type</div>
+      <div class="widget">
+        <div class="multi-widget-checkbox">
+          <input class="multi-widget-checkbox checkbox-widget"
+                 id="foo-0-remove" name="foo.0.remove" type="checkbox" value="1">
+        </div>
+        <div class="multi-widget-input">
+          <div class="object-widget required">
+            <div class="label">
+              <label for="foo-0-widgets-foofield">
+                <span>My foo field</span>
+                <span class="required">*</span>
+              </label>
             </div>
-            <div class="multi-widget-input"><div class="object-widget required">
-              <div class="label">
-                <label for="foo-0-widgets-foofield">
-                  <span>My foo field</span>
-                  <span class="required">*</span>
-                </label>
-              </div>
-              <div class="error">Required input is missing.</div>
-              <div class="widget">
-                <input id="foo-0-widgets-foofield" name="foo.0.widgets.foofield"
-                       class="text-widget required int-field" type="text" />
-              </div>
-              <div class="label">
-                <label for="foo-0-widgets-barfield">
-                  <span>My dear bar</span>
-                </label>
-              </div>
-              <div class="widget">
-                <input id="foo-0-widgets-barfield" name="foo.0.widgets.barfield"
-                       class="text-widget int-field" value="666" type="text" />
-              </div>
-              <input name="foo.0-empty-marker" type="hidden" value="1" />
+            <div class="error">Required input is missing.</div>
+            <div class="widget">
+              <input class="text-widget required int-field"
+                     id="foo-0-widgets-foofield" name="foo.0.widgets.foofield"
+                     type="text" value="">
             </div>
+            <div class="label">
+              <label for="foo-0-widgets-barfield">
+                <span>My dear bar</span>
+              </label>
+            </div>
+            <div class="widget">
+              <input class="text-widget int-field"
+                     id="foo-0-widgets-barfield" name="foo.0.widgets.barfield"
+                     type="text" value="666">
+            </div>
+            <input name="foo.0-empty-marker" type="hidden" value="1">
           </div>
         </div>
+      </div>
     </div>
     <div class="buttons">
       <input id="foo-buttons-add" name="foo.buttons.add"
-             class="submit-widget button-field" value="Add" type="submit" />
-      <input id="foo-buttons-remove" name="foo.buttons.remove"
-             class="submit-widget button-field" value="Remove selected" type="submit" />
+             class="submit-widget button-field" value="Add"
+             type="submit" />
+      <input id="foo-buttons-remove"
+             name="foo.buttons.remove"
+             class="submit-widget button-field" value="Remove selected"
+             type="submit" />
     </div>
   </div>
-  <input type="hidden" name="foo.count" value="1" />
+  <input name="foo.count" type="hidden" value="1">
 
 Let's set acceptable values:
 

Copied: z3c.form/trunk/src/z3c/form/browser/select-missing-terms.txt (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-missing-terms.txt)
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/select-missing-terms.txt	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/browser/select-missing-terms.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,238 @@
+============================
+Select Widget, missing terms
+============================
+
+The select widget allows you to select one or more values from a set of given
+options. The "SELECT" and "OPTION" elements are described here:
+
+http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#edef-SELECT
+
+As for all widgets, the select widget must provide the new ``IWidget``
+interface:
+
+  >>> from z3c.form import interfaces
+  >>> from z3c.form.browser import select
+
+The widget can be instantiated only using the request:
+
+  >>> from z3c.form.testing import TestRequest
+  >>> request = TestRequest()
+
+  >>> widget = select.SelectWidget(request)
+
+Before rendering the widget, one has to set the name and id of the widget:
+
+  >>> widget.id = 'widget-id'
+  >>> widget.name = 'widget.name'
+
+We also need to register the template for at least the widget and request:
+
+  >>> import zope.component
+  >>> from zope.pagetemplate.interfaces import IPageTemplate
+  >>> from z3c.form.testing import getPath
+  >>> from z3c.form.widget import WidgetTemplateFactory
+
+  >>> zope.component.provideAdapter(
+  ...     WidgetTemplateFactory(getPath('select_input.pt'), 'text/html'),
+  ...     (None, None, None, None, interfaces.ISelectWidget),
+  ...     IPageTemplate, name=interfaces.INPUT_MODE)
+
+We need some context:
+
+  >>> class IPerson(zope.interface.Interface):
+  ...     rating = zope.schema.Choice(
+  ...     vocabulary='Ratings')
+
+  >>> class Person(object):
+  ...     zope.interface.implements(IPerson)
+  >>> person = Person()
+
+Let's provide some values for this widget. We can do this by defining a source
+providing ``ITerms``. This source uses descriminators which will fit our setup.
+
+  >>> import zope.schema.interfaces
+  >>> from zope.schema.vocabulary import SimpleVocabulary
+  >>> from zope.schema.vocabulary import SimpleTerm
+  >>> import z3c.form.term
+
+  >>> from zope.schema import vocabulary
+  >>> ratings = vocabulary.SimpleVocabulary([
+  ...     vocabulary.SimpleVocabulary.createTerm(0, '0', u'bad'),
+  ...     vocabulary.SimpleVocabulary.createTerm(1, '1', u'okay'),
+  ...     vocabulary.SimpleVocabulary.createTerm(2, '2', u'good')
+  ...     ])
+
+  >>> def RatingsVocabulary(obj):
+  ...     return ratings
+
+  >>> vr = vocabulary.getVocabularyRegistry()
+  >>> vr.register('Ratings', RatingsVocabulary)
+
+  >>> class SelectionTerms(z3c.form.term.MissingChoiceTermsVocabulary):
+  ...     def __init__(self, context, request, form, field, widget):
+  ...         self.context = context
+  ...         self.field = field
+  ...         self.terms = ratings
+  ...         self.widget = widget
+  ...
+  ...     def _makeMissingTerm(self, token):
+  ...         if token == 'x':
+  ...             return super(SelectionTerms, self)._makeMissingTerm(token)
+  ...         else:
+  ...             raise LookupError
+
+  >>> zope.component.provideAdapter(SelectionTerms,
+  ...     (None, interfaces.IFormLayer, None, None, interfaces.ISelectWidget) )
+
+  >>> import z3c.form.datamanager
+  >>> zope.component.provideAdapter(z3c.form.datamanager.AttributeField)
+
+Now let's try if we get widget values:
+
+  >>> widget.update()
+  >>> print widget.render()
+  <select id="widget-id" name="widget.name:list"
+          class="select-widget" size="1">
+  <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+  <option id="widget-id-0" value="0">bad</option>
+  <option id="widget-id-1" value="1">okay</option>
+  <option id="widget-id-2" value="2">good</option>
+  </select>
+  <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+If we set the widget value to "x", then it should be present and selected:
+
+  >>> widget.value = ['x']
+  >>> widget.context = person
+  >>> widget.field = IPerson['rating']
+  >>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
+  >>> person.rating = 'x'
+  >>> widget.terms = None
+
+  >>> widget.update()
+  >>> print widget.render()
+  <select id="widget-id" name="widget.name:list"
+          class="select-widget" size="1">
+  <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+  <option id="widget-id-0" value="0">bad</option>
+  <option id="widget-id-1" value="1">okay</option>
+  <option id="widget-id-2" value="2">good</option>
+  <option id="widget-id-missing-0" selected="selected" value="x">Missing: x</option>
+  </select>
+  <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+If we set the widget value to "y", then it should NOT be around:
+
+  >>> widget.value = ['y']
+  >>> widget.update()
+  >>> print widget.render()
+  <select id="widget-id" name="widget.name:list"
+          class="select-widget" size="1">
+  <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+  <option id="widget-id-0" value="0">bad</option>
+  <option id="widget-id-1" value="1">okay</option>
+  <option id="widget-id-2" value="2">good</option>
+  </select>
+  <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+Let's now make sure that we can extract user entered data from a widget:
+
+  >>> widget.request = TestRequest(form={'widget.name': ['c']})
+  >>> widget.update()
+  >>> widget.extract()
+  <NO_VALUE>
+
+Well, only of it matches the context's current value:
+
+  >>> widget.request = TestRequest(form={'widget.name': ['x']})
+  >>> widget.update()
+  >>> widget.extract()
+  ['x']
+
+When "no value" is selected, then no verification against the terms is done:
+
+  >>> widget.request = TestRequest(form={'widget.name': ['--NOVALUE--']})
+  >>> widget.update()
+  >>> widget.extract(default=1)
+  ['--NOVALUE--']
+
+Let's now make sure that we can extract user entered missing data from a widget:
+
+  >>> widget.request = TestRequest(form={'widget.name': ['x']})
+  >>> widget.update()
+  >>> widget.extract()
+  ['x']
+
+  >>> widget.request = TestRequest(form={'widget.name': ['y']})
+  >>> widget.update()
+  >>> widget.extract()
+  <NO_VALUE>
+
+Unfortunately, when nothing is selected, we do not get an empty list sent into
+the request, but simply no entry at all. For this we have the empty marker, so
+that:
+
+  >>> widget.request = TestRequest(form={'widget.name-empty-marker': '1'})
+  >>> widget.update()
+  >>> widget.extract()
+  []
+
+If nothing is found in the request, the default is returned:
+
+  >>> widget.request = TestRequest()
+  >>> widget.update()
+  >>> widget.extract(default=1)
+  1
+
+Let's now make sure that a bogus value causes extract to return the default as
+described by the interface:
+
+  >>> widget.request = TestRequest(form={'widget.name': ['y']})
+  >>> widget.update()
+  >>> widget.extract(default=1)
+  1
+
+Display Widget
+--------------
+
+The select widget comes with a template for ``DISPLAY_MODE``. Let's
+register it first:
+
+  >>> zope.component.provideAdapter(
+  ...     WidgetTemplateFactory(getPath('select_display.pt'), 'text/html'),
+  ...     (None, None, None, None, interfaces.ISelectWidget),
+  ...     IPageTemplate, name=interfaces.DISPLAY_MODE)
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+  >>> widget.required = True
+  >>> widget.mode = interfaces.DISPLAY_MODE
+  >>> widget.value = ['0', '1', 'x']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <span id="widget-id" class="select-widget required">
+    <span class="selected-option">bad</span>,
+    <span class="selected-option">okay</span>,
+    <span class="selected-option">Missing: x</span>
+  </span>
+
+Hidden Widget
+-------------
+
+The select widget comes with a template for ``HIDDEN_MODE``.  Let's
+register it first:
+
+  >>> zope.component.provideAdapter(
+  ...     WidgetTemplateFactory(getPath('select_hidden.pt'), 'text/html'),
+  ...     (None, None, None, None, interfaces.ISelectWidget),
+  ...     IPageTemplate, name=interfaces.HIDDEN_MODE)
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+  >>> widget.mode = interfaces.HIDDEN_MODE
+  >>> widget.value = ['0', 'x']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <input id="widget-id-0" name="widget.name:list" value="0" class="hidden-widget" type="hidden" />
+  <input id="widget-id-missing-0" name="widget.name:list" value="x" class="hidden-widget" type="hidden" />
+  <input name="widget.name-empty-marker" type="hidden" value="1" />

Modified: z3c.form/trunk/src/z3c/form/browser/select.py
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/select.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/browser/select.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -51,7 +51,6 @@
         super(SelectWidget, self).update()
         widget.addFieldClass(self)
 
-    @property
     def items(self):
         if self.terms is None:  # update() has not been called yet
             return ()
@@ -66,16 +65,35 @@
                 'content': message,
                 'selected': self.value == []
                 })
-        for count, term in enumerate(self.terms):
+
+        ignored = set(self.value)
+
+        def addItem(idx, term, prefix=''):
             selected = self.isSelected(term)
-            id = '%s-%i' % (self.id, count)
+            if selected:
+                ignored.remove(term.token)
+            id = '%s-%s%i' % (self.id, prefix, idx)
             content = term.token
             if zope.schema.interfaces.ITitledTokenizedTerm.providedBy(term):
                 content = translate(
                     term.title, context=self.request, default=term.title)
             items.append(
-                {'id':id, 'value':term.token, 'content':content,
-                 'selected':selected})
+                {'id': id, 'value': term.token, 'content': content,
+                 'selected': selected})
+
+        for idx, term in enumerate(self.terms):
+            addItem(idx, term)
+
+        if ignored:
+            # some values are not displayed, probably they went away from the vocabulary
+            for idx, token in enumerate(sorted(ignored)):
+                try:
+                    term = self.terms.getTermByToken(token)
+                except LookupError:
+                    # just in case the term really went away
+                    continue
+
+                addItem(idx, term, prefix='missing-')
         return items
 
 

Modified: z3c.form/trunk/src/z3c/form/browser/tests.py
===================================================================
--- z3c.form/trunk/src/z3c/form/browser/tests.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/browser/tests.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -94,6 +94,11 @@
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
                      checker=checker,
                      ),
+        DocFileSuite('select-missing-terms.txt',
+                     setUp=setUp, tearDown=testing.tearDown,
+                     optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+                     checker=checker,
+                     ),
         DocFileSuite('select-source.txt',
                      setUp=setUp, tearDown=testing.tearDown,
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,

Copied: z3c.form/trunk/src/z3c/form/button.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/button.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/button.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/button.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,21 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- Actions, Action Managers and Handlers -->
+  <adapter
+      factory=".button.ButtonAction"
+      provides=".interfaces.IButtonAction"
+      />
+  <adapter
+      factory=".button.ImageButtonAction"
+      provides=".interfaces.IButtonAction"
+      />
+  <adapter
+      factory=".button.ButtonActions"
+      />
+  <adapter
+      factory=".button.ButtonActionHandler"
+      />
+
+</configure>

Modified: z3c.form/trunk/src/z3c/form/configure.zcml
===================================================================
--- z3c.form/trunk/src/z3c/form/configure.zcml	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/configure.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -9,22 +9,6 @@
       type="zope.publisher.interfaces.browser.IBrowserSkinType"
       />
 
-  <!-- Validators -->
-  <adapter
-      factory=".validator.SimpleFieldValidator"
-      />
-  <adapter
-      factory=".validator.InvariantsValidator"
-      />
-
-  <!-- Data Managers -->
-  <adapter
-      factory=".datamanager.AttributeField"
-      />
-  <adapter
-      factory=".datamanager.DictionaryField"
-      />
-
   <!-- Widget Manager -->
   <adapter
       factory=".field.FieldWidgets"
@@ -34,198 +18,19 @@
       factory=".contentprovider.FieldWidgetsAndProviders"
       />
 
-  <!-- Data Converters -->
-  <adapter
-      factory=".converter.FieldDataConverter"
-      />
-  <adapter
-      factory=".converter.IntegerDataConverter"
-      />
-  <adapter
-      factory=".converter.FloatDataConverter"
-      />
-  <adapter
-      factory=".converter.DecimalDataConverter"
-      />
-  <adapter
-      factory=".converter.DateDataConverter"
-      />
-  <adapter
-      factory=".converter.TimeDataConverter"
-      />
-  <adapter
-      factory=".converter.DatetimeDataConverter"
-      />
-  <adapter
-      factory=".converter.TimedeltaDataConverter"
-      />
-  <include file="file.zcml" />
-  <adapter
-      factory=".converter.SequenceDataConverter"
-      />
-  <adapter
-      factory=".converter.CollectionSequenceDataConverter"
-      />
-  <adapter
-      factory=".converter.BoolSingleCheckboxDataConverter"
-      />
-  <adapter
-      factory=".converter.FieldWidgetDataConverter"
-      />
-  <adapter
-      factory=".converter.TextLinesConverter"
-      />
-  <adapter
-      factory=".converter.MultiConverter"
-      />
-
-  <!-- ITerms -->
-  <adapter
-      factory=".term.ChoiceTerms"
-      />
-  <adapter
-      factory=".term.ChoiceTermsVocabulary"
-      />
-  <adapter
-      factory=".term.ChoiceTermsSource"
-      />
-  <adapter
-      factory=".term.CollectionTerms"
-      />
-  <adapter
-      factory=".term.CollectionTermsVocabulary"
-      />
-  <adapter
-      factory=".term.CollectionTermsSource"
-      />
-  <adapter
-      factory=".term.BoolTerms"
-      />
-
   <!-- Actions, Action Managers and Handlers -->
-  <adapter
-      factory=".button.ButtonAction"
-      provides=".interfaces.IButtonAction"
-      />
-  <adapter
-      factory=".button.ImageButtonAction"
-      provides=".interfaces.IButtonAction"
-      />
-  <adapter
-      factory=".button.ButtonActions"
-      />
-  <adapter
-      factory=".button.ButtonActionHandler"
-      />
   <subscriber
       handler=".form.handleActionError"
       />
 
-  <!-- Error Views -->
-  <adapter
-      factory=".error.ErrorViewSnippet"
-      trusted="True"
-      permission="zope.Public"
-      />
-  <adapter
-      factory=".error.InvalidErrorViewSnippet"
-      trusted="True"
-      permission="zope.Public"
-      />
-  <adapter
-      factory=".error.ValueErrorViewSnippet"
-      trusted="True"
-      permission="zope.Public"
-      />
-  <adapter
-      factory=".error.MultipleErrorViewSnippet"
-      trusted="True"
-      permission="zope.Public"
-      />
-  <adapter
-      factory=".error.StandardErrorViewTemplate"
-      />
-
-  <!-- APIDoc documentation -->
-  <configure
-      xmlns:zcml="http://namespaces.zope.org/zcml"
-      xmlns:apidoc="http://namespaces.zope.org/apidoc"
-      zcml:condition="have apidoc">
-
-    <apidoc:bookchapter
-        id="z3c-form"
-        title="z3c.form - Widgets and Forms"
-        doc_path="README.txt"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-form"
-        title="Forms"
-        doc_path="form.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-subform"
-        title="Sub-Forms"
-        doc_path="subform.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-field"
-        title="Fields"
-        doc_path="field.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-button"
-        title="Buttons"
-        doc_path="button.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-validator"
-        title="Validators"
-        doc_path="validator.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-zcml"
-        title="ZCML Directives"
-        doc_path="zcml.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-widget"
-        title="Widgets"
-        doc_path="widget.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-action"
-        title="Actions"
-        doc_path="action.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-value"
-        title="Attribute Values"
-        doc_path="value.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-datamanager"
-        title="Data Managers"
-        doc_path="datamanager.txt"
-        parent="z3c-form"
-        />
-    <apidoc:bookchapter
-        id="z3c-form-converter"
-        title="Data Converters"
-        doc_path="converter.txt"
-        parent="z3c-form"
-        />
-  </configure>
-
   <include file="object.zcml" />
+  <include file="apidoc.zcml" />
+  <include file="button.zcml" />
+  <include file="converter.zcml" />
+  <include file="datamanager.zcml" />
+  <include file="error.zcml" />
+  <include file="term.zcml" />
+  <include file="validator.zcml" />
 
   <include package=".browser" />
 

Copied: z3c.form/trunk/src/z3c/form/converter.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/converter.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/converter.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/converter.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,50 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- Data Converters -->
+  <adapter
+      factory=".converter.FieldDataConverter"
+      />
+  <adapter
+      factory=".converter.IntegerDataConverter"
+      />
+  <adapter
+      factory=".converter.FloatDataConverter"
+      />
+  <adapter
+      factory=".converter.DecimalDataConverter"
+      />
+  <adapter
+      factory=".converter.DateDataConverter"
+      />
+  <adapter
+      factory=".converter.TimeDataConverter"
+      />
+  <adapter
+      factory=".converter.DatetimeDataConverter"
+      />
+  <adapter
+      factory=".converter.TimedeltaDataConverter"
+      />
+  <include file="file.zcml" />
+  <adapter
+      factory=".converter.SequenceDataConverter"
+      />
+  <adapter
+      factory=".converter.CollectionSequenceDataConverter"
+      />
+  <adapter
+      factory=".converter.BoolSingleCheckboxDataConverter"
+      />
+  <adapter
+      factory=".converter.FieldWidgetDataConverter"
+      />
+  <adapter
+      factory=".converter.TextLinesConverter"
+      />
+  <adapter
+      factory=".converter.MultiConverter"
+      />
+
+</configure>

Copied: z3c.form/trunk/src/z3c/form/datamanager.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/datamanager.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/datamanager.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/datamanager.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,13 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- Data Managers -->
+  <adapter
+      factory=".datamanager.AttributeField"
+      />
+  <adapter
+      factory=".datamanager.DictionaryField"
+      />
+
+</configure>

Copied: z3c.form/trunk/src/z3c/form/error.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/error.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/error.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/error.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,30 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- Error Views -->
+  <adapter
+      factory=".error.ErrorViewSnippet"
+      trusted="True"
+      permission="zope.Public"
+      />
+  <adapter
+      factory=".error.InvalidErrorViewSnippet"
+      trusted="True"
+      permission="zope.Public"
+      />
+  <adapter
+      factory=".error.ValueErrorViewSnippet"
+      trusted="True"
+      permission="zope.Public"
+      />
+  <adapter
+      factory=".error.MultipleErrorViewSnippet"
+      trusted="True"
+      permission="zope.Public"
+      />
+  <adapter
+      factory=".error.StandardErrorViewTemplate"
+      />
+
+</configure>

Modified: z3c.form/trunk/src/z3c/form/field.py
===================================================================
--- z3c.form/trunk/src/z3c/form/field.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/field.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -284,7 +284,7 @@
                 self._data_values.append(widget)
                 self._data[shortName] = widget
                 zope.location.locate(widget, self, shortName)
-            # allways ensure that we add all keys and keep the order given from
+            # always ensure that we add all keys and keep the order given from
             # button items
             self._data_keys = uniqueOrderedKeys
 

Modified: z3c.form/trunk/src/z3c/form/form.py
===================================================================
--- z3c.form/trunk/src/z3c/form/form.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/form.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -34,22 +34,19 @@
     changes = {}
     for name, field in form.fields.items():
         # If the field is not in the data, then go on to the next one
-        if name not in data:
+        try:
+            newValue = data[name]
+        except KeyError:
             continue
         # If the value is NOT_CHANGED, ignore it, since the widget/converter
         # sent a strong message not to do so.
-        if data[name] is interfaces.NOT_CHANGED:
+        if newValue is interfaces.NOT_CHANGED:
             continue
-        # Get the datamanager and get the original value
-        dm = zope.component.getMultiAdapter(
-            (content, field.field), interfaces.IDataManager)
-        # Only update the data, if it is different
-        # Or we can not get the original value, in which case we can not check
-        # Or it is an Object, in case we'll never know
-        if (not dm.canAccess() or
-            dm.query() != data[name] or
-            zope.schema.interfaces.IObject.providedBy(field.field)):
-            dm.set(data[name])
+        if util.changedField(field.field, newValue, context=content):
+            # Only update the data, if it is different
+            dm = zope.component.getMultiAdapter(
+                (content, field.field), interfaces.IDataManager)
+            dm.set(newValue)
             # Record the change using information required later
             changes.setdefault(dm.field.interface, []).append(name)
     return changes

Modified: z3c.form/trunk/src/z3c/form/interfaces.py
===================================================================
--- z3c.form/trunk/src/z3c/form/interfaces.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/interfaces.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -114,7 +114,7 @@
 class IValidator(zope.interface.Interface):
     """A validator for a particular value."""
 
-    def validate(value):
+    def validate(value, force=False):
         """Validate the value.
 
         If successful, return ``None``. Otherwise raise an ``Invalid`` error.
@@ -1110,3 +1110,4 @@
 
 class IAfterWidgetUpdateEvent(IWidgetEvent):
     """An event sent out after the widget was updated."""
+

Modified: z3c.form/trunk/src/z3c/form/object.py
===================================================================
--- z3c.form/trunk/src/z3c/form/object.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/object.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -31,9 +31,11 @@
 from z3c.form.field import Fields
 from z3c.form.error import MultipleErrors
 
+
 def getIfName(iface):
-    return iface.__module__+'.'+iface.__name__
+    return iface.__module__ + '.' + iface.__name__
 
+
 class ObjectSubForm(form.BaseForm):
     zope.interface.implements(interfaces.ISubForm)
 
@@ -61,7 +63,7 @@
                      self.parentForm,
                      getattr(widget, 'field', None),
                      widget),
-                    interfaces.IValidator).validate(value)
+                    interfaces.IValidator).validate(value, force=True)
             except (zope.schema.ValidationError, ValueError), error:
                 # on exception, setup the widget error message
                 view = zope.component.getMultiAdapter(
@@ -87,6 +89,7 @@
     def getContent(self):
         return self.__parent__._value
 
+
 class ObjectConverter(BaseDataConverter):
     """Data converter for IObjectWidget."""
 
@@ -218,22 +221,38 @@
     def applyValue(self, widget, value=interfaces.NO_VALUE):
         """Validate and apply value to given widget.
         """
-        converter = interfaces.IDataConverter(widget)
-        try:
-            zope.component.getMultiAdapter(
-                (self.context,
-                 self.request,
-                 self.form,
-                 getattr(widget, 'field', None),
-                 widget),
-                interfaces.IValidator).validate(value)
+        if self.context is None:
+            converter = interfaces.IDataConverter(widget)
+            try:
+                widget.value = converter.toWidgetValue(value)
+            except TypeError:
+                # we're not checking the value, because there's no context
+                # in case of problems just set a bad value
+                widget.value = value
+        else:
+            context = None
+            if not self.ignoreContext:
+                # ahem, the context is not ours to check,
+                # but the context's right attribute
+                dm = zope.component.getMultiAdapter(
+                    (self.context, self.field), interfaces.IDataManager)
+                context = dm.query(default=None)
+            try:
+                zope.component.getMultiAdapter(
+                    (context,
+                     self.request,
+                     self.form,
+                     getattr(widget, 'field', None),
+                     widget),
+                    interfaces.IValidator).validate(value)
 
-            widget.value = converter.toWidgetValue(value)
-        except (zope.schema.ValidationError, ValueError):
-            # on exception, setup the widget error message
-            # set the wrong value as value
-            # the widget itself ought to cry about the error
-            widget.value = value
+                converter = interfaces.IDataConverter(widget)
+                widget.value = converter.toWidgetValue(value)
+            except (zope.schema.ValidationError, ValueError):
+                # on exception, setup the widget error message
+                # set the wrong value as value
+                # the widget itself ought to cry about the error
+                widget.value = value
 
     @apply
     def value():
@@ -241,7 +260,7 @@
         def get(self):
             #value (get) cannot raise an exception, then we return insane values
             try:
-                self.setErrors=True
+                self.setErrors = True
                 return self.extract()
             except MultipleErrors:
                 value = {}
@@ -260,17 +279,16 @@
 
         return property(get, set)
 
-
     def extract(self, default=interfaces.NO_VALUE):
-        if self.name+'-empty-marker' in self.request:
+        if self.name + '-empty-marker' in self.request:
             self.updateWidgets(setErrors=False)
 
             value, errors = self.subform.extractData(setErrors=self.setErrors)
 
             if errors:
-                #very-very-nasty: skip raising exceptions in extract
-                #while we're updating -- that happens when the widget
-                #is updated and update calls extract()
+                # very-very-nasty: skip raising exceptions in extract
+                # while we're updating -- that happens when the widget
+                # is updated and update calls extract()
                 if self._updating:
                     return default
 

Modified: z3c.form/trunk/src/z3c/form/term.py
===================================================================
--- z3c.form/trunk/src/z3c/form/term.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/term.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -22,6 +22,7 @@
 from zope.schema import vocabulary
 
 from z3c.form import interfaces
+from z3c.form import util
 from z3c.form.i18n import MessageFactory as _
 
 
@@ -37,7 +38,7 @@
         return self.terms.getTermByToken(token)
 
     def getValue(self, token):
-        return self.terms.getTermByToken(token).value
+        return self.getTermByToken(token).value
 
     def __iter__(self):
         return iter(self.terms)
@@ -48,6 +49,7 @@
     def __contains__(self, value):
         return self.terms.__contains__(value)
 
+
 class SourceTerms(Terms):
     """Base implementation for ITerms using a source instead of a vocabulary."""
 
@@ -123,6 +125,57 @@
         self.terms = vocabulary
 
 
+class MissingTermsMixin(object):
+    """This can be used in case previous values/tokens get missing
+    from the vocabulary and you still need to display/keep the values"""
+
+    def getTerm(self, value):
+        try:
+            return self.terms.getTerm(value)
+        except LookupError:
+            if (interfaces.IContextAware.providedBy(self.widget) and
+                not self.widget.ignoreContext):
+                curValue = zope.component.getMultiAdapter(
+                    (self.widget.context, self.field),
+                    interfaces.IDataManager).query()
+                if curValue == value:
+                    return self._makeMissingTerm(value)
+
+            raise
+
+    def _makeToken(self, value):
+        """create a unique valid ASCII token"""
+        return util.createCSSId(unicode(value))
+
+    def _makeMissingTerm(self, value):
+        """Return a term that should be displayed for the missing token"""
+        return vocabulary.SimpleTerm(value, self._makeToken(value),
+            title=_(u'Missing: ${value}', mapping=dict(value=unicode(value))))
+
+    def getTermByToken(self, token):
+        try:
+            return self.terms.getTermByToken(token)
+        except LookupError:
+            if (interfaces.IContextAware.providedBy(self.widget) and
+                not self.widget.ignoreContext):
+                value = zope.component.getMultiAdapter(
+                    (self.widget.context, self.field),
+                    interfaces.IDataManager).query()
+                term = self._makeMissingTerm(value)
+                if term.token == token:
+                    # check if the given token matches the value, if not
+                    # fall back on LookupError, otherwise we might accept
+                    # any crap coming from the request
+                    return term
+
+            raise LookupError(token)
+
+
+class MissingChoiceTermsVocabulary(MissingTermsMixin, ChoiceTermsVocabulary):
+    """ITerms adapter for zope.schema.IChoice based implementations using
+    vocabulary with missing terms support"""
+
+
 class ChoiceTermsSource(SourceTerms):
     "ITerms adapter for zope.schema.IChoice based implementations using source."
 
@@ -163,6 +216,7 @@
                               (False, 'false', self.falseLabel)]]
         self.terms = vocabulary.SimpleVocabulary(terms)
 
+
 @zope.interface.implementer(interfaces.ITerms)
 @zope.component.adapter(
     zope.interface.Interface,
@@ -176,6 +230,7 @@
         (context, request, form, field, terms, widget),
         interfaces.ITerms)
 
+
 class CollectionTermsVocabulary(Terms):
     """ITerms adapter for zope.schema.ICollection based implementations using
     vocabulary."""
@@ -196,6 +251,13 @@
         self.widget = widget
         self.terms = vocabulary
 
+
+class MissingCollectionTermsVocabulary(MissingTermsMixin,
+                                       CollectionTermsVocabulary):
+    """ITerms adapter for zope.schema.ICollection based implementations using
+    vocabulary with missing terms support."""
+
+
 class CollectionTermsSource(SourceTerms):
     """ITerms adapter for zope.schema.ICollection based implementations using
     source."""

Modified: z3c.form/trunk/src/z3c/form/term.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/term.txt	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/term.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -134,6 +134,95 @@
   >>> [entry.title for entry in terms]
   [u'bad', u'okay', u'good']
 
+Missing terms
+
+Sometimes it happens that a term goes away from the vocabulary, but our
+stored objects still reference that term.
+
+  >>> zope.component.provideAdapter(term.MissingChoiceTermsVocabulary)
+
+  >>> terms = term.ChoiceTerms(
+  ...     None, request, None, ratingField, widget)
+  >>> term = terms.getTermByToken('42')
+  Traceback (most recent call last):
+  ...
+  LookupError: 42
+
+The same goes with looking up a term by value:
+
+  >>> term = terms.getTerm('42')
+  Traceback (most recent call last):
+  ...
+  LookupError: 42
+
+Ooops, well this works only if the context has the right value for us.
+This is because we don't want to accept any crap that's coming from HTML.
+
+  >>> class IPerson(zope.interface.Interface):
+  ...     gender = zope.schema.Choice(title=u'Gender', vocabulary='Genders')
+  >>> class Person(object):
+  ...     zope.interface.implements(IPerson)
+  ...     gender = None
+  >>> gendersVocabulary = vocabulary.SimpleVocabulary([
+  ...     vocabulary.SimpleVocabulary.createTerm(1, 'male', u'Male'),
+  ...     vocabulary.SimpleVocabulary.createTerm(2, 'female', u'Female'),
+  ...     ])
+  >>> def GendersVocabulary(obj):
+  ...     return ratings
+  >>> vr.register('Genders', GendersVocabulary)
+
+  >>> ctx = Person()
+  >>> ctx.gender = 42
+
+  >>> genderWidget = z3c.form.widget.Widget(request)
+  >>> genderWidget.context = ctx
+  >>> from z3c.form import interfaces
+  >>> zope.interface.alsoProvides(genderWidget, interfaces.IContextAware)
+  >>> from z3c.form.datamanager import AttributeField
+  >>> zope.component.provideAdapter(AttributeField)
+
+  >>> terms = term.ChoiceTerms(
+  ...     ctx, request, None, IPerson['gender'], genderWidget)
+
+Here we go:
+
+  >>> missingTerm = terms.getTermByToken('42')
+
+We get the term, we passed the token, the value is coming from the context.
+
+  >>> missingTerm.token
+  '42'
+  >>> missingTerm.value
+  42
+
+We cannot figure the title, so we construct one.
+Override ``makeMissingTerm`` if you want your own.
+
+  >>> missingTerm.title
+  u'Missing: ${value}'
+
+Still we raise LookupError if the token does not fit the context's value:
+
+  >>> missingTerm = terms.getTermByToken('99')
+  Traceback (most recent call last):
+  ...
+  LookupError: 99
+
+The same goes with looking up a term by value.
+We get the term if the context's value fits:
+
+  >>> missingTerm = terms.getTerm(42)
+  >>> missingTerm.token
+  '42'
+
+And an exception if it does not:
+
+  >>> missingTerm = terms.getTerm(99)
+  Traceback (most recent call last):
+  ...
+  LookupError: 99
+
+
 Bool fields
 +++++++++++
 

Copied: z3c.form/trunk/src/z3c/form/term.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/term.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/term.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/term.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,28 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- ITerms -->
+  <adapter
+      factory=".term.ChoiceTerms"
+      />
+  <adapter
+      factory=".term.MissingChoiceTermsVocabulary"
+      />
+  <adapter
+      factory=".term.ChoiceTermsSource"
+      />
+  <adapter
+      factory=".term.CollectionTerms"
+      />
+  <adapter
+      factory=".term.CollectionTermsVocabulary"
+      />
+  <adapter
+      factory=".term.CollectionTermsSource"
+      />
+  <adapter
+      factory=".term.BoolTerms"
+      />
+
+</configure>

Modified: z3c.form/trunk/src/z3c/form/util.py
===================================================================
--- z3c.form/trunk/src/z3c/form/util.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/util.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -21,6 +21,7 @@
 import string
 import zope.interface
 import zope.contenttype
+import zope.schema
 
 from z3c.form import interfaces
 from z3c.form.i18n import MessageFactory as _
@@ -28,12 +29,16 @@
 
 _identifier = re.compile('[A-Za-z][a-zA-Z0-9_]*$')
 
+
 def createId(name):
     if _identifier.match(name):
         return str(name).lower()
     return name.encode('utf-8').encode('hex')
 
+
 _acceptableChars = string.letters + string.digits + '_-'
+
+
 def createCSSId(name):
     return str(''.join([((char in _acceptableChars and char) or
                          char.encode('utf-8').encode('hex'))
@@ -41,6 +46,7 @@
 
 classTypes = type, types.ClassType
 
+
 def getSpecification(spec, force=False):
     """Get the specification of the given object.
 
@@ -128,6 +134,46 @@
     return widget.filename
 
 
+def changedField(field, value, context=None):
+    """Figure if a field's value changed
+
+    Comparing the value of the context attribute and the given value"""
+    if context is None:
+        context = field.context
+    if context is None:
+        # IObjectWidget madness
+        return True
+    if zope.schema.interfaces.IObject.providedBy(field):
+        return True
+
+    # Get the datamanager and get the original value
+    dm = zope.component.getMultiAdapter(
+        (context, field), interfaces.IDataManager)
+    # now figure value chaged status
+    # Or we can not get the original value, in which case we can not check
+    # Or it is an Object, in case we'll never know
+    if (not dm.canAccess() or dm.query() != value):
+        return True
+    else:
+        return False
+
+
+def changedWidget(widget, value, field=None, context=None):
+    """figure if a widget's value changed
+
+    Comparing the value of the widget context attribute and the given value"""
+    if (interfaces.IContextAware.providedBy(widget)
+        and not widget.ignoreContext):
+        # if the widget is context aware, figure if it's field changed
+        if field is None:
+            field = widget.field
+        if context is None:
+            context = widget.context
+        return changedField(field, value, context=context)
+    # otherwise we cannot, return 'always changed'
+    return True
+
+
 class UniqueOrderedKeys(object):
     """Ensures that we only use unique keys in a list.
 

Modified: z3c.form/trunk/src/z3c/form/util.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/util.txt	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/util.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -46,7 +46,10 @@
   >>> util.createCSSId(u'This has spaces')
   'This20has20spaces'
 
+  >>> util.createCSSId(unicode(dict({1:'x', 'foobar': 42})))
+  '7b13a2027x272c2027foobar273a20427d'
 
+
 ``getWidgetById(form, id)`` Function
 ------------------------------------
 
@@ -448,3 +451,102 @@
   True
   >>> bazMarker1 is bazMarker2
   True
+
+`changedField()` function
+-------------------------
+
+Decide whether a field was changed/modified.
+
+  >>> class IPerson(zope.interface.Interface):
+  ...     login = zope.schema.TextLine(
+  ...         title=u'Login')
+  ...     address = zope.schema.Object(
+  ...         schema=zope.interface.Interface)
+
+  >>> class Person(object):
+  ...     zope.interface.implements(IPerson)
+  ...     login = 'johndoe'
+  >>> person = Person()
+
+field.context is None and no context passed:
+
+  >>> util.changedField(IPerson['login'], 'foo')
+  True
+
+IObject field:
+
+  >>> util.changedField(IPerson['address'], object(), context = person)
+  True
+
+field.context or context passed:
+
+  >>> import z3c.form.datamanager
+  >>> zope.component.provideAdapter(z3c.form.datamanager.AttributeField)
+
+  >>> util.changedField(IPerson['login'], 'foo', context = person)
+  True
+  >>> util.changedField(IPerson['login'], 'johndoe', context = person)
+  False
+
+  >>> fld = IPerson['login'].bind(person)
+  >>> util.changedField(fld, 'foo')
+  True
+  >>> util.changedField(fld, 'johndoe')
+  False
+
+No access:
+
+  >>> save = z3c.form.datamanager.AttributeField.canAccess
+  >>> z3c.form.datamanager.AttributeField.canAccess = lambda self: False
+
+  >>> util.changedField(IPerson['login'], 'foo', context = person)
+  True
+  >>> util.changedField(IPerson['login'], 'johndoe', context = person)
+  True
+
+  >>> z3c.form.datamanager.AttributeField.canAccess = save
+
+
+`changedWidget()` function
+---------------------------
+
+Decide whether a widget value was changed/modified.
+
+  >>> import z3c.form.testing
+  >>> request = z3c.form.testing.TestRequest()
+  >>> import z3c.form.widget
+  >>> widget = z3c.form.widget.Widget(request)
+
+If the widget is not IContextAware, there's nothing to check:
+
+  >>> from z3c.form import interfaces
+  >>> interfaces.IContextAware.providedBy(widget)
+  False
+
+  >>> util.changedWidget(widget, 'foo')
+  True
+
+Make it IContextAware:
+
+  >>> widget.context = person
+  >>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
+
+  >>> widget.field = IPerson['login']
+
+  >> util.changedWidget(widget, 'foo')
+  True
+
+  >>> util.changedWidget(widget, 'johndoe')
+  False
+
+Field and context is also overridable:
+
+  >>> widget.field = None
+  >>> util.changedWidget(widget, 'johndoe', field=IPerson['login'])
+  False
+
+  >>> p2 = Person()
+  >>> p2.login = 'foo'
+
+  >>> util.changedWidget(widget, 'foo', field=IPerson['login'], context=p2)
+  False

Modified: z3c.form/trunk/src/z3c/form/validator.py
===================================================================
--- z3c.form/trunk/src/z3c/form/validator.py	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/validator.py	2012-09-12 14:57:54 UTC (rev 127835)
@@ -26,8 +26,10 @@
 from z3c.form import interfaces, util
 
 
-class SimpleFieldValidator(object):
-    """Simple Field Validator"""
+class StrictSimpleFieldValidator(object):
+    """Strict Simple Field Validator
+
+    validates all incoming values"""
     zope.interface.implements(interfaces.IValidator)
     zope.component.adapts(
         zope.interface.Interface,
@@ -43,7 +45,7 @@
         self.field = field
         self.widget = widget
 
-    def validate(self, value):
+    def validate(self, value, force=False):
         """See interfaces.IValidator"""
         context = self.context
         field = self.field
@@ -80,6 +82,46 @@
             self.field.__name__)
 
 
+class SimpleFieldValidator(StrictSimpleFieldValidator):
+    """Simple Field Validator
+
+    ignores unchanged values"""
+
+    def validate(self, value, force=False):
+        """See interfaces.IValidator"""
+        if value is self.field.missing_value:
+            # let missing values run into stricter validation
+            # most important case is not let required fields pass
+            return super(SimpleFieldValidator, self).validate(value, force)
+
+        if not force:
+            if value is interfaces.NOT_CHANGED:
+                # no need to validate unchanged values
+                return
+
+            if self.widget and not util.changedWidget(
+                self.widget, value, field=self.field, context=self.context):
+                # if new value == old value, no need to validate
+                return
+
+        # otherwise StrictSimpleFieldValidator will do the job
+        return super(SimpleFieldValidator, self).validate(value, force)
+
+
+class FileUploadValidator(StrictSimpleFieldValidator):
+    """File upload validator
+    """
+    zope.component.adapts(
+        zope.interface.Interface,
+        zope.interface.Interface,
+        zope.interface.Interface,
+        zope.schema.interfaces.IBytes,
+        interfaces.IFileWidget)
+    # only FileUploadDataConverter seems to use NOT_CHANGED, but that needs
+    # to be validated, because file upload is a special case
+    # the most special case if when an ad-hoc IBytes field is required
+
+
 def WidgetValidatorDiscriminators(
     validator, context=None, request=None, view=None, field=None, widget=None):
     zope.component.adapter(
@@ -103,6 +145,7 @@
 
     """
 
+
 class Data(object):
     zope.interface.implements(interfaces.IData)
 

Modified: z3c.form/trunk/src/z3c/form/validator.txt
===================================================================
--- z3c.form/trunk/src/z3c/form/validator.txt	2012-09-12 12:50:30 UTC (rev 127834)
+++ z3c.form/trunk/src/z3c/form/validator.txt	2012-09-12 14:57:54 UTC (rev 127835)
@@ -11,7 +11,7 @@
   The schema field consists of a ``validate()`` method. Validation is
   automatically invoked when converting a unicode string to a field value
   using ``fromUnicode()``. This makes it very hard to customize the field
-  validation. No hooks were provided to exert dditional restriction at the
+  validation. No hooks were provided to exert additional restriction at the
   presentation level.
 
 * Schema/Form Validation
@@ -74,7 +74,8 @@
   ...     login = zope.schema.TextLine(
   ...         title=u'Login',
   ...         min_length=1,
-  ...         max_length=10)
+  ...         max_length=10,
+  ...         required=True)
   ...
   ...     email = zope.schema.TextLine(
   ...         title=u'E-mail')
@@ -83,6 +84,10 @@
   ...     def isLoginPartOfEmail(person):
   ...         if not person.email.startswith(person.login):
   ...             raise zope.interface.Invalid("The login not part of email.")
+  >>> class Person(object):
+  ...     zope.interface.implements(IPerson)
+  ...     login = None
+  ...     email = None
 
 
 Widget Validators
@@ -181,6 +186,73 @@
   ...     (None, None, None, IPerson['email'], None),
   ...     interfaces.IValidator)
 
+Ignoring unchanged values
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Most of the time we want to ignore unchanged fields/values at validation.
+A common usecase for this is if a value went away from a vocabulary and we want
+to keep the old value after editing.
+In case you want to strict behaviour, register ``StrictSimpleFieldValidator``
+for your layer.
+
+  >>> simple = validator.SimpleFieldValidator(
+  ...     None, None, None, IPerson['login'], None)
+
+NOT_CHANGED never gets validated.
+
+  >>> simple.validate(interfaces.NOT_CHANGED)
+
+Current value gets extracted by ``IDataManager``
+via the widget, field and context
+
+  >>> from z3c.form.datamanager import AttributeField
+  >>> zope.component.provideAdapter(AttributeField)
+
+  >>> import z3c.form.testing
+  >>> request = z3c.form.testing.TestRequest()
+  >>> import z3c.form.widget
+  >>> widget = z3c.form.widget.Widget(request)
+  >>> context = Person()
+
+  >>> widget.context = context
+  >>> from z3c.form import interfaces
+  >>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
+
+  >>> simple = validator.SimpleFieldValidator(
+  ...     context, request, None, IPerson['login'], widget)
+
+OK, let's see checking after setup.
+Works like a StrictSimpleFieldValidator until we have to validate a different value:
+
+  >>> context.login = u'john'
+  >>> simple.validate(u'carter')
+  >>> simple.validate(u'hippocratiusxy')
+  Traceback (most recent call last):
+  ...
+  TooLong: (u'hippocratiusxy', 10)
+
+Validating the unchanged value works despite it would be an error.
+
+  >>> context.login = u'hippocratiusxy'
+  >>> simple.validate(u'hippocratiusxy')
+
+Unless we want to force validation:
+
+  >>> simple.validate(u'hippocratiusxy', force=True)
+  Traceback (most recent call last):
+  ...
+  TooLong: (u'hippocratiusxy', 10)
+
+Some exceptions:
+
+``missing_value`` gets validated
+
+  >>> simple.validate(IPerson['login'].missing_value)
+  Traceback (most recent call last):
+  ...
+  RequiredMissing: login
+
+
 Widget Validators and File-Uploads
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -194,7 +266,8 @@
 
   >>> class IPhoto(zope.interface.Interface):
   ...     data = zope.schema.Bytes(
-  ...         title=u'Photo')
+  ...         title=u'Photo',
+  ...         required=True)
   ...
   ...     thumb = zope.schema.Bytes(
   ...         title=u'Thumbnail',
@@ -211,11 +284,11 @@
 required one has an required missing error, as the default value of
 the field is looked up on the field:
 
-  >>> simple_thumb = validator.SimpleFieldValidator(
+  >>> simple_thumb = validator.StrictSimpleFieldValidator(
   ...     None, None, None, IPhoto['thumb'], None)
   >>> simple_thumb.validate(interfaces.NOT_CHANGED)
 
-  >>> simple_data = validator.SimpleFieldValidator(
+  >>> simple_data = validator.StrictSimpleFieldValidator(
   ...     None, None, None, IPhoto['data'], None)
   >>> simple_data.validate(interfaces.NOT_CHANGED)
   Traceback (most recent call last):
@@ -231,11 +304,11 @@
   >>> widget = z3c.form.widget.Widget(None)
   >>> zope.interface.alsoProvides(widget, interfaces.IContextAware)
   >>> widget.ignoreContext = True
-  >>> simple_thumb = validator.SimpleFieldValidator(
+  >>> simple_thumb = validator.StrictSimpleFieldValidator(
   ...     None, None, None, IPhoto['thumb'], widget)
   >>> simple_thumb.validate(interfaces.NOT_CHANGED)
 
-  >>> simple_data = validator.SimpleFieldValidator(
+  >>> simple_data = validator.StrictSimpleFieldValidator(
   ...     None, None, None, IPhoto['data'], widget)
   >>> simple_data.validate(interfaces.NOT_CHANGED)
   Traceback (most recent call last):
@@ -271,14 +344,14 @@
   >>> widget.ignoreContext = False
   >>> zope.component.provideAdapter(z3c.form.datamanager.AttributeField)
 
-  >>> simple_thumb = validator.SimpleFieldValidator(
+  >>> simple_thumb = validator.StrictSimpleFieldValidator(
   ...     photo, None, None, IPhoto['thumb'], widget)
   >>> simple_thumb.validate(interfaces.NOT_CHANGED)
 
 If the value is not set on the context it is a required missing as
 neither context nor input have a valid value:
 
-  >>> simple_data = validator.SimpleFieldValidator(
+  >>> simple_data = validator.StrictSimpleFieldValidator(
   ...     photo, None, None, IPhoto['data'], widget)
   >>> simple_data.validate(interfaces.NOT_CHANGED)
   Traceback (most recent call last):

Copied: z3c.form/trunk/src/z3c/form/validator.zcml (from rev 127832, z3c.form/branches/adamg-missing-terms/src/z3c/form/validator.zcml)
===================================================================
--- z3c.form/trunk/src/z3c/form/validator.zcml	                        (rev 0)
+++ z3c.form/trunk/src/z3c/form/validator.zcml	2012-09-12 14:57:54 UTC (rev 127835)
@@ -0,0 +1,16 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="z3c.form">
+
+  <!-- Validators -->
+  <adapter
+      factory=".validator.SimpleFieldValidator"
+      />
+  <adapter
+      factory=".validator.FileUploadValidator"
+      />
+  <adapter
+      factory=".validator.InvariantsValidator"
+      />
+
+</configure>



More information about the checkins mailing list