[Checkins] SVN: z3c.form/branches/adamg-missing-terms/src/z3c/form/ trying to display/accept terms for IChoice that want away

Adam Groszer cvs-admin at zope.org
Thu Sep 6 09:26:09 UTC 2012


Log message for revision 127744:
  trying to display/accept terms for IChoice that want away

Changed:
  A   z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt
  U   z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py
  A   z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt

-=-
Added: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt	                        (rev 0)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt	2012-09-06 09:26:05 UTC (rev 127744)
@@ -0,0 +1,213 @@
+============================
+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)
+
+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
+
+  >>> class SelectionTerms(z3c.form.term.MissingChoiceTermsVocabulary):
+  ...     def __init__(self, context, request, form, field, widget):
+  ...         self.terms = SimpleVocabulary.fromValues(['a', 'b', 'c'])
+  ...
+  ...     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) )
+
+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="a">a</option>
+  <option id="widget-id-1" value="b">b</option>
+  <option id="widget-id-2" value="c">c</option>
+  </select>
+  <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+If we select item "x", then it should be selected:
+
+  >>> widget.value = ['x']
+  >>> 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="a">a</option>
+  <option id="widget-id-1" value="b">b</option>
+  <option id="widget-id-2" value="c">c</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 select item "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="a">a</option>
+  <option id="widget-id-1" value="b">b</option>
+  <option id="widget-id-2" value="c">c</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()
+  ['c']
+
+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)
+
+  >>> widget.required = True
+  >>> widget.mode = interfaces.DISPLAY_MODE
+  >>> widget.value = ['b', 'c']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <span id="widget-id" class="select-widget required">
+    <span class="selected-option">b</span>,
+    <span class="selected-option">c</span>
+  </span>
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+  >>> widget.value = ['b', 'x', 'y']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <span id="widget-id" class="select-widget required">
+  <span class="selected-option">b</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)
+
+We can now set our widget's mode to hidden and render it:
+
+  >>> widget.mode = interfaces.HIDDEN_MODE
+  >>> widget.value = ['b']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <input type="hidden" name="widget.name:list"
+         class="hidden-widget" value="b" id="widget-id-1" />
+  <input name="widget.name-empty-marker" type="hidden"
+         value="1" />
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+  >>> widget.value = ['b', 'x', 'y']
+  >>> widget.update()
+  >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+  <input id="widget-id-1" name="widget.name:list" value="b" 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" />


Property changes on: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision
Added: svn:eol-style
   + native

Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py	2012-09-06 09:26:05 UTC (rev 127744)
@@ -66,16 +66,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/branches/adamg-missing-terms/src/z3c/form/browser/tests.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py	2012-09-06 09:26:05 UTC (rev 127744)
@@ -94,6 +94,11 @@
                      optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
                      checker=checker,
                      ),
+        DocFileSuite('select-miss.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,

Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml	2012-09-06 09:26:05 UTC (rev 127744)
@@ -87,6 +87,9 @@
       factory=".term.ChoiceTermsVocabulary"
       />
   <adapter
+      factory=".term.MissingChoiceTermsVocabulary"
+      />
+  <adapter
       factory=".term.ChoiceTermsSource"
       />
   <adapter

Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py	2012-09-06 09:26:05 UTC (rev 127744)
@@ -1110,3 +1110,17 @@
 
 class IAfterWidgetUpdateEvent(IWidgetEvent):
     """An event sent out after the widget was updated."""
+
+
+
+
+class IForgivingChoice(zope.schema.interfaces.IChoice):
+    pass
+
+class ForgivingChoice(zope.schema.Choice):
+    zope.interface.implements(IForgivingChoice)
+
+    def _validate(self, value):
+        if self.context is not None:
+            if self.query(self.context) == value:
+                return

Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py	2012-09-06 09:26:05 UTC (rev 127744)
@@ -37,7 +37,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 +48,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 +124,54 @@
         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:
+            return self.makeMissingTerm(value)
+
+    def makeToken(self, value):
+        return unicode(value).encode('utf8').encode('base64').strip()
+
+    def makeMissingTerm(self, value, token=None):
+        """Return a term that should be displayed for the missing token or
+        raise LookupError if it's really invalid"""
+        if token is None:
+            token = self.makeToken(value)
+        return vocabulary.SimpleTerm(value, token,
+            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.widget.field),
+                    interfaces.IDataManager).query()
+                return self.makeMissingTerm(value, token=token)
+
+            raise LookupError(token)
+
+
+class MissingChoiceTermsVocabulary(MissingTermsMixin, ChoiceTermsVocabulary):
+    """ITerms adapter for zope.schema.IChoice based implementations using
+    vocabulary with missing terms support"""
+
+    zope.component.adapts(
+        zope.interface.Interface,
+        interfaces.IFormLayer,
+        zope.interface.Interface,
+        interfaces.IForgivingChoice,
+        zope.schema.interfaces.IBaseVocabulary,
+        interfaces.IWidget)
+
+
 class ChoiceTermsSource(SourceTerms):
     "ITerms adapter for zope.schema.IChoice based implementations using source."
 
@@ -163,6 +212,7 @@
                               (False, 'false', self.falseLabel)]]
         self.terms = vocabulary.SimpleVocabulary(terms)
 
+
 @zope.interface.implementer(interfaces.ITerms)
 @zope.component.adapter(
     zope.interface.Interface,
@@ -176,6 +226,7 @@
         (context, request, form, field, terms, widget),
         interfaces.ITerms)
 
+
 class CollectionTermsVocabulary(Terms):
     """ITerms adapter for zope.schema.ICollection based implementations using
     vocabulary."""
@@ -196,6 +247,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/branches/adamg-missing-terms/src/z3c/form/term.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt	2012-09-06 09:26:05 UTC (rev 127744)
@@ -134,6 +134,23 @@
   >>> [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')
+  >>> term.token
+  '42'
+  >>> term.value
+  '42'
+  >>> term.title
+  u'Missing: ${token}'
+
 Bool fields
 +++++++++++
 

Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py	2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py	2012-09-06 09:26:05 UTC (rev 127744)
@@ -104,6 +104,12 @@
             checker=checker,
             ),
         doctest.DocFileSuite(
+            '../widget-miss.txt',
+            setUp=setUp, tearDown=testing.tearDown,
+            optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+            checker=checker,
+            ),
+        doctest.DocFileSuite(
             '../button.txt',
             setUp=setUp, tearDown=testing.tearDown,
             optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,

Added: z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt	                        (rev 0)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt	2012-09-06 09:26:05 UTC (rev 127744)
@@ -0,0 +1,766 @@
+=======
+Widgets
+=======
+
+Widgets are small UI components that accept and process the textual user
+input. The only responsibility of a widget is to represent a value to the
+user, allow it to be modified and then return a new value. Good examples of
+widgets include the Qt widgets and HTML widgets. The widget is not responsible
+for converting its value to the desired internal value or validate the
+incoming data. These responsibilities are passed data converters and
+validators, respectively.
+
+There are several problems that can be identified in the original Zope 3 widget
+implementation located at ``zope.app.form``.
+
+(1) Field Dependence -- Widgets are always views of fields. While this might
+    be a correct choice for a high-level API, it is fundamentally wrong. It
+    disallows us to use widgets without defining fields. This also couples
+    certain pieces of information too tightly to the field, especially, value
+    retrieval from and storage to the context, validation and raw data
+    conversion.
+
+(2) Form Dependence -- While widgets do not have to be located within a form,
+    they are usually tightly coupled to it. It is very difficult to use
+    widgets outside the context of a form.
+
+(3) Traversability -- Widgets cannot be traversed, which means that they
+    cannot interact easily using Javascript. This is not a fundamental
+    problem, but simply a lack of the current design to recognize that small
+    UI components must also be traversable and thus have a URI.
+
+(4) Customizability -- A consequence of issue (1) is that widgets are not
+    customizable enough. Implementing real-world projects has shown that
+    widgets often want a very fine-grained ability to customize values. A
+    prime example is the label. Because the label of a widget is retrieved
+    from the field title, it is impossible to provide an alternative label for
+    a widget. While the label could be changed from the form, this would
+    require rewriting the entire form to change a label. Instead, we often
+    endde up writing cusom schemas.
+
+(5) Flexibility -- Oftentimes it is desired to have one widget, but multiple
+    styles of representation. For example, in one scenario the widget uses a
+    plain HTML widget and in another a fancy JavaScript widget is used. The
+    current implementation makes it very hard to provide alternative styles
+    for a widget.
+
+
+Creating and Using Simple Widgets
+---------------------------------
+
+When using the widget API by itself, the simplest way to use it is to just
+instantiate it using the request:
+
+  >>> from z3c.form.testing import TestRequest
+  >>> from z3c.form import widget
+  >>> request = TestRequest()
+  >>> age = widget.Widget(request)
+
+In this case we instantiated a generic widget. A full set of simple
+browser-based widgets can be found in the ``browser/`` package. Since no
+helper components are around to fill the attributes of the widget, we have to
+do it by hand:
+
+  >>> age.name = 'age'
+  >>> age.label = u'Age'
+  >>> age.value = '39'
+
+The most important attributes are the "name" and the "value". The name is used
+to identify the widget within the form. The value is either the value to be
+manipulated or the default value. The value must be provided in the form the
+widget needs it. It is the responsibility of a data converter to convert
+between the widget value and the desired internal value.
+
+Before we can render the widget, we have to register a template for the
+widget. The first step is to define the template:
+
+  >>> import tempfile
+  >>> textWidgetTemplate = tempfile.mktemp('text.pt')
+  >>> open(textWidgetTemplate, 'w').write('''
+  ... <html xmlns="http://www.w3.org/1999/xhtml"
+  ...       xmlns:tal="http://xml.zope.org/namespaces/tal"
+  ...       tal:omit-tag="">
+  ...    <input type="text" name="" value=""
+  ...           tal:attributes="name view/name; value view/value;" />
+  ... </html>
+  ... ''')
+
+Next, we have to create a template factory for the widget:
+
+  >>> from z3c.form.widget import WidgetTemplateFactory
+  >>> factory = WidgetTemplateFactory(
+  ...     textWidgetTemplate, widget=widget.Widget)
+
+The first argument, which is also required, is the path to the template
+file. An optional ``content_type`` keyword argument allows the developer to
+specify the output content type, usually "text/html". Then there are five
+keyword arguments that specify the discriminators of the template:
+
+* ``context`` -- This is the context in which the widget is displayed. In a
+  simple widget like the one we have now, the context is ``None``.
+
+* ``request`` -- This discriminator allows you to specify the type of request
+  for which the widget will be available. In our case this would be a browser
+  request. Note that browser requests can be further broken into layer, so you
+  could also specify a layer interface here.
+
+* ``view`` -- This is the view from which the widget is used. The simple
+  widget at hand, does not have a view associated with it though.
+
+* ``field`` -- This is the field for which the widget provides a
+  representation. Again, this simple widget does not use a field, so it is
+  ``None``.
+
+* ``widget`` -- This is the widget itself. With this discriminator you can
+  specify for which type of widget you are providing a template.
+
+We can now register the template factory. The name of the factory is the mode
+of the widget. By default, there are two widget modes: "input" and
+"display". However, since the mode is just a string, one can develop other
+kinds of modes as needed for a project. The default mode is "input":
+
+  >>> from z3c.form import interfaces
+  >>> age.mode is interfaces.INPUT_MODE
+  True
+
+  >>> import zope.component
+  >>> zope.component.provideAdapter(factory, name=interfaces.INPUT_MODE)
+
+Once everything is set up, the widget is updated and then rendered:
+
+  >>> age.update()
+  >>> print age.render()
+  <input type="text" name="age" value="39" />
+
+If a value is found in the request, it takes precedence, since the user
+entered the value:
+
+  >>> age.request = TestRequest(form={'age': '25'})
+  >>> age.update()
+  >>> print age.render()
+  <input type="text" name="age" value="25" />
+
+However, there is an option to turn off all request data:
+
+  >>> age.value = '39'
+  >>> age.ignoreRequest = True
+  >>> age.update()
+  >>> print age.render()
+  <input type="text" name="age" value="39" />
+
+
+Creating and Using Field Widgets
+--------------------------------
+
+An extended form of the widget allows fields to control several of the
+widget's properties. Let's create a field first:
+
+  >>> ageField = zope.schema.Int(
+  ...     __name__ = 'age',
+  ...     title = u'Age',
+  ...     min = 0,
+  ...     max = 130)
+
+We can now use our simple widget and create a field widget from it:
+
+  >>> ageWidget = widget.FieldWidget(ageField, age)
+
+Such a widget provides ``IFieldWidget``:
+
+  >>> interfaces.IFieldWidget.providedBy(ageWidget)
+  True
+
+Of course, this is more commonly done using an adapter. Commonly those
+adapters look like this:
+
+  >>> @zope.component.adapter(zope.schema.Int, TestRequest)
+  ... @zope.interface.implementer(interfaces.IFieldWidget)
+  ... def IntWidget(field, request):
+  ...     return widget.FieldWidget(field, widget.Widget(request))
+
+  >>> zope.component.provideAdapter(IntWidget)
+  >>> ageWidget = zope.component.getMultiAdapter((ageField, request),
+  ...     interfaces.IFieldWidget)
+
+Now we just have to update and render the widget:
+
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" />
+
+There is no initial value for the widget, since there is no value in the
+request and the field does not provide a default. Let's now give our field a
+default value and see what happens:
+
+  >>> ageField.default = 30
+  >>> ageWidget.update()
+  Traceback (most recent call last):
+  ...
+  TypeError: ('Could not adapt', <Widget 'age'>,
+              <InterfaceClass z3c.form.interfaces.IDataConverter>)
+
+In order for the widget to be able to take the field's default value and use
+it to provide an initial value the widget, we need to provide a data converter
+that defines how to convert from the field value to the widget value.
+
+  >>> from z3c.form import converter
+  >>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
+  >>> zope.component.provideAdapter(converter.FieldDataConverter)
+
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="30" />
+
+Again, the request value is honored above everything else:
+
+  >>> ageWidget.request = TestRequest(form={'age': '25'})
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="25" />
+
+
+Creating and Using Context Widgets
+----------------------------------
+
+When widgets represent an attribute value of an object, then this object must
+be set as the context of the widget:
+
+  >>> class Person(object):
+  ...     age = 45
+  >>> person = Person()
+
+  >>> ageWidget.context = person
+  >>> zope.interface.alsoProvides(ageWidget, interfaces.IContextAware)
+
+The result is that the context value takes over precendence over the default
+value:
+
+  >>> ageWidget.request = TestRequest()
+  >>> ageWidget.update()
+  Traceback (most recent call last):
+  ...
+  ComponentLookupError: ((...), <InterfaceClass ...IDataManager>, u'')
+
+This call fails because the widget does not know how to extract the value from
+the context. Registering a data manager for the widget does the trick:
+
+  >>> from z3c.form import datamanager
+  >>> zope.component.provideAdapter(datamanager.AttributeField)
+
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="45" />
+
+If the context value is unknown (None), the default value kicks in.
+
+  >>> person.age = None
+
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="30" />
+
+Unless the widget is explicitely asked to not to show defaults.
+This is handy for EditForms.
+
+  >>> ageWidget.showDefault = False
+
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="" />
+
+  >>> ageWidget.showDefault = True
+  >>> person.age = 45
+
+The context can be explicitely ignored, making the widget display the default
+value again:
+
+  >>> ageWidget.ignoreContext = True
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="30" />
+
+Again, the request value is honored above everything else:
+
+  >>> ageWidget.request = TestRequest(form={'age': '25'})
+  >>> ageWidget.ignoreContext = False
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="25" />
+
+But what happens if the object we are working on is security proxied? In
+particular, what happens, if the access to the attribute is denied. To see
+what happens, we have to create a proxied person:
+
+  >>> from zope.security import checker
+  >>> PersonChecker = checker.Checker({'age': 'Access'}, {'age': 'Edit'})
+
+  >>> ageWidget.request = TestRequest()
+  >>> ageWidget.context = checker.ProxyFactory(Person(), PersonChecker)
+
+After changing the security policy, ...
+
+  >>> from zope.security import management
+  >>> from z3c.form import testing
+  >>> management.endInteraction()
+  >>> newPolicy = testing.SimpleSecurityPolicy()
+  >>> oldPolicy = management.setSecurityPolicy(newPolicy)
+  >>> management.newInteraction()
+
+it is not possible anymore to update the widget:
+
+  >>> ageWidget.update()
+  Traceback (most recent call last):
+  ...
+  Unauthorized: (<Person object at ...>, 'age', 'Access')
+
+If no security declaration has been made at all, we get a
+``ForbiddenAttribute`` error:
+
+  >>> ageWidget.context = checker.ProxyFactory(Person(), checker.Checker({}))
+  >>> ageWidget.update()
+  Traceback (most recent call last):
+  ...
+  ForbiddenAttribute: ('age', <Person object at ...>)
+
+Let's clean up the setup:
+
+  >>> management.endInteraction()
+  >>> newPolicy = management.setSecurityPolicy(oldPolicy)
+  >>> management.newInteraction()
+
+  >>> ageWidget.context = Person()
+
+
+Dynamically Changing Attribute Values
+-------------------------------------
+
+Once widgets are used within a framework, it is very tedious to write Python
+code to adjust certain attributes, even though hooks exist. The easiest way to
+change those attribute values is actually to provide an adapter that provides
+the custom value.
+
+We can create a custom label for the age widget:
+
+  >>> AgeLabel = widget.StaticWidgetAttribute(
+  ...     u'Current Age',
+  ...     context=None, request=None, view=None, field=ageField, widget=None)
+
+Clearly, this code does not require us to touch the orginal form and widget
+code, given that we have enough control over the selection. In the example
+above, all the selection discriminators are listed for demonstration
+purposes. Of course, the label in this case can be created as follows:
+
+  >>> AgeLabel = widget.StaticWidgetAttribute(u'Current Age', field=ageField)
+
+Much better, isn't it? Initially the label is the title of the field:
+
+  >>> ageWidget.label
+  u'Age'
+
+Let's now simply register the label as a named adapter; the name is the name
+of the attribute to change:
+
+  >>> zope.component.provideAdapter(AgeLabel, name='label')
+
+Asking the widget for the label now will return the newly registered label:
+
+  >>> ageWidget.update()
+  >>> ageWidget.label
+  u'Current Age'
+
+Of course, simply setting the label or changing the label extraction via a
+sub-class are other options you might want to consider. Furthermore, you
+could also create a computed attribute value or implement your own component.
+
+Overriding other attributes, such as ``required``, is done in the same
+way. If any widget provides new attributes, they are also overridable this
+way. For example, the selection widget defines a label for the option that no
+value was selected. We often want to override this, because the German
+translation sucks or the wording is often too generic. Widget implementation
+should add names of overridable attributes to their "_adapterValueAttributes"
+internal attribute.
+
+Let's try to override the ``required`` attribute. By default the widget is required,
+because the field is required as well:
+
+  >>> ageWidget.required
+  True
+
+Let's provide a static widget attribute adapter with name "required":
+
+  >>> AgeNotRequired = widget.StaticWidgetAttribute(False, field=ageField)
+  >>> zope.component.provideAdapter(AgeNotRequired, name="required")
+
+Now, let's check if it works:
+
+  >>> ageWidget.update()
+  >>> ageWidget.required
+  False
+
+Overriding the default value is somewhat special due to the complexity of
+obtaining the value. So let's register one now:
+
+  >>> AgeDefault = widget.StaticWidgetAttribute(50, field=ageField)
+  >>> zope.component.provideAdapter(AgeDefault, name="default")
+
+Let's now instantiate, update and render the widget to see the default value:
+
+  >>> ageWidget = zope.component.getMultiAdapter((ageField, request),
+  ...     interfaces.IFieldWidget)
+  >>> ageWidget.update()
+  >>> print ageWidget.render()
+  <input type="text" name="age" value="50" />
+
+
+Sequence Widget
+---------------
+
+A common use case in user interfaces is to ask the user to select one or more
+items from a set of options/choices. The ``widget`` module provides a basic
+widget implementation to support this use case.
+
+The options available for selections are known as terms. Initially, there are
+no terms:
+
+  >>> request = TestRequest()
+  >>> seqWidget = widget.SequenceWidget(request)
+  >>> seqWidget.name = 'seq'
+
+  >>> seqWidget.terms is None
+  True
+
+There are two ways terms can be added, either manually or via an
+adapter. Those term objects must provide ``ITerms``. There is no simple
+default implementation, so we have to provide one ourselves:
+
+  >>> from zope.schema import vocabulary
+  >>> class Terms(vocabulary.SimpleVocabulary):
+  ...     zope.interface.implements(interfaces.ITerms)
+  ...     def getValue(self, token):
+  ...         return self.getTermByToken(token).value
+
+  >>> terms = Terms(
+  ...   [Terms.createTerm(1, 'v1', u'Value 1'),
+  ...    Terms.createTerm(2, 'v2', u'Value 2'),
+  ...    Terms.createTerm(3, 'v3', u'Value 3')])
+  >>> seqWidget.terms = terms
+
+Once the ``terms`` attribute is set, updating the widgets does not change the
+terms:
+
+  >>> seqWidget.update()
+  >>> [term.value for term in seqWidget.terms]
+  [1, 2, 3]
+
+The value of a sequence widget is a tuple/list of term tokens. When extracting
+values from the request, the values must be valid tokens, otherwise the
+default value is returned:
+
+  >>> seqWidget.request = TestRequest(form={'seq': ['v1']})
+  >>> seqWidget.extract()
+  ['v1']
+
+  >>> seqWidget.request = TestRequest(form={'seq': ['v4']})
+  >>> seqWidget.extract()
+  <NO_VALUE>
+
+  >>> seqWidget.request = TestRequest(form={'seq-empty-marker': '1'})
+  >>> 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:
+
+  >>> seqWidget.request = TestRequest(form={'seq': [seqWidget.noValueToken]})
+  >>> seqWidget.extract()
+  ['--NOVALUE--']
+
+Since the value of the widget is a tuple of tokens, when displaying the
+values, they have to be converted to the title of the term:
+
+  >>> seqWidget.value = ('v1', 'v2')
+  >>> seqWidget.displayValue
+  [u'Value 1', u'Value 2']
+
+When input forms are directly switched to display forms within the same
+request, it can happen that the value contains the "--NOVALUE--" token
+entry. This entry should be silently ignored:
+
+  >>> seqWidget.value = (seqWidget.noValueToken,)
+  >>> seqWidget.displayValue
+  []
+
+To demonstrate how the terms is automatically chosen by a widget, we should
+instantiate a field widget. Let's do this with a choice field:
+
+  >>> seqField = zope.schema.Choice(
+  ...     title=u'Sequence Field',
+  ...     vocabulary=terms)
+
+Let's now create the field widget:
+
+  >>> seqWidget = widget.FieldWidget(seqField, widget.SequenceWidget(request))
+  >>> seqWidget.terms
+
+The terms should be available as soon as the widget is updated:
+
+  >>> seqWidget.update()
+  Traceback (most recent call last):
+  ...
+  ComponentLookupError: ((...), <InterfaceClass ...ITerms>, u'')
+
+This failed, because we did not register an adapter for the terms yet. After
+the adapter is registered, everything should work as expected:
+
+  >>> from z3c.form import term
+  >>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
+  >>> zope.component.provideAdapter(term.ChoiceTerms)
+
+  >>> seqWidget.update()
+  >>> seqWidget.terms
+  <z3c.form.term.ChoiceTermsVocabulary object at ...>
+
+So that's it. Everything else is the same from then on.
+
+
+Multi Widget
+------------
+
+A common use case in user interfaces is to ask the user to define one or more
+items. The ``widget`` module provides a basic widget implementation to support
+this use case.
+
+The `MultiWidget` allows to store none, one or more values for a sequence
+field.  Don't get confused by the term sequence. The sequence used in
+`SequenceWidget` means that the widget can choose from a sequence of values
+which is really a collection. The `MultiWidget` can collect values to build
+and store a sequence of values like those used in `ITuple` or `IList` field.
+
+  >>> request = TestRequest()
+  >>> multiWidget = widget.MultiWidget(request)
+  >>> multiWidget.name = 'multi.name'
+  >>> multiWidget.id = 'multi-id'
+
+  >>> multiWidget.value
+  []
+
+Let's define a field for our multi widget:
+
+  >>> multiField = zope.schema.List(
+  ...     value_type=zope.schema.Int(default=42))
+  >>> multiWidget.field = multiField
+
+The value of a multi widget is always list. When extracting values from the
+request, the values must be a list of valid values based on the value_type
+field used from the used sequence field. The widget also uses a counter which
+is required for processing the input from a request. The counter is a marker
+for build the right amount of enumerated widgets.
+
+If we provide no request we will get no value:
+
+  >>> multiWidget.extract()
+  <NO_VALUE>
+
+If we provide an empty counter we will get an empty list.
+This is accordance with Widget.extract(), where a missing request value
+is <NO_VALUE> and an empty ('') request value is ''.
+
+  >>> multiWidget.request = TestRequest(form={'multi.name.count':'0'})
+  >>> multiWidget.extract()
+  []
+
+If we provide real values within the request, we will get it back:
+
+  >>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
+  ...                                         'multi.name.0':u'42',
+  ...                                         'multi.name.1':u'43'})
+  >>> multiWidget.extract()
+  [u'42', u'43']
+
+If we provide a bad value we will get the bad value within the extract method.
+Our widget update process will validate this bad value later:
+
+  >>> multiWidget.request = TestRequest(form={'multi.name.count':'1',
+  ...                                         'multi.name.0':u'bad'})
+  >>> multiWidget.extract()
+  [u'bad']
+
+Storing a widget value forces to update the (sub) widgets. This forces also to
+validate the (sub) widget values. To show this we need to register a
+validator:
+
+  >>> from z3c.form.validator import SimpleFieldValidator
+  >>> zope.component.provideAdapter(SimpleFieldValidator)
+
+Since the value of the widget is a list of (widget) value items, when
+displaying the values, they can be used as they are:
+
+  >>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
+  ...                                         'multi.name.0':u'42',
+  ...                                         'multi.name.1':u'43'})
+  >>> multiWidget.value = multiWidget.extract()
+  >>> multiWidget.value
+  [u'42', u'43']
+
+Each widget normally gets first processed by it's update method call after
+initialization. This update call forces to call extract, which first will get
+the right amount of (sub) widgets by the given counter value. Based on that
+counter value the right amount of widgets will get created. Each widget will
+return it's own value and this collected values get returned by the extract
+method. The multi widget update method will then store this values if any given
+as multi widget value argument. If extract doesn't return a value the multi
+widget update method will use it's default value. If we store a given value
+from the extract as multi widget value, this will force to setup the multi
+widget widgets based on the given values and apply the right value for them.
+After that the multi widget is ready for rendering. The good thing about that
+pattern is that it is possible to set a value before or after the update method
+is called. At any time if we change the multi widget value the (sub) widgets
+get updated within the new relevant value.
+
+  >>> multiRequest = TestRequest(form={'multi.name.count':'2',
+  ...                                  'multi.name.0':u'42',
+  ...                                  'multi.name.1':u'43'})
+
+  >>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(
+  ...     multiRequest))
+  >>> multiWidget.name = 'multi.name'
+  >>> multiWidget.value
+  []
+
+  >>> multiWidget.update()
+
+  >>> multiWidget.widgets[0].value
+  u'42'
+
+  >>> multiWidget.widgets[1].value
+  u'43'
+
+  >>> multiWidget.value
+  [u'42', u'43']
+
+MultiWidget also declares the ``allowAdding`` and ``allowRemoving``
+attributes that can be used in browser presentation to control add/remove
+button availability. To ease working with common cases, the
+``updateAllowAddRemove`` method provided that will set those attributes
+in respect to field's min_length and max_length, if the field provides
+zope.schema.interfaces.IMinMaxLen interface.
+
+Let's define a field with min and max length constraints and create
+a widget for it.
+
+  >>> multiField = zope.schema.List(
+  ...     value_type=zope.schema.Int(),
+  ...     min_length=2,
+  ...     max_length=5)
+
+  >>> request = TestRequest()
+  >>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(request))
+
+Lets ensure that the minimum number of widgets are created.
+
+  >>> multiWidget.update()
+  >>> len(multiWidget.widgets)
+  2
+
+Now, let's check if the function will do the right thing depending on
+the value:
+
+No value:
+
+  >>> multiWidget.updateAllowAddRemove()
+  >>> multiWidget.allowAdding, multiWidget.allowRemoving
+  (True, False)
+
+Minimum length:
+
+  >>> multiWidget.value = [u'3', u'5']
+  >>> multiWidget.updateAllowAddRemove()
+  >>> multiWidget.allowAdding, multiWidget.allowRemoving
+  (True, False)
+
+Some allowed length:
+
+  >>> multiWidget.value = [u'3', u'5', u'8', u'6']
+  >>> multiWidget.updateAllowAddRemove()
+  >>> multiWidget.allowAdding, multiWidget.allowRemoving
+  (True, True)
+
+Maximum length:
+
+  >>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42']
+  >>> multiWidget.updateAllowAddRemove()
+  >>> multiWidget.allowAdding, multiWidget.allowRemoving
+  (False, True)
+
+Over maximum length:
+
+  >>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42', u'45']
+  >>> multiWidget.updateAllowAddRemove()
+  >>> multiWidget.allowAdding, multiWidget.allowRemoving
+  (False, True)
+
+I know a guy who once switched widget mode in the middle. All simple widgets
+are easy to hack, but multiWidget needs to update all subwidgets:
+
+  >>> [w.mode for w in multiWidget.widgets]
+  ['input', 'input', 'input', 'input', 'input', 'input']
+
+Switch the multiWidget mode:
+
+  >>> multiWidget.mode = interfaces.DISPLAY_MODE
+
+Yes, all subwidgets switch mode:
+
+  >>> [w.mode for w in multiWidget.widgets]
+  ['display', 'display', 'display', 'display', 'display', 'display']
+
+Widget Events
+-------------
+
+Widget-system interaction can be very rich and wants to be extended in
+unexpected ways. Thus there exists a generic widget event that can be used by
+other code.
+
+  >>> event = widget.WidgetEvent(ageWidget)
+  >>> event
+  <WidgetEvent <Widget 'age'>>
+
+These events provide the ``IWidgetEvent`` interface:
+
+  >>> interfaces.IWidgetEvent.providedBy(event)
+  True
+
+There exists a special event that can be send out after a widget has been
+updated, ...
+
+  >>> afterUpdate = widget.AfterWidgetUpdateEvent(ageWidget)
+  >>> afterUpdate
+  <AfterWidgetUpdateEvent <Widget 'age'>>
+
+which provides another special interface:
+
+  >>> interfaces.IAfterWidgetUpdateEvent.providedBy(afterUpdate)
+  True
+
+This event should be used by widget-managing components and is not created and
+sent out internally by the widget's ``update()`` method. The event was
+designed to provide an additional hook between updating the widget and
+rendering it.
+
+
+Cleanup
+-------
+
+Let's not leave temporary files lying around
+
+  >>> import os
+  >>> os.remove(textWidgetTemplate)


Property changes on: z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt
___________________________________________________________________
Added: svn:keywords
   + Date Author Id Revision
Added: svn:eol-style
   + native



More information about the checkins mailing list