[Zope3-checkins]
SVN: Zope3/branches/f12gsprint-widget/src/zope/widget/
Added doctests, fleshed out interface and implementation.
Dylan Reinhardt
zope at dylanreinhardt.com
Fri Sep 2 16:13:03 EDT 2005
Log message for revision 38248:
Added doctests, fleshed out interface and implementation.
Changed:
U Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py
A Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py
A Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py
A Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt
-=-
Modified: Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py 2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/interfaces.py 2005-09-02 20:13:02 UTC (rev 38248)
@@ -22,7 +22,13 @@
from zope.interface import Attribute, Interface, implements
from zope.schema import Bool
+# TODO: dots in names
+class ConversionError(ValueError):
+ """ Value could not be converted to correct type """
+
+class InvalidStateError(RuntimeError):
+ """ This widget's state has been invalidated by a call to setValue()"""
class IBaseWidget(IView):
"""Generically describes the behavior of a widget.
@@ -48,9 +54,6 @@
Hint may be translated for the request.""")
- visible = Attribute(
- """A flag indicating whether or not the widget is visible.""")
-
required = Bool(
title=u"Required",
description=u"""If True, widget should be displayed as requiring input.
@@ -69,8 +72,7 @@
def initialize(prefix=None, value=None, state=None):
""" Initialize widget and set its value.
- If any widget method is called before initialize is called,
- raise RuntimeError.
+ Initialize must be called before any other method besides __init__.
If prefix is passed in, the prefix attribute of the widget is set.
Prefix must be None or be a string. See prefix attribute for more
@@ -85,22 +87,21 @@
from the getState method of a widget of the same class.
Only one of value or state may be passed, passing both raises
- ValueError.
+ TypeError.
- If the widget's value is not valid, its error attribute will contain an
- appropriate ValidationError. If a widget wishes to add an object to
- message, that may be done here.
+ If the widget's value is not valid, its error attribute will contain
+ either a ConversionError or zope.schema.interfaces.ValidationError. If
+ a widget wishes to add an object to message, that may be done here.
"""
def getState():
- """ Return a pickleable object representing the state of the widget as
- currently rendered.
+ """ If the widget has been viewed previously, returns a non-None,
+ picklable object representing the widget's state, unless setValue has
+ been called, in which case it raises InvalidStateError. Otherwise,
+ returns None.
- This object can later be passed in to initialize to restore the
+ A state object can later be passed in to initialize to restore the
state of the widget.
-
- If the widget has never been rendered, getState should return None.
- If the widget *has* been rendered, returning None would be an error.
"""
def hasState():
@@ -124,7 +125,7 @@
Prefix name may be None, or a string. Any other value raises
ValueError. When prefixes are concatenated with the widget name, a dot
is used as a delimiter; a trailing dot is neither required nor suggested
- in the prefix itself.""")
+ in the prefix itself. """)
error = Attribute(""" An exception created by initialize or setValue. If
this value is not None, it is raised when getValue is called.
Added: Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py 2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/tests.py 2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,10 @@
+import unittest
+from zope.testing import doctest
+
+def test_suite():
+ return unittest.TestSuite((
+ doctest.DocFileSuite(
+ 'widget.txt'),))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest = 'test_suite')
\ No newline at end of file
Added: Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py 2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/widget.py 2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,141 @@
+# simple implementation of new widget API
+
+from zope import interface
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+from zope.schema.interfaces import ValidationError
+
+import zope.widget.interfaces
+
+class BaseInputWidget(object):
+ """base simple widget"""
+
+ interface.implements(zope.widget.interfaces.IInputWidget)
+ __doc__ = zope.widget.interfaces.IInputWidget.__doc__
+
+ _name = None
+ name = property(lambda self: self._name)
+
+ _prefix = None
+ def prefix(self, value):
+ self._prefix = value
+ if value is None:
+ self._name = self.context.__name__
+ else:
+ self._name = '.'.join((value, self.context.__name__))
+ prefix = property(lambda self: self._prefix, prefix)
+
+ _error = None
+ error = property(lambda self: self._error)
+
+ _message = None
+ message = property(lambda self: self._message)
+
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ self.label = context.title
+ self.hint = context.description
+ self.required = context.required
+
+ _valueForced = False
+ _initialized = False
+ _state = None
+ def initialize(self, prefix=None, value=None, state=None):
+ self._initialized = True
+ self.prefix = prefix
+ if state is None:
+ state = self._calculateStateFromRequest()
+ else:
+ if value is not None:
+ raise TypeError('May pass only one of value and state')
+ self._state = state
+ if value is None:
+ if self._state is None:
+ value = self.context.default
+ else:
+ value = self._calculateValueFromState()
+ self.setValue(value)
+ self._valueForced = False
+ else:
+ self.setValue(value)
+
+ def getState(self):
+ if not self._initialized:
+ raise RuntimeError('Initialize widget first')
+ if self._state is not None and self._valueForced:
+ raise zope.widget.interfaces.InvalidStateError()
+ return self._state
+
+ def hasState(self):
+ if not self._initialized:
+ raise RuntimeError('Initialize widget first')
+ return self._state is not None
+
+ _value = None
+ def getValue(self):
+ if not self._initialized:
+ raise RuntimeError('Initialize widget first')
+ if self.error is not None:
+ raise self.error
+ return self._value
+
+ def setValue(self, value):
+ if not self._initialized:
+ raise RuntimeError('Initialize widget first')
+ try:
+ self.context.validate(value)
+ except ValidationError, e:
+ self._error = e
+ else:
+ self._error = None
+ self._message = None
+ self._value = value
+ self._valueForced = True
+
+ def __call__(self):
+ raise NotImplementedError
+
+ def _calculateStateFromRequest(self):
+ """return the widget's state object if it was rendered previously, or
+ None"""
+ return self.request.form.get(self.name)
+
+ def _calculateValueFromState(self):
+ """return the current value on the basis of the _state attribute"""
+ raise NotImplementedError
+
+
+class AdvancedBaseInputWidget(BaseInputWidget):
+ """base advanced widget"""
+
+ def _calculateStateFromRequest(self):
+ res = {}
+ name = self.name
+ len_name = len(name)
+ prename = name + "."
+ for n, v in self.request.form.items():
+ if n == name or n.startswith(prename):
+ res[n[len_name:]] = v
+ return res or None
+
+class TextLineWidget(BaseInputWidget):
+
+ #template = namedtemplate.NamedTemplate('default')
+
+ def __call__(self):
+ if self._valueForced:
+ value = self.getValue()
+ else:
+ value = self._state
+ return self.template(value=value)
+
+ #("""<input type="text" value=%(value)s name=%(name)s """
+ # """id=%(name)s size="20" />""")
+
+ def _calculateValueFromState(self):
+ try:
+ value = unicode(self._state)
+ except ValueError, v: # XXX
+ e = zope.widget.interfaces.ConversionError(v)
+ else:
+ return value
\ No newline at end of file
Added: Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt
===================================================================
--- Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt 2005-09-02 19:50:56 UTC (rev 38247)
+++ Zope3/branches/f12gsprint-widget/src/zope/widget/widget.txt 2005-09-02 20:13:02 UTC (rev 38248)
@@ -0,0 +1,298 @@
+=====
+Widget API
+=====
+
+[...]
+
+
+=====================
+
+
+
+
+
+
+ >>> from zope.widget import widget, interfaces
+
+ >>> from zope.publisher.browser import TestRequest
+ >>> from zope.schema import TextLine
+ >>> request = TestRequest()
+ >>> field = TextLine(__name__='test_name', title=u'Test Title',
+ ... min_length=2, description=u'Test Description')
+ >>> context = object()
+ >>> field = field.bind(context)
+ >>> w = widget.TextLineWidget(field, request)
+
+After creating a widget, the next step that must be taken is to initialize it.
+
+This is a separate step because the adapter machinery looks up the widget by
+context and request and instantiates it. The widget must then be told how to
+obtain its value using initialize().
+
+The initialize method has three optional arguments: prefix, value and state.
+
+A widget that is initialized without any of these arguments will use the field
+default value and will have a name equivalent to the field name.
+
+ >>> w.initialize()
+ >>> w.prefix # None
+ >>> w.name
+ 'test_name'
+ >>> w.getState() # None
+ >>> w.hasState()
+ False
+
+Because we have set no value for the widget, nor was any available in the
+request, a required field will raise a RequiredMissing is getValue is called.
+
+ >>> w.getValue()
+ Traceback (most recent call last):
+ ...
+ RequiredMissing
+
+If the previous request rendered the field, the state will be gathered from it
+during initialize; if value is not passed explicitly to initialize, it will
+be set from the gathered state.
+
+ >>> request = TestRequest()
+ >>> request.form['test_name'] = u'Joe'
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize()
+ >>> w.hasState()
+ True
+ >>> w.getState() is not None
+ True
+ >>> w.getValue()
+ u'Joe'
+
+A widget that is initialized with a prefix will prepend that prefix, plus a dot,
+to the field name.
+
+ >>> request = TestRequest()
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(prefix='test_prefix')
+ >>> w.prefix
+ 'test_prefix'
+ >>> w.name
+ 'test_prefix.test_name'
+ >>> w.hasState()
+ False
+
+The prefix is also used for finding the previous information in the request, if
+any.
+
+ >>> request = TestRequest()
+ >>> request.form['test_prefix.test_name'] = u'Joe'
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(prefix='test_prefix')
+ >>> w.getValue()
+ u'Joe'
+ >>> w.hasState()
+ True
+ >>> w.getState() is not None
+ True
+
+Prefix may be set directly. It affects the read-only name attribute.
+
+ >>> w.prefix = 'new_prefix'
+ >>> w.name
+ 'new_prefix.test_name'
+ >>> w.prefix = None
+ >>> w.name
+ 'test_name'
+
+If a widget has been initialized, it will already have state gathered from the
+request, whether or not the prefix is the same. This can be useful if the
+widget was drawn with one prefix, with valuable data, but needs to be redrawn
+with a new prefix.
+
+ >>> w.getValue()
+ u'Joe'
+ >>> w.hasState()
+ True
+ >>> w.getState() is not None
+ True
+
+A widget that is initialized with a value will attempt to set that value if
+it is valid.
+
+ >>> request = TestRequest()
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(value=u'test value')
+ >>> w.getValue()
+ u'test value'
+ >>> w.hasState()
+ False
+ >>> w.getState() # None
+
+State objects are black boxes. The only contract they have is that they are
+pickleable. Do not rely on their values, except for their intended use.
+
+It is possible to keep a widget's state and use it to initilize a new widget
+of the same class. This affects the value and whatever rendering the widget
+supports. It does not affect the prefix.
+
+ >>> request = TestRequest()
+ >>> request.form['test_prefix.test_name'] = u'Joe'
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(prefix='test_prefix')
+ >>> s = w.getState()
+
+ >>> request = TestRequest()
+ >>> w2 = widget.TextLineWidget(field, request)
+ >>> w2.initialize(state=s)
+ >>> w2.hasState()
+ True
+
+ >>> w.getState() == w2.getState()
+ True
+ >>> w.getValue() == w2.getValue()
+ True
+ >>> w.prefix != w2.prefix
+ True
+ >>> w.name != w2.name
+ True
+
+A widget that is initialized with an invalid or unconvertable value will
+store an exception object in the error attribute. Calling getValue raises
+that error.
+
+ >>> from zope.schema.interfaces import ValidationError
+ >>> request = TestRequest()
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(value=5)
+ >>> isinstance(w.error, ValidationError)
+ True
+ >>> w.getState() # None
+ >>> w.hasState()
+ False
+ >>> w.getValue()
+ Traceback (most recent call last):
+ ...
+ WrongType: (5, <type 'unicode'>)
+
+
+
+ >>> request = TestRequest()
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize()
+ >>> w.setValue(5)
+ >>> isinstance(w.error, ValidationError)
+ True
+ >>> w.getState() # None
+ >>> w.hasState()
+ False
+ >>> w.getValue()
+ Traceback (most recent call last):
+ ...
+ WrongType: (5, <type 'unicode'>)
+
+
+ >>> request = TestRequest()
+ >>> request.form['test_name'] = 't' # violates min length
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize()
+ >>> isinstance(w.error, ValidationError)
+ True
+ >>> w.getState() is not None
+ True
+ >>> w.hasState()
+ True
+ >>> w.getValue()
+ Traceback (most recent call last):
+ ...
+ TooShort: (u't', 2)
+
+
+Attempting to pass both a value and a state to initialize will raise a
+TypeError.
+
+ >>> request = TestRequest()
+ >>> w3 = widget.TextLineWidget(field, request)
+ >>> w3.initialize(state=s, value='foo')
+ Traceback (most recent call last):
+ ...
+ TypeError: May pass only one of value and state
+
+Note that if the widget has state and you pass in a value to initialize or
+call setValue, calling getState will raise an InvalidStateError.
+
+ >>> request = TestRequest()
+ >>> request.form['test_prefix.test_name'] = u'Joe' # widget has state
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(prefix='test_prefix', value='foo')
+ >>> s = w.getState()
+ Traceback (most recent call last):
+ ...
+ InvalidStateError
+
+ >>> request = TestRequest()
+ >>> request.form['test_prefix.test_name'] = u'Joe' # widget has state
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize(prefix='test_prefix')
+ >>> w.setValue('foo')
+ >>> s = w.getState()
+ Traceback (most recent call last):
+ ...
+ InvalidStateError
+
+
+
+Until the widget has been initialized, the default implementation raises
+RuntimeError if another method is called as a help to the developer.
+
+ >>> w = widget.TextLineWidget(field, TestRequest())
+ >>> w.setValue('foo')
+ Traceback (most recent call last):
+ ...
+ RuntimeError: Initialize widget first
+
+ >>> w.getValue()
+ Traceback (most recent call last):
+ ...
+ RuntimeError: Initialize widget first
+
+ >>> w.getState()
+ Traceback (most recent call last):
+ ...
+ RuntimeError: Initialize widget first
+
+ >>> w.hasState()
+ Traceback (most recent call last):
+ ...
+ RuntimeError: Initialize widget first
+
+
+Widget objects have a label and a hint attribute. Translation of these
+values is left to the template or presentation code. Widget objects also have a
+required attribute.
+
+ >>> request = TestRequest()
+ >>> w = widget.TextLineWidget(field, request)
+ >>> w.initialize()
+ >>> w.label == field.title
+ True
+ >>> w.label
+ u'Test Title'
+ >>> w.hint == field.description
+ True
+ >>> w.hint
+ u'Test Description'
+ >>> w.required == field.required
+ True
+ >>> w.required
+ True
+
+All three can be changed by setting the property.
+
+ >>> w.label = 'test label'
+ >>> w.label
+ 'test label'
+ >>> w.hint = 'test hint'
+ >>> w.hint
+ 'test hint'
+ >>> w.required = False
+ >>> w.required
+ False
+
+# XXX remember to show __call__ and message
More information about the Zope3-Checkins
mailing list