[Zope3-checkins] SVN: Zope3/trunk/src/zope/ Added "sources".

Jim Fulton jim at zope.com
Fri Oct 8 03:17:08 EDT 2004


Log message for revision 27789:
  Added "sources".
  
  Sources are like vocabularies, except that they only provide sets of
  values. In particular, they don't provide presentation favilities like
  titles and tokens. Rather, views on sources provide these facilities.
  
  In this first step, I provide only queriable sources, including some
  basic first-cut widget implementations.
  
  See source.txt
  
  TODO:
  
  - Iterable sources (and associated widgets)
  
  - Set- and Tuple(Sequence)-based widgets
  
  - Nice looking widgets :)
  
  


Changed:
  U   Zope3/trunk/src/zope/app/form/browser/configure.zcml
  U   Zope3/trunk/src/zope/app/form/browser/interfaces.py
  A   Zope3/trunk/src/zope/app/form/browser/source.py
  A   Zope3/trunk/src/zope/app/form/browser/source.txt
  A   Zope3/trunk/src/zope/app/form/browser/tests/test_source.py
  U   Zope3/trunk/src/zope/app/form/interfaces.py
  U   Zope3/trunk/src/zope/schema/_field.py
  U   Zope3/trunk/src/zope/schema/interfaces.py


-=-
Modified: Zope3/trunk/src/zope/app/form/browser/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/form/browser/configure.zcml	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/browser/configure.zcml	2004-10-08 07:17:07 UTC (rev 27789)
@@ -305,4 +305,31 @@
       permission="zope.Public"
       />
 
+  <!-- Source Views -->
+  <view
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      for="zope.schema.interfaces.IChoice
+           zope.schema.interfaces.ISource"
+      provides="zope.app.form.interfaces.IDisplayWidget"
+      factory=".source.SourceDisplayWidget"
+      permission="zope.Public"
+      />
+  <view
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      for="zope.schema.interfaces.IChoice
+           zope.schema.interfaces.ISource"
+      provides="zope.app.form.interfaces.IInputWidget"
+      factory=".source.SourceInputWidget"
+      permission="zope.Public"
+      />
+  <view
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      for="zope.schema.interfaces.ISequence
+           zope.schema.interfaces.ISource"
+      provides="zope.app.form.interfaces.IInputWidget"
+      factory=".source.SourceListInputWidget"
+      permission="zope.Public"
+      />
+     
+
 </configure>

Modified: Zope3/trunk/src/zope/app/form/browser/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/form/browser/interfaces.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/browser/interfaces.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -157,3 +157,34 @@
 
     def snippet():
         """Convert a widget input error to an html snippet."""
+
+
+class ITerms(Interface):
+    
+    def getTerm(value):
+        """Return an ITitledTokenizedTerm object for the given value
+        
+        LookupError is raised if the value isn't in the source
+        """
+        
+    def getValue(token):
+        """Return a value for a given identifier token
+        
+        LookupError is raised if there isn't a value in the source.
+        """
+
+class ISourceQueryView(Interface):
+    """View support for querying non-iterable vocabularies
+    """
+        
+    def render(name):
+        """Return a rendering of the search form elements
+        """
+
+    def results(name):
+        """Return the results of the query
+
+        The value returned is an iterable.
+
+        None may be returned to indicate that there are no results.
+        """

Added: Zope3/trunk/src/zope/app/form/browser/source.py
===================================================================
--- Zope3/trunk/src/zope/app/form/browser/source.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/browser/source.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -0,0 +1,405 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""Source widgets support
+
+$Id$
+"""
+
+import cgi
+import zope.schema.interfaces
+from zope.schema.interfaces import ISourceQueriables
+from zope.app import zapi 
+import zope.app.form.interfaces
+import zope.app.form.browser.widget
+import zope.app.form.browser.interfaces
+from zope.app.i18n import ZopeMessageIDFactory as _
+
+
+class SourceDisplayWidget(zope.app.form.Widget):
+
+    def __init__(self, field, source, request):
+        super(SourceDisplayWidget, self).__init__(field, request)
+        self.source = source
+
+    def hidden(self):
+        return ''
+
+    def error(self):
+        return ''
+
+    def __call__(self):
+        """Render the current value
+        """
+
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            value = self.context.default
+            
+        if value == self.context.missing_value:
+            value = self._translate(_("SourceDisplayWidget-missing",
+                                      default="Nothing"))
+        else:
+            terms = zapi.getMultiAdapter(
+                (self.source, self.request),
+                zope.app.form.browser.interfaces.ITerms,
+                )
+                
+            try:
+                term = terms.getTerm(value)
+            except LookupError:
+                value = self._translate(_("SourceDisplayWidget-invalid",
+                                          default="Invalid value"))
+            else:
+                value = cgi.escape(term.title)
+
+        return value
+
+class SourceInputWidget(zope.app.form.InputWidget):
+
+    _error = None
+
+    zope.interface.implements(zope.app.form.interfaces.IInputWidget)
+
+    def __init__(self, field, source, request):
+        super(SourceInputWidget, self).__init__(field, request)
+        self.source = source
+        self.terms = zapi.getMultiAdapter(
+            (source, self.request),
+            zope.app.form.browser.interfaces.ITerms,
+            )
+
+        queriables = ISourceQueriables(source, None)
+        if queriables is None:
+            # treat the source itself as a queriable
+            queriables = ((self.name, source), )
+        else:
+            base = self.name+'.'
+            queriables = [(base + unicode(i).encode('base64').strip(), s)
+                          for (i, s) in queriables.getQueriables()]
+            
+        self.queryviews = [
+            (i, zapi.getMultiAdapter(
+                    (s, self.request),
+                    zope.app.form.browser.interfaces.ISourceQueryView,
+                    )
+             ) for (i, s) in queriables]
+            
+    def _value(self):
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            for name, queryview in self.queryviews:
+                if name+'.apply' in self.request:
+                    token = self.request.form.get(name+'.selection')
+                    if token is not None:
+                        break
+                else:
+                    token = self.request.form.get(self.name)
+                
+            if token is not None:
+                try:
+                    value = self.terms.getValue(str(token))
+                except LookupError:
+                    value = self.context.missing_value
+            else:
+                value = self.context.missing_value
+
+        return value
+    
+    def hidden(self):
+        value = self._value()
+        if value == self.context.missing_value:
+            return '' # Nothing to hide ;)
+
+        try:
+            term = self.terms.getTerm(value)
+        except LookupError:
+            # A value was set, but it's not valid.  Treat
+            # it as if it was missing and return nothing.
+            return ''
+                
+        return ('<input type="hidden" name="%s" value="%s">'
+                % (self.name, cgi.escape(term.token))
+                )
+
+    def error(self):
+        if self._error:
+            return zapi.getViewProviding(self._error, IWidgetInputErrorView,
+                                         self.request).snippet()
+        return ""
+    
+    def __call__(self):
+        result = ['<div class="value">']
+        value = self._value()
+        field = self.context
+
+        term = None
+        if value == field.missing_value:
+            result.append(u'  ' +
+                          self._translate(_("SourceDisplayWidget-missing",
+                                            default="Nothing"))
+                          )
+        else:
+            try:
+                term = self.terms.getTerm(value)
+            except LookupError:
+                result.append(u'  ' +
+                              self._translate(_("SourceDisplayWidget-missing",
+                                                default="Nothing Valid"))
+                              )
+            else:
+                result.append(u'  ' + cgi.escape(term.title))
+                result.append('  <input type="hidden" name="%s" value="%s">'
+                              % (self.name, cgi.escape(term.token)))
+        result.append('  <br>')
+
+        result.append('  <input type="hidden" name="%s.displayed" value="y">'
+                      % self.name)
+        
+        result.append('  <div class="queries">')
+        for name, queryview in self.queryviews:
+            result.append('    <div class="query">')
+            result.append('      <div class="queryinput">')
+            result.append(queryview.render(name+'.query'))
+            result.append('      </div> <!-- queryinput -->')
+
+            qresults = queryview.results(name+'.query')
+            if qresults:
+                result.append('      <div class="queryresults">\n%s' %
+                              self._renderResults(qresults, name))
+                result.append('      </div> <!-- queryresults -->')
+            result.append('    </div> <!-- query -->')
+        result.append('  </div> <!-- queries -->')
+        result.append('</div> <!-- value -->')
+        return '\n'.join(result)
+
+    def _renderResults(self, results, name):
+        terms = []
+        for value in results:
+            term = self.terms.getTerm(value)
+            terms.append((term.title, term.token))
+        terms.sort()
+        
+        return (
+            '<select name="%s.selection">\n'
+            '%s\n'
+            '</select>\n'
+            '<input type="submit" name="%s.apply" value="Apply">'
+            % (name,
+               '\n'.join(
+                   [('<option value="%s">%s</option>'
+                     % (token, title))
+                    for (title, token) in terms]),
+               name)
+            )
+
+    required = property(lambda self: self.context.required)
+
+    def getInputValue(self):
+        token = self.request.get(self.name)
+        field = self.context
+
+        if token is None:
+            if field.required:
+                raise zope.app.form.interfacesMissingInputError(
+                    field.__name__, self.label,
+                    )
+            return field.missing_value
+
+        try:
+            value = self.terms.getValue(str(token))
+        except LookupError:
+            err = zope.schema.interfaces.ValidationError(
+                "Invalid value id", token)
+            raise WidgetInputError(field.__name__, self.label, err)
+
+        # Remaining code copied from SimpleInputWidget
+
+        # value must be valid per the field constraints
+        try:
+            field.validate(value)
+        except ValidationError, err:
+            self._error = WidgetInputError(field.__name__, self.label, err)
+            raise self._error
+
+        return value
+
+    def hasInput(self):
+        return self.name+'.displayed' in self.request.form
+
+class SourceListInputWidget(SourceInputWidget):
+
+    def _value(self):
+        if self._renderedValueSet():
+            value = self._data
+        else:
+            tokens = self.request.form.get(self.name)
+            for name, queryview in self.queryviews:
+                if name+'.apply' in self.request:
+                    newtokens = self.request.form.get(name+'.selection')
+                    if newtokens:
+                        if tokens:
+                            tokens = tokens + newtokens
+                        else:
+                            tokens = newtokens
+            
+            if tokens:
+                remove = self.request.form.get(self.name+'.checked')
+                if remove and (self.name+'.remove' in self.request):
+                    tokens = [token
+                              for token in tokens
+                              if token not in remove
+                              ]
+                value = []
+                for token in tokens:
+                    try:
+                        v = self.terms.getValue(str(token))
+                    except LookupError:
+                        pass # skip invalid tokens (shrug)
+                    else:
+                        value.append(v)
+            else:
+                value = self.context.missing_value
+
+        if value:
+            r = []
+            seen = {}
+            for s in value:
+                if s not in seen:
+                    r.append(s)
+                    seen[s] = 1
+            value = r
+
+        return value
+    
+    def hidden(self):
+        value = self._value()
+        if value == self.context.missing_value:
+            return '' # Nothing to hide ;)
+
+        result = []
+        for v in value:
+            try:
+                term = self.terms.getTerm(value)
+            except LookupError:
+                # A value was set, but it's not valid.  Treat
+                # it as if it was missing and skip
+                continue
+            else:
+                result.append('<input type="hidden" name="%s:list" value="%s">'
+                              % (self.name, cgi.escape(term.token))
+                              )
+
+    def __call__(self):
+        result = ['<div class="value">']
+        value = self._value()
+        field = self.context
+
+        if value:
+            for v in value:
+                try:
+                    term = self.terms.getTerm(v)
+                except LookupError:
+                    continue # skip
+                else:
+                    result.append(
+                        '  <input type="checkbox" name="%s.checked:list"'
+                        ' value="%s">'
+                        % (self.name, cgi.escape(term.token))
+                        )
+                    result.append('  ' + cgi.escape(term.title))
+                    result.append(
+                        '  <input type="hidden" name="%s:list" value="%s">'
+                        % (self.name, cgi.escape(term.token)))
+                    result.append('  <br>')
+
+            result.append(
+                '  <input type="submit" name="%s.remove" value="%s">'
+                % (self.name,
+                   self._translate(_("MultipleSourceInputWidget-remove",
+                                     default="Remove")))
+                )
+            result.append('  <br>')
+
+        result.append('  <input type="hidden" name="%s.displayed" value="y">'
+                      % self.name)
+        
+        result.append('  <div class="queries">')
+
+        for name, queryview in self.queryviews:
+            result.append('    <div class="query">')
+            result.append('      <div class="queryinput">')
+            result.append(queryview.render(name+'.query'))
+            result.append('      </div> <!-- queryinput -->')
+
+            qresults = queryview.results(name+'.query')
+            if qresults:
+                result.append('      <div class="queryresults">\n%s' %
+                              self._renderResults(qresults, name))
+                result.append('      </div> <!-- queryresults -->')
+            result.append('    </div> <!-- query -->')
+
+        result.append('  </div> <!-- queries -->')
+        result.append('</div> <!-- value -->')
+        return '\n'.join(result)
+
+    def _renderResults(self, results, name):
+        terms = []
+        for value in results:
+            term = self.terms.getTerm(value)
+            terms.append((term.title, term.token))
+        terms.sort()
+        return (
+            '<select name="%s.selection:list" multiple>\n'
+            '%s\n'
+            '</select>\n'
+            '<input type="submit" name="%s.apply" value="Apply">'
+            % (name,
+               '\n'.join([('<option value="%s">%s</option>' % (token, title))
+                          for (title, token) in terms]),
+               name)
+            )
+
+    def getInputValue(self):
+        tokens = self.request.get(self.name)
+        field = self.context
+
+        if not tokens:
+            if field.required:
+                raise zope.app.form.interfacesMissingInputError(
+                    field.__name__, self.label,
+                    )
+            return field.missing_value
+
+        value = []
+        for token in tokens:
+            try:
+                v = self.terms.getValue(str(token))
+            except LookupError:
+                err = zope.schema.interfaces.ValidationError(
+                    "Invalid value id", token)
+                raise WidgetInputError(field.__name__, self.label, err)
+            value.append(v)
+            
+        # Remaining code copied from SimpleInputWidget
+
+        # value must be valid per the field constraints
+        try:
+            field.validate(value)
+        except ValidationError, err:
+            self._error = WidgetInputError(field.__name__, self.label, err)
+            raise self._error
+
+        return value

Added: Zope3/trunk/src/zope/app/form/browser/source.txt
===================================================================
--- Zope3/trunk/src/zope/app/form/browser/source.txt	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/browser/source.txt	2004-10-08 07:17:07 UTC (rev 27789)
@@ -0,0 +1,508 @@
+=============================
+Source Widget Query Framework
+=============================
+
+Sources are objects that represent sets of values from which one might
+choose and are used with Choice schema fields.  An important aspect of
+sources is that they have too many values to enumerate.  Rather than
+listing all of the values, we, instead, provide interfaces for
+querying values and selecting values from query results.  Matters are
+further complicated by the fact that different sources may have very
+different interfaces for querying them.
+
+To make matters more interesting, a source may be an aggregation of
+several collections, each with their own querying facilities.
+An example of such a source is a principal source, where principals
+might come from a number of places, such as an LDAP database and
+ZCML-based principal definitions.
+
+The default widgets for selecting values from sources use the
+following approach:
+
+- One or more query objects are obtained from the source by adapting
+  the source to `zope.schema.ISourceQueriables`.  If no adapter is
+  obtained, then the source itself is assumed to be queriable.
+
+- For each queriable found, a
+  `zope.app.form.browser.interfaces.ISourceQueryView' view is looked
+  up.  This view is used to obtain the HTML for displaying a query
+  form.  The view is also used to obtain search results.
+
+In addition to providing queriables and query views, the widgets need
+views that can be used to get tokens to represent source values in
+forms, as well as textual representations of values.  We use
+`zope.app.form.browser.interfaces.ITerms` views for that.
+
+Let's start with a simple example.  We have a very trivial source,
+which is basically a list:
+
+  >>> import zope.interface
+  >>> import zope.schema
+  >>> class SourceList(list):
+  ...     zope.interface.implements(zope.schema.interfaces.ISource)
+  
+We provide an `ITerms` view for the source:
+
+  >>> class Term:
+  ...
+  ...     def __init__(self, **kw):
+  ...         self.__dict__.update(kw)
+
+  >>> class ListTerms:
+  ...
+  ...     def __init__(self, source, request):
+  ...         pass # We don't actually need the source or the request :)
+  ... 
+  ...     def getTerm(self, value):
+  ...         title = unicode(value)
+  ...         token = title.encode('base64').strip()
+  ...         return Term(title=title, token=token)
+  ... 
+  ...     def getValue(self, token):
+  ...         return token.decode('base64')
+
+  >>> from zope.app.tests import ztapi
+  >>> from zope.publisher.interfaces.browser import IBrowserRequest
+  >>> import zope.app.form.browser.interfaces
+  >>> ztapi.provideAdapter((SourceList, IBrowserRequest), 
+  ...                      zope.app.form.browser.interfaces.ITerms,
+  ...                      ListTerms)
+   
+This view just uses the unicode representations of values as titles
+and the base-64 encoding of the titles as tokens.  This is a very 
+simple strategy that's only approriate when the values have short and
+unique unicode representations.
+
+We aren't going to provide an adapter to `ISourceQueriables`, so the
+source itself will be used as it's own queriable.  We need to provide a
+query view for the source:
+
+  >>> class ListQueryView:
+  ... 
+  ...     def __init__(self, source, request):
+  ...         self.source = source
+  ...         self.request = request
+  ... 
+  ...     def render(self, name):
+  ...         return (
+  ...             '<input name="%s.string">\n'
+  ...             '<input type="submit" name="%s" value="Search">'
+  ...             % (name, name)
+  ...             )
+  ... 
+  ...     def results(self, name):
+  ...         if name in self.request:
+  ...             search_string = self.request.get(name+'.string')
+  ...             if search_string is not None:
+  ...                 return [value
+  ...                         for value in self.source
+  ...                         if search_string in value
+  ...                         ]
+  ...         return None
+
+  >>> ztapi.provideAdapter((SourceList, IBrowserRequest), 
+  ...                      zope.app.form.browser.interfaces.ISourceQueryView,
+  ...                      ListQueryView)
+
+Now, we can define a choice field:
+
+  >>> dog = zope.schema.Choice(
+  ...    __name__ = 'dog',
+  ...    title=u"Dogs",
+  ...    source=SourceList(['spot', 'bowser', 'prince', 'duchess', 'lassie']),
+  ...    )
+
+When we get a choice input widget for a choice field, the default
+widget factory gets a view on the field and the field's source.  We'll
+just create the view directly:
+
+  >>> import zope.app.form.browser.source
+  >>> from zope.publisher.browser import TestRequest
+  >>> request = TestRequest()
+  >>> widget = zope.app.form.browser.source.SourceInputWidget(
+  ...     dog, dog.source, request)
+  
+Now if we render the widget, we'll see the input value (initially
+nothing) and a form elements for seaching for values:
+
+  >>> print widget()
+  <div class="value">
+    Nothing
+    <br>
+    <input type="hidden" name="field.dog.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.dog.query.string">
+  <input type="submit" name="field.dog.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+This shows that we haven't selected a dog. We get a search box that we
+can type seach strings into.  Let's supply a search string.  We do
+this by providing data in the form and by "selecting" the submit
+button:
+
+  >>> request.form['field.dog.displayed'] = u'y'
+  >>> request.form['field.dog.query.string'] = u'o'
+  >>> request.form['field.dog.query'] = u'Search'
+
+Now if we render the widget, we'll see the search results:
+
+  >>> print widget()
+  <div class="value">
+    Nothing
+    <br>
+    <input type="hidden" name="field.dog.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.dog.query.string">
+  <input type="submit" name="field.dog.query" value="Search">
+        </div> <!-- queryinput -->
+        <div class="queryresults">
+  <select name="field.dog.selection">
+  <option value="Ym93c2Vy">bowser</option>
+  <option value="c3BvdA==">spot</option>
+  </select>
+  <input type="submit" name="field.dog.apply" value="Apply">
+        </div> <!-- queryresults -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+If we select an item:
+
+  >>> request.form['field.dog.displayed'] = u'y'
+  >>> del request.form['field.dog.query.string']
+  >>> del request.form['field.dog.query']
+  >>> request.form['field.dog.selection'] = u'c3BvdA=='
+  >>> request.form['field.dog.apply'] = u'Apply'
+
+Then we'll show the newly selected value:
+
+  >>> print widget()
+  <div class="value">
+    spot
+    <input type="hidden" name="field.dog" value="c3BvdA==">
+    <br>
+    <input type="hidden" name="field.dog.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.dog.query.string">
+  <input type="submit" name="field.dog.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+Now, let's look at a more complicated example.  We'll define a source
+that combines multiple sources:
+
+  >>> class MultiSource:
+  ...
+  ...     zope.interface.implements(
+  ...        zope.schema.interfaces.ISource,
+  ...        zope.schema.interfaces.ISourceQueriables,
+  ...        )
+  ...
+  ...     def __init__(self, *sources):
+  ...         self.sources = [(unicode(i), s) for (i, s) in enumerate(sources)]
+  ...
+  ...     def __contains__(self, value):
+  ...         for i, s in self.sources:
+  ...             if value in s:
+  ...                 return True
+  ...         return False
+  ...
+  ...     def getQueriables(self):
+  ...         return self.sources
+
+This multi-source implements `ISourceQueriables`. It assumes that the
+sources it's given are queriable and just returns the sources as the
+queryable objects.
+
+We can reuse our terms view:
+
+  >>> ztapi.provideAdapter((MultiSource, IBrowserRequest), 
+  ...                      zope.app.form.browser.interfaces.ITerms,
+  ...                      ListTerms)
+
+Now, we'll create a pet choice that combines dogs and cats:
+
+  >>> pet = zope.schema.Choice(
+  ...    __name__ = 'pet',
+  ...    title=u"Dogs and Cats",
+  ...    source=MultiSource(
+  ...      dog.source,
+  ...      SourceList(['boots', 'puss', 'tabby', 'tom', 'tiger']),
+  ...      ),
+  ...    )
+
+and a widget:
+
+  >>> widget = zope.app.form.browser.source.SourceInputWidget(
+  ...     pet, pet.source, request)
+
+Now if we display the widget, we'll see search inputs for both dogs
+and cats:
+
+  >>> print widget()
+  <div class="value">
+    Nothing
+    <br>
+    <input type="hidden" name="field.pet.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MA==.query.string">
+  <input type="submit" name="field.pet.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MQ==.query.string">
+  <input type="submit" name="field.pet.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+As before, we can perform a search:
+
+  >>> request.form['field.pet.displayed'] = u'y'
+  >>> request.form['field.pet.MQ==.query.string'] = u't'
+  >>> request.form['field.pet.MQ==.query'] = u'Search'
+
+In which case, we'll get some results:
+
+  >>> print widget() # doctest:
+  <div class="value">
+    Nothing
+    <br>
+    <input type="hidden" name="field.pet.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MA==.query.string">
+  <input type="submit" name="field.pet.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MQ==.query.string">
+  <input type="submit" name="field.pet.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+        <div class="queryresults">
+  <select name="field.pet.MQ==.selection">
+  <option value="Ym9vdHM=">boots</option>
+  <option value="dGFiYnk=">tabby</option>
+  <option value="dGlnZXI=">tiger</option>
+  <option value="dG9t">tom</option>
+  </select>
+  <input type="submit" name="field.pet.MQ==.apply" value="Apply">
+        </div> <!-- queryresults -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+from which we can choose:
+
+  >>> request.form['field.pet.displayed'] = u'y'
+  >>> del request.form['field.pet.MQ==.query.string']
+  >>> del request.form['field.pet.MQ==.query']
+  >>> request.form['field.pet.MQ==.selection'] = u'dGFiYnk='
+  >>> request.form['field.pet.MQ==.apply'] = u'Apply'
+
+and get a selection:
+
+  >>> print widget()
+  <div class="value">
+    tabby
+    <input type="hidden" name="field.pet" value="dGFiYnk=">
+    <br>
+    <input type="hidden" name="field.pet.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MA==.query.string">
+  <input type="submit" name="field.pet.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pet.MQ==.query.string">
+  <input type="submit" name="field.pet.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+There's a display widget, which doesn't use queriables, since it
+doesn't assign values:
+
+  >>> request = TestRequest()
+  >>> widget = zope.app.form.browser.source.SourceDisplayWidget(
+  ...     pet, pet.source, request)
+  >>> print widget()
+  Nothing
+
+  >>> widget.setRenderedValue('tabby')
+  >>> print widget()
+  tabby
+
+If we specify a list of choices:
+
+  >>> pets = zope.schema.List(__name__ = 'pets', title=u"Pets",
+  ...                         value_type=pet)
+
+when a widget is computed for the field, a view will be looked up
+for the field and the source, where, in this case, the field is a
+list field.   We'll just call the widget factory directly:
+
+  >>> widget = zope.app.form.browser.source.SourceListInputWidget(
+  ...     pets, pets.value_type.source, request)
+
+If we render the widget:
+
+  >>> print widget()
+  <div class="value">
+    <input type="hidden" name="field.pets.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MA==.query.string">
+  <input type="submit" name="field.pets.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MQ==.query.string">
+  <input type="submit" name="field.pets.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+Here the output looks very similar to the simple choice case.  We get
+a search input for each source.  In this case, we don't show any
+inputs (XXX we probably should make it clearer that there are no
+selected values.)
+
+As before, we can search one of the sources:
+
+  >>> request.form['field.pets.displayed'] = u'y'
+  >>> request.form['field.pets.MQ==.query.string'] = u't'
+  >>> request.form['field.pets.MQ==.query'] = u'Search'
+
+In which case, we'll get some results:
+
+  >>> print widget()
+  <div class="value">
+    <input type="hidden" name="field.pets.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MA==.query.string">
+  <input type="submit" name="field.pets.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MQ==.query.string">
+  <input type="submit" name="field.pets.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+        <div class="queryresults">
+  <select name="field.pets.MQ==.selection:list" multiple>
+  <option value="Ym9vdHM=">boots</option>
+  <option value="dGFiYnk=">tabby</option>
+  <option value="dGlnZXI=">tiger</option>
+  <option value="dG9t">tom</option>
+  </select>
+  <input type="submit" name="field.pets.MQ==.apply" value="Apply">
+        </div> <!-- queryresults -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+from which we can select some values:
+
+  >>> request.form['field.pets.displayed'] = u'y'
+  >>> del request.form['field.pets.MQ==.query.string']
+  >>> del request.form['field.pets.MQ==.query']
+  >>> request.form['field.pets.MQ==.selection'] = [
+  ...     u'dGFiYnk=', u'dGlnZXI=', u'dG9t']
+  >>> request.form['field.pets.MQ==.apply'] = u'Apply'
+
+Which then leads to the selections appearing as widget selections:
+
+  >>> print widget()
+  <div class="value">
+    <input type="checkbox" name="field.pets.checked:list" value="dGFiYnk=">
+    tabby
+    <input type="hidden" name="field.pets:list" value="dGFiYnk=">
+    <br>
+    <input type="checkbox" name="field.pets.checked:list" value="dGlnZXI=">
+    tiger
+    <input type="hidden" name="field.pets:list" value="dGlnZXI=">
+    <br>
+    <input type="checkbox" name="field.pets.checked:list" value="dG9t">
+    tom
+    <input type="hidden" name="field.pets:list" value="dG9t">
+    <br>
+    <input type="submit" name="field.pets.remove" value="Remove">
+    <br>
+    <input type="hidden" name="field.pets.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MA==.query.string">
+  <input type="submit" name="field.pets.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MQ==.query.string">
+  <input type="submit" name="field.pets.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->
+
+We now see the values we selected.  We also have chackboxes and
+buttons that allow is to remove selections:
+
+  >>> request.form['field.pets.displayed'] = u'y'
+  >>> request.form['field.pets'] = [u'dGFiYnk=', u'dGlnZXI=', u'dG9t']
+  >>> del request.form['field.pets.MQ==.selection']
+  >>> del request.form['field.pets.MQ==.apply']
+  >>> request.form['field.pets.checked'] = [u'dGFiYnk=', u'dG9t']
+  >>> request.form['field.pets.remove'] = u'Remove'
+
+  >>> print widget()
+  <div class="value">
+    <input type="checkbox" name="field.pets.checked:list" value="dGlnZXI=">
+    tiger
+    <input type="hidden" name="field.pets:list" value="dGlnZXI=">
+    <br>
+    <input type="submit" name="field.pets.remove" value="Remove">
+    <br>
+    <input type="hidden" name="field.pets.displayed" value="y">
+    <div class="queries">
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MA==.query.string">
+  <input type="submit" name="field.pets.MA==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+      <div class="query">
+        <div class="queryinput">
+  <input name="field.pets.MQ==.query.string">
+  <input type="submit" name="field.pets.MQ==.query" value="Search">
+        </div> <!-- queryinput -->
+      </div> <!-- query -->
+    </div> <!-- queries -->
+  </div> <!-- value -->

Added: Zope3/trunk/src/zope/app/form/browser/tests/test_source.py
===================================================================
--- Zope3/trunk/src/zope/app/form/browser/tests/test_source.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/browser/tests/test_source.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -0,0 +1,30 @@
+##############################################################################
+#
+# Copyright (c) 2004 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.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+
+from zope.app.tests import placelesssetup
+
+def test_suite():
+    from zope.testing import doctest
+    return doctest.DocFileSuite(
+        '../source.txt',
+        setUp=placelesssetup.setUp, tearDown=placelesssetup.tearDown)
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main(defaultTest='test_suite')
+

Modified: Zope3/trunk/src/zope/app/form/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/form/interfaces.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/app/form/interfaces.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -34,7 +34,7 @@
     
     implements(IWidgetInputError)
 
-    def __init__(self, field_name, widget_title, errors):
+    def __init__(self, field_name, widget_title, errors=None):
         """Initialize Error
 
         `errors` is a ``ValidationError`` or a list of ValidationError objects
@@ -180,7 +180,7 @@
     def applyChanges(content):
         """Validate the widget data and apply it to the content.
 
-        See `validate()` for validation performed.
+        Return a boolean indicating whether a change was actually applied.
         """
 
     def hasInput():

Modified: Zope3/trunk/src/zope/schema/_field.py
===================================================================
--- Zope3/trunk/src/zope/schema/_field.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/schema/_field.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -33,7 +33,7 @@
 from zope.schema.interfaces import IChoice, ITuple, IList, ISet, IDict
 from zope.schema.interfaces import IPassword, IObject, IDate
 from zope.schema.interfaces import IURI, IId, IFromUnicode
-from zope.schema.interfaces import IVocabulary
+from zope.schema.interfaces import ISource, IVocabulary
 
 from zope.schema.interfaces import ValidationError, InvalidValue
 from zope.schema.interfaces import WrongType, WrongContainedType, NotUnique
@@ -175,13 +175,24 @@
     """
     implements(IChoice)
 
-    def __init__(self, values=None, vocabulary=None, **kw):
+    def __init__(self, values=None, vocabulary=None, source=None, **kw):
         """Initialize object."""
-        assert not (values is None and vocabulary is None), \
-               "You must specify either values or vocabulary."
-        assert values is None or vocabulary is None, \
-               "You cannot specify both values and vocabulary."
+
+
+        if vocabulary is not None:
+            assert (isinstance(vocabulary, basestring)
+                    or IVocabulary.providedBy(vocabulary))
+            assert source is None, (
+                "You cannot specify both source and vocabulary.")
+        elif source is not None:
+            assert ISource.providedBy(source)
+            vocabulary = source
         
+        assert not (values is None and vocabulary is None), (
+               "You must specify either values or vocabulary.")
+        assert values is None or vocabulary is None, (
+               "You cannot specify both values and vocabulary.")
+        
         self.vocabulary = None
         self.vocabularyName = None
         if values is not None:
@@ -189,7 +200,7 @@
         elif isinstance(vocabulary, (unicode, str)):
             self.vocabularyName = vocabulary
         else:
-            assert IVocabulary.providedBy(vocabulary)
+            assert ISource.providedBy(vocabulary)
             self.vocabulary = vocabulary
         # Before a default value is checked, it is validated. However, a
         # named vocabulary is usually not complete when these fields are
@@ -200,6 +211,8 @@
         super(Choice, self).__init__(**kw)
         self._init_field = False
 
+    source = property(lambda self: self.vocabulary)
+
     def bind(self, object):
         """See zope.schema._bootstrapinterfaces.IField."""
         clone = super(Choice, self).bind(object)

Modified: Zope3/trunk/src/zope/schema/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/schema/interfaces.py	2004-10-08 05:21:51 UTC (rev 27788)
+++ Zope3/trunk/src/zope/schema/interfaces.py	2004-10-08 07:17:07 UTC (rev 27789)
@@ -462,7 +462,49 @@
     
     title = TextLine(title=_(u"Title"))
 
-class IBaseVocabulary(Interface):
+class ISource(Interface):
+    """A set of values from which to choose
+
+    Sources represent sets of values. They are used to specify the
+    source for choice fields.
+
+    Sources can be large (even infinite), in which case, they need to
+    be queried to find out what their values are.
+    
+    """
+
+    def __contains__(value):
+        """Return whether the value is available in this source
+        """
+
+class ISourceQueriables(Interface):
+    """A collection of objects for querying sources
+    """
+
+    def getQueriables():
+        """Return an iterable of objects that can be queried
+
+        The returned obects should be two-tuples with:
+
+        - A unicode id
+
+          The id must uniquely identify the queriable object within
+          the set of queryable objects. Furthermore, in subsequent
+          calls, the same id should be used for a given queriable
+          object.
+
+        - A queryable object
+
+          This is an object for which there is a view is provided for
+          searcing for items.
+
+        """
+    
+
+# TODO, define iterable sources.  For now, we'll just use vocabularies.
+# Eventually, we'll replace vocabularies with iterable sources.
+    
+class IBaseVocabulary(ISource):
     """Representation of a vocabulary.
 
     At this most basic level, a vocabulary only need to support a test
@@ -471,9 +513,6 @@
     vocabularies which are intrinsically ordered).
     """
 
-    def __contains__(value):
-        """Returns True if the value is available in this vocabulary."""
-
     def getTerm(value):
         """Return the ITerm object for the term 'value'.
 



More information about the Zope3-Checkins mailing list