[Zope3-checkins] CVS: Zope3/src/zope/app/browser/form - vocabularywidget.py:1.2 configure.zcml:1.7

Fred L. Drake, Jr. fred@zope.com
Tue, 20 May 2003 12:10:58 -0400


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

Modified Files:
	configure.zcml 
Added Files:
	vocabularywidget.py 
Log Message:
Merge schema-vocabulary-branch into the trunk.

Preliminary documentation on vocabularies and schema vocabulary fields
can be found at http://dev.zope.org/Zope3/VocabularyFields.


=== Zope3/src/zope/app/browser/form/vocabularywidget.py 1.1 => 1.2 ===
--- /dev/null	Tue May 20 12:10:58 2003
+++ Zope3/src/zope/app/browser/form/vocabularywidget.py	Tue May 20 12:10:27 2003
@@ -0,0 +1,379 @@
+##############################################################################
+#
+# Copyright (c) 2003 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+"""Vocabulary widget support.
+
+This includes support for vocabulary fields' use of the vocabulary to
+determine the actual widget to display, and support for supplemental
+query objects and helper views.
+
+"""
+
+from zope.app.browser.form import widget
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app.interfaces.browser.form import IVocabularyQueryView
+from zope.publisher.browser import BrowserView
+from zope.component import getView
+
+
+# These widget factories delegate to the vocabulary on the field.
+
+def VocabularyFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display")
+
+def VocabularyFieldEditWidget(field, request):
+    """Return a value-selection widget based on a vocabulary field."""
+    return _get_vocabulary_edit_widget(field, request, ismulti=False)
+
+def VocabularyMultiFieldDisplayWidget(field, request):
+    """Return a display widget based on a vocabulary field."""
+    return _get_vocabulary_widget(field, request, "display-multi")
+
+def VocabularyMultiFieldEditWidget(field, request):
+    """Return a value-selection widget based on a vocabulary field."""
+    return _get_vocabulary_edit_widget(field, request, ismulti=True)
+
+
+# Helper functions for the factories:
+
+def _get_vocabulary_widget(field, request, viewname):
+    view = getView(field.vocabulary, "field-%s-widget" % viewname, request)
+    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"
+    view = _get_vocabulary_widget(field, request, viewname)
+    query = field.vocabulary.getQuery()
+    if query is not None:
+        queryview = getView(query, queryname, request)
+        view.setQuery(query, queryview)
+    return view
+
+
+# Widget implementation:
+
+class ViewSupport:
+    """Helper class for vocabulary and vocabulary-query widgets."""
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+        self.field = None
+
+    def textForValue(self, term):
+        # Extract the value from the term.  This can be overridden to
+        # support more complex term objects.
+        return term.value
+
+
+class VocabularyWidgetBase(ViewSupport, widget.BrowserWidget):
+    """Convenience base class for vocabulary-based widgets."""
+
+    propertyNames = widget.BrowserWidget.propertyNames + ["extra"]
+
+    extra = ""
+    type = "vocabulary"
+
+    def _getDefault(self):
+        # Override this since the context is not the field for
+        # vocabulary-based widgets.
+        return self.field.default
+
+    def setField(self, field):
+        assert self.field is None
+        # only allow this to happen for a bound field
+        assert field.context is not None
+        self.field = field
+        self.name = self._prefix + field.__name__
+
+    def __call__(self):
+        if self.haveData():
+            value = self._showData()
+        else:
+            value = self.field.get(self.field.context)
+        return self.render(value)
+
+    def render(self, value):
+        raise NotImplementedError(
+            "render() must be implemented by a subclass")
+
+
+class VocabularyDisplayWidget(VocabularyWidgetBase):
+    """Simple single-selection display that can be used in many cases."""
+
+    def render(self, value):
+        term = self.field.vocabulary.getTerm(value)
+        return self.textForValue(term)
+
+
+class VocabularyMultiDisplayWidget(VocabularyWidgetBase):
+
+    propertyNames = (VocabularyWidgetBase.propertyNames
+                     + ['itemTag', 'tag'])
+
+    itemTag = 'li'
+    tag = 'ol'
+
+    def render(self, value):
+        if value == self._missing:
+            return widget.renderElement('span',
+                                        type=self.getValue('type'),
+                                        name=self.name,
+                                        id=self.name,
+                                        cssClass=self.getValue('cssClass'),
+                                        contents=_("(no values)"),
+                                        extra=self.getValue('extra'))
+        else:
+            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="\n".join(rendered_items),
+                                        extra=self.getValue('extra'))
+
+    def renderItems(self, value):
+        L = []
+        vocabulary = self.context
+        cssClass = self.getValue('cssClass') or ''
+        if cssClass:
+            cssClass += "-item"
+        tag = self.getValue('itemTag')
+        for v in value:
+            term = vocabulary.getTerm(v)
+            L.append(widget.renderElement(tag,
+                                          cssClass=cssClass,
+                                          contents=self.textForValue(term)))
+        return L
+
+
+class VocabularyEditWidgetBase(VocabularyWidgetBase):
+    propertyNames = (VocabularyWidgetBase.propertyNames
+                     + ['size', 'tag'])
+    size = 5
+    tag = 'select'
+
+    query = None
+    queryview = None
+
+    def setQuery(self, query, queryview):
+        assert self.query is None
+        assert self.queryview is None
+        if query is None:
+            assert queryview is None
+        else:
+            assert queryview is not None
+            self.query = query
+            self.queryview = queryview
+            # Use of a hyphen to form the name for the query widget
+            # ensures that it won't clash with anything else, since
+            # field names are normally Python identifiers.
+            queryview.setName(self.name + "-query")
+
+    def setPrefix(self, prefix):
+        VocabularyWidgetBase.setPrefix(self, prefix)
+        if self.queryview is not None:
+            self.queryview.setName(self.name + "-query")
+
+    def render(self, value):
+        contents = []
+        have_results = False
+        if self.queryview:
+            s = self.queryview.renderResults(value)
+            if s:
+                contents.append(self._div('queryresults', s))
+                s = self.queryview.renderInput()
+                contents.append(self._div('queryinput', s))
+                have_results = True
+        contents.append(self._div('value', self.renderValue(value)))
+        if self.queryview and not have_results:
+            s = self.queryview.renderInput()
+            if s:
+                contents.append(self._div('queryinput', s))
+        return self._div(self.getValue('cssClass'), "\n".join(contents),
+                         id=self.name,
+                         extra=self.getValue('extra'))
+
+    def _div(self, cssClass, contents, **kw):
+        if contents:
+            return widget.renderElement('div',
+                                        cssClass=cssClass,
+                                        contents="\n%s\n" % contents,
+                                        **kw)
+        return ""
+
+    def renderItemsWithValues(self, values):
+        """Render the list of possible values, with those found in
+        'values' being marked as selected."""
+
+        cssClass = self.getValue('cssClass')
+
+        # multiple items with the same value are not allowed from a
+        # vocabulary, so that need not be considered here
+        rendered_items = []
+        count = 0
+        for term in self.context:
+            item_value = term.value
+            item_text = self.textForValue(term)
+
+            if item_value in values:
+                rendered_item = self.renderSelectedItem(count,
+                                                        item_text,
+                                                        item_value,
+                                                        self.name,
+                                                        cssClass)
+            else:
+                rendered_item = self.renderItem(count,
+                                                item_text,
+                                                item_value,
+                                                self.name,
+                                                cssClass)
+
+            rendered_items.append(rendered_item)
+            count += 1
+
+        return rendered_items
+
+    def renderItem(self, index, text, value, name, cssClass):
+        return widget.renderElement('option',
+                                    contents=text,
+                                    value=value,
+                                    cssClass=cssClass)
+
+    def renderSelectedItem(self, index, text, value, name, cssClass):
+        return widget.renderElement('option',
+                                    contents=text,
+                                    value=value,
+                                    cssClass=cssClass,
+                                    selected=None)
+
+
+class VocabularyEditWidget(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__
+    propertyNames = VocabularyEditWidgetBase.propertyNames + ['firstItem']
+    firstItem = False
+
+    def renderValue(self, value):
+        rendered_items = self.renderItems(value)
+        contents = "\n%s\n" % "\n".join(rendered_items)
+        return widget.renderElement('select',
+                                    name=self.name,
+                                    contents=contents,
+                                    size=self.getValue('size'))
+
+    def renderItems(self, value):
+        vocabulary = self.context
+
+        # check if we want to select first item
+        if (value == self._missing
+            and getattr(self.context, 'firstItem', False)
+            and len(vocabulary) > 0):
+            # Grab the first item from the iterator:
+            values = [iter(vocabulary).next().value]
+        elif value != self._missing:
+            values = [value]
+        else:
+            values = ()
+
+        return VocabularyEditWidgetBase.renderItemsWithValues(self, values)
+
+    def hidden(self):
+        return widget.renderElement('input',
+                                    type='hidden',
+                                    name=self.name,
+                                    id=self.name,
+                                    value=self._showData())
+
+
+class VocabularyMultiEditWidget(VocabularyEditWidgetBase):
+    """Vocabulary-backed widget supporting multiple selections."""
+
+    def renderItems(self, value):
+        if value == self._missing:
+            values = ()
+        else:
+            values = list(value)
+        return VocabularyEditWidgetBase.renderItemsWithValues(self, values)
+
+    def renderValue(self, value):
+        # All we really add here is the ':list' in the name argument
+        # to widget.renderElement().
+        rendered_items = self.renderItems(value)
+        return widget.renderElement(self.getValue('tag'),
+                                    name=self.name + ':list',
+                                    multiple=None,
+                                    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(ViewSupport, BrowserView):
+    """Vocabulary query support base class."""
+
+    __implements__ = IVocabularyQueryView
+
+    # This specifically isn't a widget in it's own right, but is a
+    # form of BrowserView (at least conceptually).
+
+    def setName(self, name):
+        assert not name.endswith(".")
+        self.name = name
+
+    def renderInput(self):
+        return self.renderQueryInput()
+
+    def renderResults(self, value):
+        results = self.getResults()
+        if results is not None:
+            return self.renderQueryResults(results, value)
+        else:
+            return ""
+
+    def renderQueryResults(self, results, value):
+        raise NotImplementedError(
+            "renderQueryResults() must be implemented by a subclass")
+
+    def renderQueryInput(self):
+        raise NotImplementedError(
+            "renderQueryInput() must be implemented by a subclass")
+
+    def getResults(self):
+        # This is responsible for running the query against the query
+        # object (self.context), and returning a results object.  If
+        # there isn't a query in the form, returns None.
+        return None


=== Zope3/src/zope/app/browser/form/configure.zcml 1.6 => 1.7 ===
--- Zope3/src/zope/app/browser/form/configure.zcml:1.6	Thu Apr 10 05:34:28 2003
+++ Zope3/src/zope/app/browser/form/configure.zcml	Tue May 20 12:10:27 2003
@@ -11,7 +11,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.ITextLine"
       name="edit"
-      class="zope.app.browser.form.widget.TextWidget" 
+      class="zope.app.browser.form.widget.TextWidget"
       />
 
   <browser:page
@@ -19,7 +19,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IText"
       name="edit"
-      class="zope.app.browser.form.widget.TextAreaWidget" 
+      class="zope.app.browser.form.widget.TextAreaWidget"
       />
 
   <browser:page
@@ -27,7 +27,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.ISourceText"
       name="edit"
-      class="zope.app.browser.form.widget.TextAreaWidget" 
+      class="zope.app.browser.form.widget.TextAreaWidget"
       />
 
   <browser:page
@@ -35,7 +35,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IBytesLine"
       name="edit"
-      class="zope.app.browser.form.widget.BytesWidget" 
+      class="zope.app.browser.form.widget.BytesWidget"
       />
 
   <browser:page
@@ -43,7 +43,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IBytes"
       name="edit"
-      class="zope.app.browser.form.widget.BytesAreaWidget" 
+      class="zope.app.browser.form.widget.BytesAreaWidget"
       />
 
   <browser:page
@@ -51,7 +51,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IInt"
       name="edit"
-      class="zope.app.browser.form.widget.IntWidget" 
+      class="zope.app.browser.form.widget.IntWidget"
       />
 
   <browser:page
@@ -59,7 +59,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IFloat"
       name="edit"
-      class="zope.app.browser.form.widget.FloatWidget" 
+      class="zope.app.browser.form.widget.FloatWidget"
       />
 
   <browser:page
@@ -67,7 +67,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IDatetime"
       name="edit"
-      class="zope.app.browser.form.widget.DatetimeWidget" 
+      class="zope.app.browser.form.widget.DatetimeWidget"
       />
 
   <browser:page
@@ -75,7 +75,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IBool"
       name="edit"
-      class="zope.app.browser.form.widget.CheckBoxWidget" 
+      class="zope.app.browser.form.widget.CheckBoxWidget"
       />
 
   <browser:page
@@ -83,7 +83,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.ITuple"
       name="edit"
-      class="zope.app.browser.form.widget.TextAreaWidget" 
+      class="zope.app.browser.form.widget.TextAreaWidget"
       />
 
   <browser:page
@@ -91,7 +91,7 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IList"
       name="edit"
-      class="zope.app.browser.form.widget.TextAreaWidget" 
+      class="zope.app.browser.form.widget.TextAreaWidget"
       />
 
   <browser:page
@@ -99,17 +99,54 @@
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IPassword"
       name="edit"
-      class="zope.app.browser.form.widget.PasswordWidget" 
+      class="zope.app.browser.form.widget.PasswordWidget"
       />
-      
+
+  <!-- Vocabulary fields share special widget factories that redirect
+       to the vocabularies they reference. -->
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyField"
+      name="display"
+      factory=".vocabularywidget.VocabularyFieldDisplayWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyField"
+      name="edit"
+      factory=".vocabularywidget.VocabularyFieldEditWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyMultiField"
+      name="display"
+      factory=".vocabularywidget.VocabularyMultiFieldDisplayWidget"
+      />
+
+  <view
+      permission="zope.Public"
+      type="zope.publisher.interfaces.browser.IBrowserPresentation"
+      allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+      for="zope.schema.vocabulary.IVocabularyMultiField"
+      name="edit"
+      factory=".vocabularywidget.VocabularyMultiFieldEditWidget"
+      />
+
   <!-- Default simple display view -->
   <browser:page
       permission="zope.Public"
       allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
       for="zope.schema.interfaces.IField"
       name="display"
-      class="zope.app.browser.form.widget.DisplayWidget" 
+      class="zope.app.browser.form.widget.DisplayWidget"
       />
 
-  
 </zopeConfigure>