[Zope3-checkins] CVS: Zope3/src/zope/app/browser/form - configure.zcml:1.9.2.1 editview.py:1.23.8.1 meta.zcml:1.10.12.1 schemadisplay.py:1.2.12.1 vocabularywidget.py:1.6.2.1 widget.py:1.30.2.1

Grégoire Weber zope@i-con.ch
Sun, 22 Jun 2003 10:24:10 -0400


Update of /cvs-repository/Zope3/src/zope/app/browser/form
In directory cvs.zope.org:/tmp/cvs-serv24874/src/zope/app/browser/form

Modified Files:
      Tag: cw-mail-branch
	configure.zcml editview.py meta.zcml schemadisplay.py 
	vocabularywidget.py widget.py 
Log Message:
Synced up with HEAD

=== Zope3/src/zope/app/browser/form/configure.zcml 1.9 => 1.9.2.1 ===
--- Zope3/src/zope/app/browser/form/configure.zcml:1.9	Wed May 21 20:39:33 2003
+++ Zope3/src/zope/app/browser/form/configure.zcml	Sun Jun 22 10:22:39 2003
@@ -104,6 +104,8 @@
 
   <!-- Vocabulary fields share special widget factories that redirect
        to the vocabularies they reference. -->
+
+  <!-- Single selection -->
   <view
       permission="zope.Public"
       type="zope.publisher.interfaces.browser.IBrowserPresentation"
@@ -122,24 +124,83 @@
       factory=".vocabularywidget.VocabularyFieldEditWidget"
       />
 
+  <!-- Bags -->
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyBagField"
+      name="display"
+      factory=".vocabularywidget.VocabularyBagFieldDisplayWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyBagField"
+      name="edit"
+      factory=".vocabularywidget.VocabularyBagFieldEditWidget"
+      />
+
+  <!-- Lists -->
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyListField"
+      name="display"
+      factory=".vocabularywidget.VocabularyListFieldDisplayWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyListField"
+      name="edit"
+      factory=".vocabularywidget.VocabularyListFieldEditWidget"
+      />
+
+  <!-- Sets -->
   <view
       permission="zope.Public"
       type="zope.publisher.interfaces.browser.IBrowserPresentation"
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
-      for="zope.schema.vocabulary.IVocabularyMultiField"
+      for="zope.schema.vocabulary.IVocabularySetField"
       name="display"
-      factory=".vocabularywidget.VocabularyMultiFieldDisplayWidget"
+      factory=".vocabularywidget.VocabularySetFieldDisplayWidget"
       />
 
   <view
       permission="zope.Public"
       type="zope.publisher.interfaces.browser.IBrowserPresentation"
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
-      for="zope.schema.vocabulary.IVocabularyMultiField"
+      for="zope.schema.vocabulary.IVocabularySetField"
       name="edit"
-      factory=".vocabularywidget.VocabularyMultiFieldEditWidget"
+      factory=".vocabularywidget.VocabularySetFieldEditWidget"
       />
 
+  <!-- Unique lists -->
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyUniqueListField"
+      name="display"
+      factory=".vocabularywidget.VocabularyUniqueListFieldDisplayWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyUniqueListField"
+      name="edit"
+      factory=".vocabularywidget.VocabularyUniqueListFieldEditWidget"
+      />
+
+  <!-- Query view helpers -->
   <view
       permission="zope.Public"
       type="zope.publisher.interfaces.browser.IBrowserPresentation"
@@ -156,6 +217,38 @@
       for="zope.schema.interfaces.IIterableVocabularyQuery"
       name="widget-query-multi-helper"
       factory=".vocabularywidget.IterableVocabularyQueryMultiView"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      factory=".vocabularywidget.VocabularyEditWidget"
+      name="field-edit-widget"
+      for="zope.schema.interfaces.IVocabulary"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      factory=".vocabularywidget.VocabularyDisplayWidget"
+      name="field-display-widget"
+      for="zope.schema.interfaces.IVocabulary"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      factory=".vocabularywidget.VocabularyMultiEditWidget"
+      name="field-multi-edit"
+      for="zope.schema.interfaces.IVocabulary"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      factory=".vocabularywidget.VocabularyMultiDisplayWidget"
+      name="field-multi-display"
+      for="zope.schema.interfaces.IVocabulary"
       />
 
   <!-- Default simple display view -->


=== Zope3/src/zope/app/browser/form/editview.py 1.23 => 1.23.8.1 ===
--- Zope3/src/zope/app/browser/form/editview.py:1.23	Fri May  2 14:27:07 2003
+++ Zope3/src/zope/app/browser/form/editview.py	Sun Jun 22 10:22:39 2003
@@ -24,7 +24,7 @@
 from zope.schema import getFieldNamesInOrder
 
 from zope.configuration.action import Action
-from zope.proxy.context import ContextWrapper
+from zope.app.context import ContextWrapper
 from zope.publisher.interfaces.browser import IBrowserPresentation
 from zope.publisher.browser import BrowserView
 from zope.security.checker import defineChecker, NamesChecker
@@ -76,7 +76,7 @@
             widget.setPrefix(prefix)
 
     def widgets(self):
-        return [getattr(self, name)
+        return [getattr(self, name+'_widget')
                 for name in self.fieldNames]
 
     def apply_update(self, data):


=== Zope3/src/zope/app/browser/form/meta.zcml 1.10 => 1.10.12.1 ===
--- Zope3/src/zope/app/browser/form/meta.zcml:1.10	Wed Apr 30 17:53:20 2003
+++ Zope3/src/zope/app/browser/form/meta.zcml	Sun Jun 22 10:22:39 2003
@@ -122,12 +122,12 @@
       <description>
         Define an automatically generated add form
 
-        The addForm directive creates and register's a view for
+        The addform directive creates and register's a view for
         adding an object based on a schema.
 
         Adding an object is a bit trickier than editing an object,
         because the object the schema applies to isn't available when
-        forms are being rendered.  The addForm directive provides an
+        forms are being rendered.  The addform directive provides an
         customization interface to overcome this difficulty.
 
         See zope.app.interfaces.browser.form.IAddFormCustomization.


=== Zope3/src/zope/app/browser/form/schemadisplay.py 1.2 => 1.2.12.1 ===
--- Zope3/src/zope/app/browser/form/schemadisplay.py:1.2	Wed Apr 30 19:37:51 2003
+++ Zope3/src/zope/app/browser/form/schemadisplay.py	Sun Jun 22 10:22:39 2003
@@ -20,7 +20,7 @@
 from zope.schema import getFieldNamesInOrder
 
 from zope.configuration.action import Action
-from zope.proxy.context import ContextWrapper
+from zope.app.context import ContextWrapper
 from zope.publisher.interfaces.browser import IBrowserPresentation
 from zope.publisher.browser import BrowserView
 from zope.security.checker import defineChecker, NamesChecker
@@ -69,7 +69,7 @@
             widget.setPrefix(prefix)
 
     def widgets(self):
-        return [getattr(self, name)
+        return [getattr(self, name+'_widget')
                 for name in self.fieldNames]
 
 


=== Zope3/src/zope/app/browser/form/vocabularywidget.py 1.6 => 1.6.2.1 ===
--- Zope3/src/zope/app/browser/form/vocabularywidget.py:1.6	Thu May 22 02:32:13 2003
+++ Zope3/src/zope/app/browser/form/vocabularywidget.py	Sun Jun 22 10:22:39 2003
@@ -22,32 +22,64 @@
 
 from xml.sax.saxutils import quoteattr
 
+from zope.interface import implements, implementedBy
 from zope.app.browser.form import widget
 from zope.app.i18n import ZopeMessageIDFactory as _
 from zope.app.interfaces.browser.form import IVocabularyQueryView
+from zope.app.interfaces.form import WidgetInputError, MissingInputError
+from zope.interface.declarations import directlyProvides
 from zope.publisher.browser import BrowserView
 from zope.component import getView
 from zope.schema.interfaces import IIterableVocabulary, IVocabularyQuery
 from zope.schema.interfaces import IIterableVocabularyQuery
+from zope.schema.interfaces import ValidationError
 
 
 # These widget factories delegate to the vocabulary on the field.
 
+# Display
+
 def VocabularyFieldDisplayWidget(field, request):
     """Return a display widget based on a vocabulary field."""
     return _get_vocabulary_widget(field, request, "display")
 
+def VocabularyBagFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display-bag")
+
+def VocabularyListFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display-list")
+
+def VocabularySetFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display-set")
+
+def VocabularyUniqueListFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display-unique-list")
+
+# Edit
+
 def VocabularyFieldEditWidget(field, request):
     """Return a value-selection widget based on a vocabulary field."""
-    return _get_vocabulary_edit_widget(field, request, ismulti=False)
+    return _get_vocabulary_edit_widget(field, request)
 
-def VocabularyMultiFieldDisplayWidget(field, request):
-    """Return a display widget based on a vocabulary field."""
-    return _get_vocabulary_widget(field, request, "display-multi")
+def VocabularyBagFieldEditWidget(field, request):
+    """Return a value-selection widget based on a vocabulary field."""
+    return _get_vocabulary_edit_widget(field, request, "bag")
+
+def VocabularyListFieldEditWidget(field, request):
+    """Return a value-selection widget based on a vocabulary field."""
+    return _get_vocabulary_edit_widget(field, request, "list")
+
+def VocabularySetFieldEditWidget(field, request):
+    """Return a value-selection widget based on a vocabulary field."""
+    return _get_vocabulary_edit_widget(field, request, "set")
 
-def VocabularyMultiFieldEditWidget(field, request):
+def VocabularyUniqueListFieldEditWidget(field, request):
     """Return a value-selection widget based on a vocabulary field."""
-    return _get_vocabulary_edit_widget(field, request, ismulti=True)
+    return _get_vocabulary_edit_widget(field, request, "unique-list")
 
 
 # Helper functions for the factories:
@@ -57,57 +89,71 @@
     view.setField(field)
     return view
 
-def _get_vocabulary_edit_widget(field, request, ismulti):
-    if ismulti:
-        viewname = "edit-multi"
-        queryname = "widget-query-multi-helper"
-    else:
-        viewname = "edit"
-        queryname = "widget-query-helper"
+def _get_vocabulary_edit_widget(field, request, modifier=''):
+    if modifier:
+        modifier = "-" + modifier
+    viewname = "edit" + modifier
     view = _get_vocabulary_widget(field, request, viewname)
     query = field.vocabulary.getQuery()
-    if query is None and IIterableVocabulary.isImplementedBy(field.vocabulary):
-        query = IterableVocabularyQuery(field.vocabulary)
     if query is not None:
+        queryname = "widget-query%s-helper" % modifier
         queryview = getView(query, queryname, request)
         view.setQuery(query, queryview)
+        queryview.setWidget(view)
     return view
 
 
-class IterableVocabularyQuery:
+class IterableVocabularyQuery(object):
     """Simple query object used to invoke the simple selection mechanism."""
 
-    __implements__ = IIterableVocabularyQuery
+    implements(IIterableVocabularyQuery)
 
-    def __init__(self, vocabulary):
+    def __init__(self, vocabulary, *interfaces):
         self.vocabulary = vocabulary
+        if interfaces:
+            directlyProvides(self, *interfaces)
+
+
+class TranslationHook:
+
+    def translate(self, msgid):
+        # XXX This is where we should be calling on the translation service
+        return msgid.default
+
+def message(msgid, default):
+    msgid.default = default
+    return msgid
 
 
 # Widget implementation:
 
-class ViewSupport:
+class ViewSupport(object, TranslationHook):
     """Helper class for vocabulary and vocabulary-query widgets."""
 
     def textForValue(self, term):
-        # Extract the value from the term.  This can be overridden to
-        # support more complex term objects.
-        return term.value
+        # Extract a string from the term.  This can be overridden to
+        # support more complex term objects.  The token is returned
+        # here since it's the only thing known to be a string, or
+        # str()able.
+        return term.token
 
     def mkselectionlist(self, type, info, name):
-        L = ["<table>\n"]
-        for term, selected, disabled in info:
-            flag = ""
-            if selected:
-                flag = "checked "
-            if disabled:
-                flag += "disabled "
-            L.append("<tr><td>"
-                     "<input type='%s' value='%s' name='%s' %s/>"
-                     "</td>\n    <td>%s</td>"
-                     "</tr>\n"
-                     % (type, term.value, name, flag, self.textForValue(term)))
-        L.append("</table>")
-        return ''.join(L)
+        L = [self.mkselectionitem(type, name, *item) for item in info]
+        return widget.renderElement("table",
+                                    contents="\n%s\n" % "\n".join(L))
+
+    def mkselectionitem(self, type, name, term, selected, disabled):
+        flag = ""
+        if selected:
+            flag = " checked='checked'"
+        if disabled:
+            flag += " disabled='disabled'"
+        if flag:
+            flag = "\n      " + flag
+        return ("<tr><td>"
+                "<input type='%s' value='%s' name='%s'%s />"
+                "</td>\n    <td>%s</td></tr>"
+                % (type, term.token, name, flag, self.textForValue(term)))
 
 
 class VocabularyWidgetBase(ViewSupport, widget.BrowserWidget):
@@ -117,44 +163,141 @@
 
     extra = ""
     type = "vocabulary"
+    context = None
 
     def __init__(self, context, request):
         self.request = request
-        self.context = None
-
-    def _getDefault(self):
-        # Override this since the context is not the field for
-        # vocabulary-based widgets.
-        return self.context.default
+        self.vocabulary = context
+        # self.context is set to the field in setField below
 
     def setField(self, field):
         assert self.context is None
         # only allow this to happen for a bound field
         assert field.context is not None
         self.context = field
-        self.name = self._prefix + field.__name__
+        self.setPrefix(self._prefix)
+        assert self.name
+
+    def setPrefix(self, prefix):
+        super(VocabularyWidgetBase, self).setPrefix(prefix)
+        # names for other information from the form
+        self.empty_marker_name = self.name + "-empty-marker"
 
     def __call__(self):
-        if self.haveData():
-            value = self._showData()
+        if self._data is None:
+            if self.haveData():
+                data = self.getData(True)
+            else:
+                data = self._getDefault()
         else:
-            value = self.context.get(self.context.context)
-        return self.render(value)
+            data = self._data
+        return self.render(data)
 
     def render(self, value):
         raise NotImplementedError(
             "render() must be implemented by a subclass")
 
+    def convertTokensToValues(self, tokens):
+        L = []
+        for token in tokens:
+            try:
+                term = self.context.vocabulary.getTermByToken(token)
+            except LookupError:
+                raise WidgetInputError(
+                    self.context.__name__,
+                    self.context.title,
+                    "token %r not found in vocabulary" % token)
+            else:
+                L.append(term.value)
+        return L
 
-class VocabularyDisplayWidget(VocabularyWidgetBase):
+    _have_field_data = False
+
+    def getData(self, optional=False):
+        data = self._compute_data()
+        field = self.context
+        if data is None:
+            if field.required and not optional:
+                raise MissingInputError(field.__name__, field.title,
+                                        'the field is required')
+            return self._getDefault()
+        elif not optional:
+            try:
+                field.validate(data)
+            except ValidationError, v:
+                raise WidgetInputError(self.context.__name__, self.title, v)
+        return data
+
+    def _emptyMarker(self):
+        return "<input name='%s' type='hidden' value='1' />" % (
+            self.empty_marker_name)
+
+    def haveData(self):
+        return (self.name in self.request.form or
+                self.empty_marker_name in self.request.form)
+
+    def setData(self, value):
+        self._data = value
+
+    def _compute_data(self):
+        raise NotImplementedError(
+            "_compute_data() must be implemented by a subclass\n"
+            "It may be inherited from the mix-in classes SingleDataHelper\n"
+            "or MultiDataHelper (from zope.app.browser.form.vocabularywidget)")
+
+    def _showData(self):
+        raise NotImplementedError(
+            "vocabulary-based widgets don't use the _showData() method")
+
+    def _convert(self, value):
+        raise NotImplementedError(
+            "vocabulary-based widgets don't use the _convert() method")
+
+    def _unconvert(self, value):
+        raise NotImplementedError(
+            "vocabulary-based widgets don't use the _unconvert() method")
+
+class SingleDataHelper(object):
+
+    _msg_no_value = message(_("vocabulary-no-value"), "(no value)")
+
+    def _compute_data(self):
+        token = self.request.form.get(self.name)
+        if token:
+            return self.convertTokensToValues([token])[0]
+        else:
+            return None
+
+class MultiDataHelper(object):
+
+    def _compute_data(self):
+        if self.name in self.request.form:
+            tokens = self.request.form[self.name]
+            if not isinstance(tokens, list):
+                tokens = [tokens]
+            return self.convertTokensToValues(tokens)
+        return []
+
+    def _getDefault(self):
+        # Return the default value for this widget;
+        # may be overridden by subclasses.
+        val = self.context.default
+        if val is None:
+            val = []
+        return val
+
+class VocabularyDisplayWidget(SingleDataHelper, VocabularyWidgetBase):
     """Simple single-selection display that can be used in many cases."""
 
     def render(self, value):
-        term = self.context.vocabulary.getTerm(value)
-        return self.textForValue(term)
+        if value is None:
+            return self.translate(self._msg_no_value)
+        else:
+            term = self.context.vocabulary.getTerm(value)
+            return self.textForValue(term)
 
 
-class VocabularyMultiDisplayWidget(VocabularyWidgetBase):
+class VocabularyMultiDisplayWidget(MultiDataHelper, VocabularyWidgetBase):
 
     propertyNames = (VocabularyWidgetBase.propertyNames
                      + ['itemTag', 'tag'])
@@ -163,22 +306,22 @@
     tag = 'ol'
 
     def render(self, value):
-        if value == self._missing:
-            return widget.renderElement('span',
+        if value:
+            rendered_items = self.renderItems(value)
+            return widget.renderElement(self.getValue('tag'),
                                         type=self.getValue('type'),
                                         name=self.name,
                                         id=self.name,
                                         cssClass=self.getValue('cssClass'),
-                                        contents=_("(no values)"),
+                                        contents="\n".join(rendered_items),
                                         extra=self.getValue('extra'))
         else:
-            rendered_items = self.renderItems(value)
-            return widget.renderElement(self.getValue('tag'),
+            return widget.renderElement('span',
                                         type=self.getValue('type'),
                                         name=self.name,
                                         id=self.name,
                                         cssClass=self.getValue('cssClass'),
-                                        contents="\n".join(rendered_items),
+                                        contents=_("(no values)"),
                                         extra=self.getValue('extra'))
 
     def renderItems(self, value):
@@ -196,7 +339,25 @@
         return L
 
 
-class ActionHelper:
+class VocabularyListDisplayWidget(VocabularyMultiDisplayWidget):
+    """Display widget for ordered multi-selection fields.
+
+    This can be used for both VocabularyListField and
+    VocabularyUniqueListField fields.
+    """
+    tag = 'ol'
+
+
+class VocabularyBagDisplayWidget(VocabularyMultiDisplayWidget):
+    """Display widget for unordered multi-selection fields.
+
+    This can be used for both VocabularyBagField and
+    VocabularySetField fields.
+    """
+    tag = 'ul'
+
+
+class ActionHelper(object, TranslationHook):
     __actions = None
 
     def addAction(self, action, msgid):
@@ -206,6 +367,8 @@
         self.__actions[action] = msgid
 
     def getAction(self):
+        assert self.__actions is not None, \
+               "getAction() called on %r with no actions defined" % self
         get = self.request.form.get
         for action in self.__actions.iterkeys():
             name = "%s.action-%s" % (self.name, action)
@@ -216,12 +379,8 @@
     def renderAction(self, action, disabled=False):
         msgid = self.__actions[action]
         return ("<input type='submit' name='%s.action-%s' value=%s %s/>"
-                % (self.name, action, self.translate(msgid),
-                   disabled and "disabled " or ""))
-
-    def translate(self, msgid):
-        # XXX This is where we should be calling on the translation service
-        return msgid.default
+                % (self.name, action, quoteattr(self.translate(msgid)),
+                   disabled and "\n       disabled='disabled' " or ""))
 
 
 class VocabularyEditWidgetBase(VocabularyWidgetBase):
@@ -248,14 +407,14 @@
             queryview.setName(self.name + "-query")
 
     def setPrefix(self, prefix):
-        VocabularyWidgetBase.setPrefix(self, prefix)
+        super(VocabularyEditWidgetBase, self).setPrefix(prefix)
         if self.queryview is not None:
             self.queryview.setName(self.name + "-query")
 
     def render(self, value):
         contents = []
         have_results = False
-        if self.queryview:
+        if self.queryview is not None:
             s = self.queryview.renderResults(value)
             if s:
                 contents.append(self._div('queryresults', s))
@@ -263,7 +422,8 @@
                 contents.append(self._div('queryinput', s))
                 have_results = True
         contents.append(self._div('value', self.renderValue(value)))
-        if self.queryview and not have_results:
+        contents.append(self._emptyMarker())
+        if self.queryview is not None and not have_results:
             s = self.queryview.renderInput()
             if s:
                 contents.append(self._div('queryinput', s))
@@ -290,19 +450,18 @@
         rendered_items = []
         count = 0
         for term in self.context.vocabulary:
-            item_value = term.value
             item_text = self.textForValue(term)
 
-            if item_value in values:
+            if term.value in values:
                 rendered_item = self.renderSelectedItem(count,
                                                         item_text,
-                                                        item_value,
+                                                        term.token,
                                                         self.name,
                                                         cssClass)
             else:
                 rendered_item = self.renderItem(count,
                                                 item_text,
-                                                item_value,
+                                                term.token,
                                                 self.name,
                                                 cssClass)
 
@@ -325,13 +484,13 @@
                                     selected=None)
 
 
-class VocabularyEditWidget(VocabularyEditWidgetBase):
+class SelectListWidget(SingleDataHelper, VocabularyEditWidgetBase):
     """Vocabulary-backed single-selection edit widget.
 
     This widget can be used when the number of selections isn't going
     to be very large.
     """
-    __implements__ = widget.SingleItemsWidget.__implements__
+    implements(implementedBy(widget.SingleItemsWidget))
     propertyNames = VocabularyEditWidgetBase.propertyNames + ['firstItem']
     firstItem = False
 
@@ -344,8 +503,7 @@
                                     size=self.getValue('size'))
 
     def renderItems(self, value):
-        vocabulary = self.context
-
+        vocabulary = self.context.vocabulary
         # check if we want to select first item
         if (value == self._missing
             and getattr(self.context, 'firstItem', False)
@@ -356,18 +514,22 @@
             values = [value]
         else:
             values = ()
+        L = self.renderItemsWithValues(values)
+        if not self.context.required:
+            option = ("<option name='%s' value=''>%s</option>"
+                      % (self.name, self.translate(self._msg_no_value)))
+            L.insert(0, option)
+        return L
 
-        return VocabularyEditWidgetBase.renderItemsWithValues(self, values)
+# more general alias
+VocabularyEditWidget = SelectListWidget
 
-    def hidden(self):
-        return widget.renderElement('input',
-                                    type='hidden',
-                                    name=self.name,
-                                    id=self.name,
-                                    value=self._showData())
+class DropdownListWidget(SelectListWidget):
+    """Variation of the SelectListWidget that uses a drop-down list."""
 
+    size = 1
 
-class VocabularyMultiEditWidget(VocabularyEditWidgetBase):
+class VocabularyMultiEditWidget(MultiDataHelper, VocabularyEditWidgetBase):
     """Vocabulary-backed widget supporting multiple selections."""
 
     def renderItems(self, value):
@@ -375,7 +537,7 @@
             values = ()
         else:
             values = list(value)
-        return VocabularyEditWidgetBase.renderItemsWithValues(self, values)
+        return self.renderItemsWithValues(values)
 
     def renderValue(self, value):
         # All we really add here is the ':list' in the name argument
@@ -387,27 +549,17 @@
                                     size=self.getValue('size'),
                                     contents="\n".join(rendered_items))
 
-    def hidden(self):
-        L = []
-        for v in self._showData():
-            s = widget.renderElement('input',
-                                     type='hidden',
-                                     name=self.name + ':list',
-                                     value=v)
-            assert s[-1] == '>'
-            L.append(s[:-1])
-            L.append('\n>')
-        return ''.join(L)
-
 
 class VocabularyQueryViewBase(ActionHelper, ViewSupport, BrowserView):
     """Vocabulary query support base class."""
 
-    __implements__ = IVocabularyQueryView
+    implements(IVocabularyQueryView)
 
     # This specifically isn't a widget in it's own right, but is a
     # form of BrowserView (at least conceptually).
 
+    widget = None
+
     def __init__(self, context, request):
         self.vocabulary = context.vocabulary
         self.context = context
@@ -418,6 +570,11 @@
         assert not name.endswith(".")
         self.name = name
 
+    def setWidget(self, widget):
+        assert self.widget is None
+        assert widget is not None
+        self.widget = widget
+
     def renderInput(self):
         return self.renderQueryInput()
 
@@ -442,24 +599,14 @@
         # there isn't a query in the form, returns None.
         return None
 
-    _performed_action = False
-
     def performAction(self, value):
-        """Make sure the real action, performQueryAction(), only runs once."""
-        if self._performed_action:
-            return value
-        self._performed_action = True
-        return self.performQueryAction(value)
+        return value
 
 
 ADD_DONE = "adddone"
 ADD_MORE = "addmore"
 MORE = "more"
 
-def _message(msgid, default):
-    msgid.default = default
-    return msgid
-
 
 class IterableVocabularyQueryViewBase(VocabularyQueryViewBase):
     """Query view for IIterableVocabulary objects without more
@@ -469,44 +616,69 @@
     getQuery() returns None.
     """
 
-    __implements__ = IVocabularyQueryView
+    implements(IVocabularyQueryView)
 
     queryResultBatchSize = 8
 
-    action = None
-
-    _msg_add_done = _message(_("vocabulary-query-button-add-done"), "Add")
-    _msg_add_more = _message(_("vocabulary-query-button-add-more"), "Add+More")
-    _msg_more     = _message(_("vocabulary-query-button-more"),     "More")
+    _msg_add_done   = message(_("vocabulary-query-button-add-done"),
+                              "Add+Done")
+    _msg_add_more   = message(_("vocabulary-query-button-add-more"),
+                              "Add+More")
+    _msg_more       = message(_("vocabulary-query-button-more"),
+                              "More")
+    _msg_no_results = message(_("vocabulary-query-message-no-results"),
+                              "No Results")
+    _msg_results_header = message(_("vocabulary-query-header-results"),
+                                 "Search results")
 
-    def setName(self, name):
-        VocabularyQueryViewBase.setName(self, name)
+    def __init__(self, *args, **kw):
+        super(IterableVocabularyQueryViewBase, self).__init__(*args, **kw)
         self.addAction(ADD_DONE, self._msg_add_done)
         self.addAction(ADD_MORE, self._msg_add_more)
         self.addAction(MORE,     self._msg_more)
+
+    def setName(self, name):
+        super(IterableVocabularyQueryViewBase, self).setName(name)
         name = self.name
         self.query_index_name = name + ".start"
         self.query_selections_name = name + ".picks"
         #
         get = self.request.form.get
         self.action = self.getAction()
-        try:
-            self.query_index = int(get(self.query_index_name, 0))
-        except ValueError:
-            self.query_index = 0
-        else:
-            if self.query_index < 0:
-                self.query_index = 0
-        self.query_selections = get(self.query_selections_name, [])
-        if not isinstance(self.query_selections, list):
-            self.query_selections = [self.query_selections]
+        self.query_index = None
+        if self.query_index_name in self.request.form:
+            try:
+                index = int(self.request.form[self.query_index_name])
+            except ValueError:
+                pass
+            else:
+                if index >= 0:
+                    self.query_index = index
+        QS = get(self.query_selections_name, [])
+        if not isinstance(QS, list):
+            QS = [QS]
+        self.query_selections = []
+        for token in QS:
+            try:
+                term = self.vocabulary.getTermByToken(token)
+            except LookupError:
+                # XXX unsure what to pass to exception constructor
+                raise WidgetInputError(
+                    "(query view for %s)" % self.context,
+                    "(query view for %s)" % self.context,
+                    "token %r not in vocabulary" % token)
+            else:
+                self.query_selections.append(term.value)
 
     def renderQueryInput(self):
         # There's no query support, so we can't actually have input.
         return ""
 
     def getResults(self):
-        return self.vocabulary
+        if self.query_index is not None:
+            return self.vocabulary
+        else:
+            return None
 
     def renderQueryResults(self, results, value):
         # display query results batch
@@ -517,6 +689,9 @@
             for xxx in range(qi):
                 it.next()
         except StopIteration:
+            # we should only get here with a botched request; ADD_MORE
+            # and MORE will normally be disabled if there are no results
+            # (see below)
             have_more = False
         items = []
         QS = []
@@ -533,24 +708,28 @@
                 # see if there's anything else:
                 it.next()
         except StopIteration:
+            if not items:
+                return "<div class='results'>%s</div>" % (
+                    self.translate(self._msg_no_results))
             have_more = False
         self.query_selections = QS
-        L = ["<div class='results'>\n",
+        return ''.join(
+            ["<div class='results'>\n",
+             "<h4>%s</h4>\n" % (
+                 self.translate(self._msg_results_header)),
              self.makeSelectionList(items, self.query_selections_name),
              "\n",
              self.renderAction(ADD_DONE), "\n",
              self.renderAction(ADD_MORE, not have_more), "\n",
-             self.renderAction(MORE, not have_more), "\n"]
-        if qi:
-            L.append("<input type='hidden' name='%s' value='%d' />\n"
-                     % (self.query_index_name, qi))
-        L.append("</div>")
-        return ''.join(L)
+             self.renderAction(MORE, not have_more), "\n"
+             "<input type='hidden' name='%s' value='%d' />\n"
+             % (self.query_index_name, qi),
+             "</div>"])
 
-    def performQueryAction(self, value):
+    def performAction(self, value):
         if self.action == ADD_DONE:
             value = self.addSelections(value)
-            self.query_index = 0
+            self.query_index = None
             self.query_selections = []
         elif self.action == ADD_MORE:
             value = self.addSelections(value)
@@ -573,8 +752,8 @@
         return self.mkselectionlist("radio", items, name)
 
     def renderQueryResults(self, results, value):
-        return IterableVocabularyQueryViewBase.renderQueryResults(
-            self, results, [value])
+        return super(IterableVocabularyQueryView, self).renderQueryResults(
+            results, [value])
 
 class IterableVocabularyQueryMultiView(IterableVocabularyQueryViewBase):
 


=== Zope3/src/zope/app/browser/form/widget.py 1.30 => 1.30.2.1 ===
--- Zope3/src/zope/app/browser/form/widget.py:1.30	Mon May 19 16:54:03 2003
+++ Zope3/src/zope/app/browser/form/widget.py	Sun Jun 22 10:22:39 2003
@@ -17,7 +17,9 @@
 
 __metaclass__ = type
 
-from zope.proxy.introspection import removeAllProxies
+import warnings
+from zope.interface import implements
+from zope.proxy import removeAllProxies
 from zope.publisher.browser import BrowserView
 from zope.app.interfaces.browser.form import IBrowserWidget
 from zope.app.form.widget import Widget
@@ -32,9 +34,52 @@
 
 
 class BrowserWidget(Widget, BrowserView):
-    """A field widget that knows how to display itself as HTML."""
+    """A field widget that knows how to display itself as HTML.
 
-    __implements__ = IBrowserWidget
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import Field
+    >>> field = Field(__name__='foo', title=u'Foo')
+    >>> request = TestRequest(form={'field.foo': u'hello\\r\\nworld'})
+    >>> widget = BrowserWidget(field, request)
+    >>> widget.name
+    'field.foo'
+    >>> widget.title
+    u'Foo'
+    >>> int(widget.haveData())
+    1
+    >>> widget.getData()
+    u'hello\\r\\nworld'
+    >>> int(widget.required)
+    1
+    >>> widget.setData('Hey\\nfolks')
+    >>> widget.getData()
+    u'hello\\r\\nworld'
+
+    >>> widget.setPrefix('test')
+    >>> widget.name
+    'test.foo'
+    >>> int(widget.haveData())
+    0
+    >>> widget.getData()
+    Traceback (most recent call last):
+    ...
+    MissingInputError: ('foo', u'Foo', 'the field is required')
+    >>> field.required = False
+    >>> int(widget.required)
+    0
+    >>> widget.getData()
+
+    When we generate labels, the labels are translated, so we need to set up
+    a lot of machinery to support translation:
+
+    >>> setUp()
+    >>> print widget.label()
+    <label for="test.foo">Foo</label>
+    >>> tearDown()
+    
+    """
+
+    implements(IBrowserWidget)
 
     propertyNames = (Widget.propertyNames +
                      ['tag', 'type', 'cssClass', 'extra'])
@@ -47,7 +92,7 @@
 
     def haveData(self):
         if self.name in self.request.form:
-            return self._convert(self.request[self.name]) is not None
+            return self._convert(self.request[self.name]) != self._missing
         return False
 
     def getData(self, optional=0):
@@ -116,10 +161,15 @@
                              extra = self.getValue('extra'))
 
     def render(self, value):
+        warnings.warn("The widget render method is deprecated",
+                      DeprecationWarning, 2)
+
         self.setData(value)
         return self()
 
     def renderHidden(self, value):
+        warnings.warn("The widget render method is deprecated",
+                      DeprecationWarning, 2)
         self.setData(value)
         return self.hidden()
 
@@ -143,7 +193,66 @@
         return self._showData()
 
 class CheckBoxWidget(BrowserWidget):
-    """Checkbox widget"""
+    """Checkbox widget
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import Bool
+    >>> field = Bool(__name__='foo', title=u'on')
+    >>> request = TestRequest(form={'field.foo.used': u'on',
+    ...                             'field.foo': u'on'})
+    >>> widget = CheckBoxWidget(field, request)
+    >>> int(widget.haveData())
+    1
+    >>> int(widget.getData())
+    1
+    
+    >>> def normalize(s):
+    ...   return '\\n  '.join(s.split())
+
+    >>> print normalize( widget() )
+    <input
+      class="hiddenType"
+      id="field.foo.used"
+      name="field.foo.used"
+      type="hidden"
+      value=""
+      />
+      <input
+      class="checkboxType"
+      checked="checked"
+      id="field.foo"
+      name="field.foo"
+      type="checkbox"
+      />
+
+    >>> print normalize( widget.hidden() )
+    <input
+      class="hiddenType"
+      id="field.foo"
+      name="field.foo"
+      type="hidden"
+      value="on"
+      />
+
+    Calling setData will change what gets output:
+    
+    >>> widget.setData(False)
+    >>> print normalize( widget() )
+    <input
+      class="hiddenType"
+      id="field.foo.used"
+      name="field.foo.used"
+      type="hidden"
+      value=""
+      />
+      <input
+      class="checkboxType"
+      id="field.foo"
+      name="field.foo"
+      type="checkbox"
+      />
+
+    """
     propertyNames = BrowserWidget.propertyNames + \
                      ['extra', 'default']
 
@@ -157,19 +266,35 @@
             kw = {'checked': None}
         else:
             kw = {}
-        return renderElement(self.getValue('tag'),
+        return "%s %s" % (
+            renderElement(self.getValue('tag'),
+                          type = 'hidden',
+                          name = self.name+".used",
+                          id = self.name+".used",
+                          value=""
+                          ),
+            renderElement(self.getValue('tag'),
                              type = self.getValue('type'),
                              name = self.name,
                              id = self.name,
                              cssClass = self.getValue('cssClass'),
                              extra = self.getValue('extra'),
-                             **kw)
+                             **kw),
+            )
 
     def _convert(self, value):
         return value == 'on'
 
+    def _unconvert(self, value):
+        return value and "on" or ""
+        return value == 'on'
+
     def haveData(self):
-        return True
+        return (
+            self.name+".used" in self.request.form
+            or
+            self.name in self.request.form
+            )
 
     def getData(self, optional=0):
         # When it's checked, its value is 'on'.
@@ -178,24 +303,65 @@
         value = self.request.form.get(self.name, 'off')
         return value == 'on'
 
-class PossiblyEmptyMeansMissing:
-
-    def haveData(self):
-        v = self.request.form.get(self.name)
-        if v is None:
-            return False
-        if not v and getattr(self.context, 'min_length', 1) > 0:
-            return False
-        return True
+class PossiblyEmptyMeansMissing(BrowserWidget):
 
     def _convert(self, value):
-        v = self.request.form.get(self.name)
-        if not v and getattr(self.context, 'min_length', 1) > 0:
+        value = super(PossiblyEmptyMeansMissing, self)._convert(value)
+        if not value and getattr(self.context, 'min_length', 1) > 0:
             return None
-        return v
+        return value
 
 class TextWidget(PossiblyEmptyMeansMissing, BrowserWidget):
-    """Text widget."""
+    """Text widget.
+
+    Single-line text (unicode) input
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import TextLine
+    >>> field = TextLine(__name__='foo', title=u'on')
+    >>> request = TestRequest(form={'field.foo': u'Bob'})
+    >>> widget = TextWidget(field, request)
+    >>> int(widget.haveData())
+    1
+    >>> widget.getData()
+    u'Bob'
+    
+    >>> def normalize(s):
+    ...   return '\\n  '.join(filter(None, s.split(' ')))
+
+    >>> print normalize( widget() )
+    <input
+      class="textType"
+      id="field.foo"
+      name="field.foo"
+      size="20"
+      type="text"
+      value="Bob"
+      />
+
+    >>> print normalize( widget.hidden() )
+    <input
+      class="hiddenType"
+      id="field.foo"
+      name="field.foo"
+      type="hidden"
+      value="Bob"
+      />
+
+    Calling setData will change what gets output:
+    
+    >>> widget.setData("Barry")
+    >>> print normalize( widget() )
+    <input
+      class="textType"
+      id="field.foo"
+      name="field.foo"
+      size="20"
+      type="text"
+      value="Barry"
+      />
+
+    """
     propertyNames = (BrowserWidget.propertyNames +
                      ['displayWidth', 'displayMaxWidth', 'extra', 'default']
                      )
@@ -208,11 +374,6 @@
     style = ''
     __values = None
 
-    def _convert(self, value):
-        if self.context.min_length and not value:
-            return None
-        return value
-
     def __init__(self, *args):
         super(TextWidget, self).__init__(*args)
 
@@ -278,12 +439,9 @@
                                  size = self.getValue('displayWidth'),
                                  extra = self.getValue('extra'))
 
-class Bytes:
+class Bytes(BrowserWidget):
 
     def _convert(self, value):
-        if self.context.min_length and not value:
-            return None
-
         value = super(Bytes, self)._convert(value)
         if type(value) is unicode:
             try:
@@ -294,7 +452,21 @@
         return value
 
 class BytesWidget(Bytes, TextWidget):
-    pass
+    """Bytes widget.
+
+    Single-line data (string) input
+
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import BytesLine
+    >>> field = BytesLine(__name__='foo', title=u'on')
+    >>> request = TestRequest(form={'field.foo': u'Bob'})
+    >>> widget = BytesWidget(field, request)
+    >>> int(widget.haveData())
+    1
+    >>> widget.getData()
+    'Bob'
+
+    """
 
 class IntWidget(TextWidget):
     displayWidth = 10
@@ -329,7 +501,55 @@
                 raise ConversionError("Invalid datetime data", v)
 
 class TextAreaWidget(PossiblyEmptyMeansMissing, BrowserWidget):
-    """Textarea widget."""
+    """TextArea widget.
+
+    Multi-line text (unicode) input.
+    
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import Text
+    >>> field = Text(__name__='foo', title=u'on')
+    >>> request = TestRequest(form={'field.foo': u'Hello\\r\\nworld!'})
+    >>> widget = TextAreaWidget(field, request)
+    >>> int(widget.haveData())
+    1
+    >>> widget.getData()
+    u'Hello\\nworld!'
+    
+    >>> def normalize(s):
+    ...   return '\\n  '.join(filter(None, s.split(' ')))
+
+    >>> print normalize( widget() )
+    <textarea
+      cols="60"
+      id="field.foo"
+      name="field.foo"
+      rows="15"
+      >Hello\r
+    world!</textarea>
+
+    >>> print normalize( widget.hidden() )
+    <input
+      class="hiddenType"
+      id="field.foo"
+      name="field.foo"
+      type="hidden"
+      value="Hello\r
+    world!"
+      />
+
+    Calling setData will change what gets output:
+    
+    >>> widget.setData("Hey\\ndude!")
+    >>> print normalize( widget() )
+    <textarea
+      cols="60"
+      id="field.foo"
+      name="field.foo"
+      rows="15"
+      >Hey\r
+    dude!</textarea>
+
+    """
     propertyNames = BrowserWidget.propertyNames + ['width', 'height', 'extra']
 
     default = ""
@@ -339,10 +559,17 @@
     style = ''
 
     def _convert(self, value):
-        if self.context.min_length and not value:
-            return None
+        value = super(TextAreaWidget, self)._convert(value)
+        if value:
+            value = value.replace("\r\n", "\n")
         return value
 
+    def _unconvert(self, value):
+        value = super(TextAreaWidget, self)._unconvert(value)
+        if value:
+            value = value.replace("\n", "\r\n")
+        return value        
+
     def __call__(self):
         return renderElement("textarea",
                              name = self.name,
@@ -362,7 +589,21 @@
                 self.label(), self())
 
 class BytesAreaWidget(Bytes, TextAreaWidget):
-    pass
+    """BytesArea widget.
+
+    Multi-line text (unicode) input.
+    
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.schema import Bytes
+    >>> field = Bytes(__name__='foo', title=u'on')
+    >>> request = TestRequest(form={'field.foo': u'Hello\\r\\nworld!'})
+    >>> widget = BytesAreaWidget(field, request)
+    >>> int(widget.haveData())
+    1
+    >>> widget.getData()
+    'Hello\\nworld!'
+
+    """
 
 class PasswordWidget(TextWidget):
     """Password Widget"""
@@ -512,7 +753,6 @@
 
 class ListWidget(SingleItemsWidget):
     """List widget."""
-    __implements__ = SingleItemsWidget.__implements__
     propertyNames = (SingleItemsWidget.propertyNames +
                      ['firstItem', 'items', 'size', 'extra']
                      )
@@ -734,7 +974,9 @@
         extra = ""
 
     # handle other attributes
-    for key, value in kw.items():
+    items = kw.items()
+    items.sort()
+    for key, value in items:
         if value == None:
             value = key
         attr_list.append('%s="%s"' % (key, str(value)))
@@ -750,3 +992,16 @@
         return "%s>%s</%s>" % (renderTag(tag, **kw), contents, tag)
     else:
         return renderTag(tag, **kw) + " />"
+
+def setUp():
+    import zope.app.tests.placelesssetup
+    global setUp
+    setUp = zope.app.tests.placelesssetup.setUp
+    setUp()
+
+def tearDown():
+    import zope.app.tests.placelesssetup
+    global tearDown
+    tearDown = zope.app.tests.placelesssetup.tearDown
+    tearDown()
+