[Checkins] SVN: z3c.form/branches/adamg-missing-terms/src/z3c/form/ trying to display/accept terms for IChoice that want away
Adam Groszer
cvs-admin at zope.org
Thu Sep 6 09:26:09 UTC 2012
Log message for revision 127744:
trying to display/accept terms for IChoice that want away
Changed:
A z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
U z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py
U z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py
U z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml
U z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py
U z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py
U z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt
U z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py
A z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt
-=-
Added: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt (rev 0)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt 2012-09-06 09:26:05 UTC (rev 127744)
@@ -0,0 +1,213 @@
+============================
+Select Widget, missing terms
+============================
+
+The select widget allows you to select one or more values from a set of given
+options. The "SELECT" and "OPTION" elements are described here:
+
+http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#edef-SELECT
+
+As for all widgets, the select widget must provide the new ``IWidget``
+interface:
+
+ >>> from z3c.form import interfaces
+ >>> from z3c.form.browser import select
+
+The widget can be instantiated only using the request:
+
+ >>> from z3c.form.testing import TestRequest
+ >>> request = TestRequest()
+
+ >>> widget = select.SelectWidget(request)
+
+Before rendering the widget, one has to set the name and id of the widget:
+
+ >>> widget.id = 'widget-id'
+ >>> widget.name = 'widget.name'
+
+We also need to register the template for at least the widget and request:
+
+ >>> import zope.component
+ >>> from zope.pagetemplate.interfaces import IPageTemplate
+ >>> from z3c.form.testing import getPath
+ >>> from z3c.form.widget import WidgetTemplateFactory
+
+ >>> zope.component.provideAdapter(
+ ... WidgetTemplateFactory(getPath('select_input.pt'), 'text/html'),
+ ... (None, None, None, None, interfaces.ISelectWidget),
+ ... IPageTemplate, name=interfaces.INPUT_MODE)
+
+Let's provide some values for this widget. We can do this by defining a source
+providing ``ITerms``. This source uses descriminators which will fit our setup.
+
+ >>> import zope.schema.interfaces
+ >>> from zope.schema.vocabulary import SimpleVocabulary
+ >>> from zope.schema.vocabulary import SimpleTerm
+ >>> import z3c.form.term
+
+ >>> class SelectionTerms(z3c.form.term.MissingChoiceTermsVocabulary):
+ ... def __init__(self, context, request, form, field, widget):
+ ... self.terms = SimpleVocabulary.fromValues(['a', 'b', 'c'])
+ ...
+ ... def makeMissingTerm(self, token):
+ ... if token == 'x':
+ ... return super(SelectionTerms, self).makeMissingTerm(token)
+ ... else:
+ ... raise LookupError
+
+ >>> zope.component.provideAdapter(SelectionTerms,
+ ... (None, interfaces.IFormLayer, None, None, interfaces.ISelectWidget) )
+
+Now let's try if we get widget values:
+
+ >>> widget.update()
+ >>> print widget.render()
+ <select id="widget-id" name="widget.name:list"
+ class="select-widget" size="1">
+ <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+ <option id="widget-id-0" value="a">a</option>
+ <option id="widget-id-1" value="b">b</option>
+ <option id="widget-id-2" value="c">c</option>
+ </select>
+ <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+If we select item "x", then it should be selected:
+
+ >>> widget.value = ['x']
+ >>> widget.update()
+ >>> print widget.render()
+ <select id="widget-id" name="widget.name:list"
+ class="select-widget" size="1">
+ <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+ <option id="widget-id-0" value="a">a</option>
+ <option id="widget-id-1" value="b">b</option>
+ <option id="widget-id-2" value="c">c</option>
+ <option id="widget-id-missing-0" selected="selected" value="x">Missing: x</option>
+ </select>
+ <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+If we select item "y", then it should not be around:
+
+ >>> widget.value = ['y']
+ >>> widget.update()
+ >>> print widget.render()
+ <select id="widget-id" name="widget.name:list"
+ class="select-widget" size="1">
+ <option id="widget-id-novalue" value="--NOVALUE--">no value</option>
+ <option id="widget-id-0" value="a">a</option>
+ <option id="widget-id-1" value="b">b</option>
+ <option id="widget-id-2" value="c">c</option>
+ </select>
+ <input name="widget.name-empty-marker" type="hidden" value="1" />
+
+Let's now make sure that we can extract user entered data from a widget:
+
+ >>> widget.request = TestRequest(form={'widget.name': ['c']})
+ >>> widget.update()
+ >>> widget.extract()
+ ['c']
+
+When "no value" is selected, then no verification against the terms is done:
+
+ >>> widget.request = TestRequest(form={'widget.name': ['--NOVALUE--']})
+ >>> widget.update()
+ >>> widget.extract(default=1)
+ ['--NOVALUE--']
+
+Let's now make sure that we can extract user entered missing data from a widget:
+
+ >>> widget.request = TestRequest(form={'widget.name': ['x']})
+ >>> widget.update()
+ >>> widget.extract()
+ ['x']
+
+ >>> widget.request = TestRequest(form={'widget.name': ['y']})
+ >>> widget.update()
+ >>> widget.extract()
+ <NO_VALUE>
+
+Unfortunately, when nothing is selected, we do not get an empty list sent into
+the request, but simply no entry at all. For this we have the empty marker, so
+that:
+
+ >>> widget.request = TestRequest(form={'widget.name-empty-marker': '1'})
+ >>> widget.update()
+ >>> widget.extract()
+ []
+
+If nothing is found in the request, the default is returned:
+
+ >>> widget.request = TestRequest()
+ >>> widget.update()
+ >>> widget.extract(default=1)
+ 1
+
+Let's now make sure that a bogus value causes extract to return the default as
+described by the interface:
+
+ >>> widget.request = TestRequest(form={'widget.name': ['y']})
+ >>> widget.update()
+ >>> widget.extract(default=1)
+ 1
+
+Display Widget
+--------------
+
+The select widget comes with a template for ``DISPLAY_MODE``. Let's
+register it first:
+
+ >>> zope.component.provideAdapter(
+ ... WidgetTemplateFactory(getPath('select_display.pt'), 'text/html'),
+ ... (None, None, None, None, interfaces.ISelectWidget),
+ ... IPageTemplate, name=interfaces.DISPLAY_MODE)
+
+ >>> widget.required = True
+ >>> widget.mode = interfaces.DISPLAY_MODE
+ >>> widget.value = ['b', 'c']
+ >>> widget.update()
+ >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+ <span id="widget-id" class="select-widget required">
+ <span class="selected-option">b</span>,
+ <span class="selected-option">c</span>
+ </span>
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+ >>> widget.value = ['b', 'x', 'y']
+ >>> widget.update()
+ >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+ <span id="widget-id" class="select-widget required">
+ <span class="selected-option">b</span>,
+ <span class="selected-option">Missing: x</span>
+ </span>
+
+Hidden Widget
+-------------
+
+The select widget comes with a template for ``HIDDEN_MODE``. Let's
+register it first:
+
+ >>> zope.component.provideAdapter(
+ ... WidgetTemplateFactory(getPath('select_hidden.pt'), 'text/html'),
+ ... (None, None, None, None, interfaces.ISelectWidget),
+ ... IPageTemplate, name=interfaces.HIDDEN_MODE)
+
+We can now set our widget's mode to hidden and render it:
+
+ >>> widget.mode = interfaces.HIDDEN_MODE
+ >>> widget.value = ['b']
+ >>> widget.update()
+ >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+ <input type="hidden" name="widget.name:list"
+ class="hidden-widget" value="b" id="widget-id-1" />
+ <input name="widget.name-empty-marker" type="hidden"
+ value="1" />
+
+Let's see what happens if we have values that are not in the vocabulary:
+
+ >>> widget.value = ['b', 'x', 'y']
+ >>> widget.update()
+ >>> print widget.render() # doctest: +NORMALIZE_WHITESPACE
+ <input id="widget-id-1" name="widget.name:list" value="b" class="hidden-widget" type="hidden" />
+ <input id="widget-id-missing-0" name="widget.name:list" value="x" class="hidden-widget" type="hidden" />
+ <input name="widget.name-empty-marker" type="hidden" value="1" />
Property changes on: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select-miss.txt
___________________________________________________________________
Added: svn:keywords
+ Date Author Id Revision
Added: svn:eol-style
+ native
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/select.py 2012-09-06 09:26:05 UTC (rev 127744)
@@ -66,16 +66,35 @@
'content': message,
'selected': self.value == []
})
- for count, term in enumerate(self.terms):
+
+ ignored = set(self.value)
+
+ def addItem(idx, term, prefix=''):
selected = self.isSelected(term)
- id = '%s-%i' % (self.id, count)
+ if selected:
+ ignored.remove(term.token)
+ id = '%s-%s%i' % (self.id, prefix, idx)
content = term.token
if zope.schema.interfaces.ITitledTokenizedTerm.providedBy(term):
content = translate(
term.title, context=self.request, default=term.title)
items.append(
- {'id':id, 'value':term.token, 'content':content,
- 'selected':selected})
+ {'id': id, 'value': term.token, 'content': content,
+ 'selected': selected})
+
+ for idx, term in enumerate(self.terms):
+ addItem(idx, term)
+
+ if ignored:
+ # some values are not displayed, probably they went away from the vocabulary
+ for idx, token in enumerate(sorted(ignored)):
+ try:
+ term = self.terms.getTermByToken(token)
+ except LookupError:
+ # just in case the term really went away
+ continue
+
+ addItem(idx, term, prefix='missing-')
return items
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/browser/tests.py 2012-09-06 09:26:05 UTC (rev 127744)
@@ -94,6 +94,11 @@
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
checker=checker,
),
+ DocFileSuite('select-miss.txt',
+ setUp=setUp, tearDown=testing.tearDown,
+ optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+ checker=checker,
+ ),
DocFileSuite('select-source.txt',
setUp=setUp, tearDown=testing.tearDown,
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/configure.zcml 2012-09-06 09:26:05 UTC (rev 127744)
@@ -87,6 +87,9 @@
factory=".term.ChoiceTermsVocabulary"
/>
<adapter
+ factory=".term.MissingChoiceTermsVocabulary"
+ />
+ <adapter
factory=".term.ChoiceTermsSource"
/>
<adapter
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/interfaces.py 2012-09-06 09:26:05 UTC (rev 127744)
@@ -1110,3 +1110,17 @@
class IAfterWidgetUpdateEvent(IWidgetEvent):
"""An event sent out after the widget was updated."""
+
+
+
+
+class IForgivingChoice(zope.schema.interfaces.IChoice):
+ pass
+
+class ForgivingChoice(zope.schema.Choice):
+ zope.interface.implements(IForgivingChoice)
+
+ def _validate(self, value):
+ if self.context is not None:
+ if self.query(self.context) == value:
+ return
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/term.py 2012-09-06 09:26:05 UTC (rev 127744)
@@ -37,7 +37,7 @@
return self.terms.getTermByToken(token)
def getValue(self, token):
- return self.terms.getTermByToken(token).value
+ return self.getTermByToken(token).value
def __iter__(self):
return iter(self.terms)
@@ -48,6 +48,7 @@
def __contains__(self, value):
return self.terms.__contains__(value)
+
class SourceTerms(Terms):
"""Base implementation for ITerms using a source instead of a vocabulary."""
@@ -123,6 +124,54 @@
self.terms = vocabulary
+class MissingTermsMixin(object):
+ """This can be used in case previous values/tokens get missing
+ from the vocabulary and you still need to display/keep the values"""
+
+ def getTerm(self, value):
+ try:
+ return self.terms.getTerm(value)
+ except LookupError:
+ return self.makeMissingTerm(value)
+
+ def makeToken(self, value):
+ return unicode(value).encode('utf8').encode('base64').strip()
+
+ def makeMissingTerm(self, value, token=None):
+ """Return a term that should be displayed for the missing token or
+ raise LookupError if it's really invalid"""
+ if token is None:
+ token = self.makeToken(value)
+ return vocabulary.SimpleTerm(value, token,
+ title=_(u'Missing: ${value}', mapping=dict(value=unicode(value))))
+
+ def getTermByToken(self, token):
+ try:
+ return self.terms.getTermByToken(token)
+ except LookupError:
+ if (interfaces.IContextAware.providedBy(self.widget) and
+ not self.widget.ignoreContext):
+ value = zope.component.getMultiAdapter(
+ (self.widget.context, self.widget.field),
+ interfaces.IDataManager).query()
+ return self.makeMissingTerm(value, token=token)
+
+ raise LookupError(token)
+
+
+class MissingChoiceTermsVocabulary(MissingTermsMixin, ChoiceTermsVocabulary):
+ """ITerms adapter for zope.schema.IChoice based implementations using
+ vocabulary with missing terms support"""
+
+ zope.component.adapts(
+ zope.interface.Interface,
+ interfaces.IFormLayer,
+ zope.interface.Interface,
+ interfaces.IForgivingChoice,
+ zope.schema.interfaces.IBaseVocabulary,
+ interfaces.IWidget)
+
+
class ChoiceTermsSource(SourceTerms):
"ITerms adapter for zope.schema.IChoice based implementations using source."
@@ -163,6 +212,7 @@
(False, 'false', self.falseLabel)]]
self.terms = vocabulary.SimpleVocabulary(terms)
+
@zope.interface.implementer(interfaces.ITerms)
@zope.component.adapter(
zope.interface.Interface,
@@ -176,6 +226,7 @@
(context, request, form, field, terms, widget),
interfaces.ITerms)
+
class CollectionTermsVocabulary(Terms):
"""ITerms adapter for zope.schema.ICollection based implementations using
vocabulary."""
@@ -196,6 +247,13 @@
self.widget = widget
self.terms = vocabulary
+
+class MissingCollectionTermsVocabulary(MissingTermsMixin,
+ CollectionTermsVocabulary):
+ """ITerms adapter for zope.schema.ICollection based implementations using
+ vocabulary with missing terms support."""
+
+
class CollectionTermsSource(SourceTerms):
"""ITerms adapter for zope.schema.ICollection based implementations using
source."""
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/term.txt 2012-09-06 09:26:05 UTC (rev 127744)
@@ -134,6 +134,23 @@
>>> [entry.title for entry in terms]
[u'bad', u'okay', u'good']
+Missing terms
+
+Sometimes it happens that a term goes away from the vocabulary, but our
+stored objects still reference that term.
+
+ >>> zope.component.provideAdapter(term.MissingChoiceTermsVocabulary)
+
+ >>> terms = term.ChoiceTerms(
+ ... None, request, None, ratingField, widget)
+ >>> term = terms.getTermByToken('42')
+ >>> term.token
+ '42'
+ >>> term.value
+ '42'
+ >>> term.title
+ u'Missing: ${token}'
+
Bool fields
+++++++++++
Modified: z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py 2012-09-06 09:24:57 UTC (rev 127743)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/tests/test_doc.py 2012-09-06 09:26:05 UTC (rev 127744)
@@ -104,6 +104,12 @@
checker=checker,
),
doctest.DocFileSuite(
+ '../widget-miss.txt',
+ setUp=setUp, tearDown=testing.tearDown,
+ optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
+ checker=checker,
+ ),
+ doctest.DocFileSuite(
'../button.txt',
setUp=setUp, tearDown=testing.tearDown,
optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS,
Added: z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt
===================================================================
--- z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt (rev 0)
+++ z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt 2012-09-06 09:26:05 UTC (rev 127744)
@@ -0,0 +1,766 @@
+=======
+Widgets
+=======
+
+Widgets are small UI components that accept and process the textual user
+input. The only responsibility of a widget is to represent a value to the
+user, allow it to be modified and then return a new value. Good examples of
+widgets include the Qt widgets and HTML widgets. The widget is not responsible
+for converting its value to the desired internal value or validate the
+incoming data. These responsibilities are passed data converters and
+validators, respectively.
+
+There are several problems that can be identified in the original Zope 3 widget
+implementation located at ``zope.app.form``.
+
+(1) Field Dependence -- Widgets are always views of fields. While this might
+ be a correct choice for a high-level API, it is fundamentally wrong. It
+ disallows us to use widgets without defining fields. This also couples
+ certain pieces of information too tightly to the field, especially, value
+ retrieval from and storage to the context, validation and raw data
+ conversion.
+
+(2) Form Dependence -- While widgets do not have to be located within a form,
+ they are usually tightly coupled to it. It is very difficult to use
+ widgets outside the context of a form.
+
+(3) Traversability -- Widgets cannot be traversed, which means that they
+ cannot interact easily using Javascript. This is not a fundamental
+ problem, but simply a lack of the current design to recognize that small
+ UI components must also be traversable and thus have a URI.
+
+(4) Customizability -- A consequence of issue (1) is that widgets are not
+ customizable enough. Implementing real-world projects has shown that
+ widgets often want a very fine-grained ability to customize values. A
+ prime example is the label. Because the label of a widget is retrieved
+ from the field title, it is impossible to provide an alternative label for
+ a widget. While the label could be changed from the form, this would
+ require rewriting the entire form to change a label. Instead, we often
+ endde up writing cusom schemas.
+
+(5) Flexibility -- Oftentimes it is desired to have one widget, but multiple
+ styles of representation. For example, in one scenario the widget uses a
+ plain HTML widget and in another a fancy JavaScript widget is used. The
+ current implementation makes it very hard to provide alternative styles
+ for a widget.
+
+
+Creating and Using Simple Widgets
+---------------------------------
+
+When using the widget API by itself, the simplest way to use it is to just
+instantiate it using the request:
+
+ >>> from z3c.form.testing import TestRequest
+ >>> from z3c.form import widget
+ >>> request = TestRequest()
+ >>> age = widget.Widget(request)
+
+In this case we instantiated a generic widget. A full set of simple
+browser-based widgets can be found in the ``browser/`` package. Since no
+helper components are around to fill the attributes of the widget, we have to
+do it by hand:
+
+ >>> age.name = 'age'
+ >>> age.label = u'Age'
+ >>> age.value = '39'
+
+The most important attributes are the "name" and the "value". The name is used
+to identify the widget within the form. The value is either the value to be
+manipulated or the default value. The value must be provided in the form the
+widget needs it. It is the responsibility of a data converter to convert
+between the widget value and the desired internal value.
+
+Before we can render the widget, we have to register a template for the
+widget. The first step is to define the template:
+
+ >>> import tempfile
+ >>> textWidgetTemplate = tempfile.mktemp('text.pt')
+ >>> open(textWidgetTemplate, 'w').write('''
+ ... <html xmlns="http://www.w3.org/1999/xhtml"
+ ... xmlns:tal="http://xml.zope.org/namespaces/tal"
+ ... tal:omit-tag="">
+ ... <input type="text" name="" value=""
+ ... tal:attributes="name view/name; value view/value;" />
+ ... </html>
+ ... ''')
+
+Next, we have to create a template factory for the widget:
+
+ >>> from z3c.form.widget import WidgetTemplateFactory
+ >>> factory = WidgetTemplateFactory(
+ ... textWidgetTemplate, widget=widget.Widget)
+
+The first argument, which is also required, is the path to the template
+file. An optional ``content_type`` keyword argument allows the developer to
+specify the output content type, usually "text/html". Then there are five
+keyword arguments that specify the discriminators of the template:
+
+* ``context`` -- This is the context in which the widget is displayed. In a
+ simple widget like the one we have now, the context is ``None``.
+
+* ``request`` -- This discriminator allows you to specify the type of request
+ for which the widget will be available. In our case this would be a browser
+ request. Note that browser requests can be further broken into layer, so you
+ could also specify a layer interface here.
+
+* ``view`` -- This is the view from which the widget is used. The simple
+ widget at hand, does not have a view associated with it though.
+
+* ``field`` -- This is the field for which the widget provides a
+ representation. Again, this simple widget does not use a field, so it is
+ ``None``.
+
+* ``widget`` -- This is the widget itself. With this discriminator you can
+ specify for which type of widget you are providing a template.
+
+We can now register the template factory. The name of the factory is the mode
+of the widget. By default, there are two widget modes: "input" and
+"display". However, since the mode is just a string, one can develop other
+kinds of modes as needed for a project. The default mode is "input":
+
+ >>> from z3c.form import interfaces
+ >>> age.mode is interfaces.INPUT_MODE
+ True
+
+ >>> import zope.component
+ >>> zope.component.provideAdapter(factory, name=interfaces.INPUT_MODE)
+
+Once everything is set up, the widget is updated and then rendered:
+
+ >>> age.update()
+ >>> print age.render()
+ <input type="text" name="age" value="39" />
+
+If a value is found in the request, it takes precedence, since the user
+entered the value:
+
+ >>> age.request = TestRequest(form={'age': '25'})
+ >>> age.update()
+ >>> print age.render()
+ <input type="text" name="age" value="25" />
+
+However, there is an option to turn off all request data:
+
+ >>> age.value = '39'
+ >>> age.ignoreRequest = True
+ >>> age.update()
+ >>> print age.render()
+ <input type="text" name="age" value="39" />
+
+
+Creating and Using Field Widgets
+--------------------------------
+
+An extended form of the widget allows fields to control several of the
+widget's properties. Let's create a field first:
+
+ >>> ageField = zope.schema.Int(
+ ... __name__ = 'age',
+ ... title = u'Age',
+ ... min = 0,
+ ... max = 130)
+
+We can now use our simple widget and create a field widget from it:
+
+ >>> ageWidget = widget.FieldWidget(ageField, age)
+
+Such a widget provides ``IFieldWidget``:
+
+ >>> interfaces.IFieldWidget.providedBy(ageWidget)
+ True
+
+Of course, this is more commonly done using an adapter. Commonly those
+adapters look like this:
+
+ >>> @zope.component.adapter(zope.schema.Int, TestRequest)
+ ... @zope.interface.implementer(interfaces.IFieldWidget)
+ ... def IntWidget(field, request):
+ ... return widget.FieldWidget(field, widget.Widget(request))
+
+ >>> zope.component.provideAdapter(IntWidget)
+ >>> ageWidget = zope.component.getMultiAdapter((ageField, request),
+ ... interfaces.IFieldWidget)
+
+Now we just have to update and render the widget:
+
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" />
+
+There is no initial value for the widget, since there is no value in the
+request and the field does not provide a default. Let's now give our field a
+default value and see what happens:
+
+ >>> ageField.default = 30
+ >>> ageWidget.update()
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Could not adapt', <Widget 'age'>,
+ <InterfaceClass z3c.form.interfaces.IDataConverter>)
+
+In order for the widget to be able to take the field's default value and use
+it to provide an initial value the widget, we need to provide a data converter
+that defines how to convert from the field value to the widget value.
+
+ >>> from z3c.form import converter
+ >>> zope.component.provideAdapter(converter.FieldWidgetDataConverter)
+ >>> zope.component.provideAdapter(converter.FieldDataConverter)
+
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="30" />
+
+Again, the request value is honored above everything else:
+
+ >>> ageWidget.request = TestRequest(form={'age': '25'})
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="25" />
+
+
+Creating and Using Context Widgets
+----------------------------------
+
+When widgets represent an attribute value of an object, then this object must
+be set as the context of the widget:
+
+ >>> class Person(object):
+ ... age = 45
+ >>> person = Person()
+
+ >>> ageWidget.context = person
+ >>> zope.interface.alsoProvides(ageWidget, interfaces.IContextAware)
+
+The result is that the context value takes over precendence over the default
+value:
+
+ >>> ageWidget.request = TestRequest()
+ >>> ageWidget.update()
+ Traceback (most recent call last):
+ ...
+ ComponentLookupError: ((...), <InterfaceClass ...IDataManager>, u'')
+
+This call fails because the widget does not know how to extract the value from
+the context. Registering a data manager for the widget does the trick:
+
+ >>> from z3c.form import datamanager
+ >>> zope.component.provideAdapter(datamanager.AttributeField)
+
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="45" />
+
+If the context value is unknown (None), the default value kicks in.
+
+ >>> person.age = None
+
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="30" />
+
+Unless the widget is explicitely asked to not to show defaults.
+This is handy for EditForms.
+
+ >>> ageWidget.showDefault = False
+
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="" />
+
+ >>> ageWidget.showDefault = True
+ >>> person.age = 45
+
+The context can be explicitely ignored, making the widget display the default
+value again:
+
+ >>> ageWidget.ignoreContext = True
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="30" />
+
+Again, the request value is honored above everything else:
+
+ >>> ageWidget.request = TestRequest(form={'age': '25'})
+ >>> ageWidget.ignoreContext = False
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="25" />
+
+But what happens if the object we are working on is security proxied? In
+particular, what happens, if the access to the attribute is denied. To see
+what happens, we have to create a proxied person:
+
+ >>> from zope.security import checker
+ >>> PersonChecker = checker.Checker({'age': 'Access'}, {'age': 'Edit'})
+
+ >>> ageWidget.request = TestRequest()
+ >>> ageWidget.context = checker.ProxyFactory(Person(), PersonChecker)
+
+After changing the security policy, ...
+
+ >>> from zope.security import management
+ >>> from z3c.form import testing
+ >>> management.endInteraction()
+ >>> newPolicy = testing.SimpleSecurityPolicy()
+ >>> oldPolicy = management.setSecurityPolicy(newPolicy)
+ >>> management.newInteraction()
+
+it is not possible anymore to update the widget:
+
+ >>> ageWidget.update()
+ Traceback (most recent call last):
+ ...
+ Unauthorized: (<Person object at ...>, 'age', 'Access')
+
+If no security declaration has been made at all, we get a
+``ForbiddenAttribute`` error:
+
+ >>> ageWidget.context = checker.ProxyFactory(Person(), checker.Checker({}))
+ >>> ageWidget.update()
+ Traceback (most recent call last):
+ ...
+ ForbiddenAttribute: ('age', <Person object at ...>)
+
+Let's clean up the setup:
+
+ >>> management.endInteraction()
+ >>> newPolicy = management.setSecurityPolicy(oldPolicy)
+ >>> management.newInteraction()
+
+ >>> ageWidget.context = Person()
+
+
+Dynamically Changing Attribute Values
+-------------------------------------
+
+Once widgets are used within a framework, it is very tedious to write Python
+code to adjust certain attributes, even though hooks exist. The easiest way to
+change those attribute values is actually to provide an adapter that provides
+the custom value.
+
+We can create a custom label for the age widget:
+
+ >>> AgeLabel = widget.StaticWidgetAttribute(
+ ... u'Current Age',
+ ... context=None, request=None, view=None, field=ageField, widget=None)
+
+Clearly, this code does not require us to touch the orginal form and widget
+code, given that we have enough control over the selection. In the example
+above, all the selection discriminators are listed for demonstration
+purposes. Of course, the label in this case can be created as follows:
+
+ >>> AgeLabel = widget.StaticWidgetAttribute(u'Current Age', field=ageField)
+
+Much better, isn't it? Initially the label is the title of the field:
+
+ >>> ageWidget.label
+ u'Age'
+
+Let's now simply register the label as a named adapter; the name is the name
+of the attribute to change:
+
+ >>> zope.component.provideAdapter(AgeLabel, name='label')
+
+Asking the widget for the label now will return the newly registered label:
+
+ >>> ageWidget.update()
+ >>> ageWidget.label
+ u'Current Age'
+
+Of course, simply setting the label or changing the label extraction via a
+sub-class are other options you might want to consider. Furthermore, you
+could also create a computed attribute value or implement your own component.
+
+Overriding other attributes, such as ``required``, is done in the same
+way. If any widget provides new attributes, they are also overridable this
+way. For example, the selection widget defines a label for the option that no
+value was selected. We often want to override this, because the German
+translation sucks or the wording is often too generic. Widget implementation
+should add names of overridable attributes to their "_adapterValueAttributes"
+internal attribute.
+
+Let's try to override the ``required`` attribute. By default the widget is required,
+because the field is required as well:
+
+ >>> ageWidget.required
+ True
+
+Let's provide a static widget attribute adapter with name "required":
+
+ >>> AgeNotRequired = widget.StaticWidgetAttribute(False, field=ageField)
+ >>> zope.component.provideAdapter(AgeNotRequired, name="required")
+
+Now, let's check if it works:
+
+ >>> ageWidget.update()
+ >>> ageWidget.required
+ False
+
+Overriding the default value is somewhat special due to the complexity of
+obtaining the value. So let's register one now:
+
+ >>> AgeDefault = widget.StaticWidgetAttribute(50, field=ageField)
+ >>> zope.component.provideAdapter(AgeDefault, name="default")
+
+Let's now instantiate, update and render the widget to see the default value:
+
+ >>> ageWidget = zope.component.getMultiAdapter((ageField, request),
+ ... interfaces.IFieldWidget)
+ >>> ageWidget.update()
+ >>> print ageWidget.render()
+ <input type="text" name="age" value="50" />
+
+
+Sequence Widget
+---------------
+
+A common use case in user interfaces is to ask the user to select one or more
+items from a set of options/choices. The ``widget`` module provides a basic
+widget implementation to support this use case.
+
+The options available for selections are known as terms. Initially, there are
+no terms:
+
+ >>> request = TestRequest()
+ >>> seqWidget = widget.SequenceWidget(request)
+ >>> seqWidget.name = 'seq'
+
+ >>> seqWidget.terms is None
+ True
+
+There are two ways terms can be added, either manually or via an
+adapter. Those term objects must provide ``ITerms``. There is no simple
+default implementation, so we have to provide one ourselves:
+
+ >>> from zope.schema import vocabulary
+ >>> class Terms(vocabulary.SimpleVocabulary):
+ ... zope.interface.implements(interfaces.ITerms)
+ ... def getValue(self, token):
+ ... return self.getTermByToken(token).value
+
+ >>> terms = Terms(
+ ... [Terms.createTerm(1, 'v1', u'Value 1'),
+ ... Terms.createTerm(2, 'v2', u'Value 2'),
+ ... Terms.createTerm(3, 'v3', u'Value 3')])
+ >>> seqWidget.terms = terms
+
+Once the ``terms`` attribute is set, updating the widgets does not change the
+terms:
+
+ >>> seqWidget.update()
+ >>> [term.value for term in seqWidget.terms]
+ [1, 2, 3]
+
+The value of a sequence widget is a tuple/list of term tokens. When extracting
+values from the request, the values must be valid tokens, otherwise the
+default value is returned:
+
+ >>> seqWidget.request = TestRequest(form={'seq': ['v1']})
+ >>> seqWidget.extract()
+ ['v1']
+
+ >>> seqWidget.request = TestRequest(form={'seq': ['v4']})
+ >>> seqWidget.extract()
+ <NO_VALUE>
+
+ >>> seqWidget.request = TestRequest(form={'seq-empty-marker': '1'})
+ >>> seqWidget.extract()
+ []
+
+Note that we also support single values being returned outside a sequence. The
+extracted value is then wrapped by a tuple. This feature is useful when
+integrating with third-party client frameworks that do not know about the Zope
+naming conventions.
+
+ >>> seqWidget.request = TestRequest(form={'seq': 'v1'})
+ >>> seqWidget.extract()
+ ('v1',)
+
+If the no-value token has been selected, it is returned without further
+verification:
+
+ >>> seqWidget.request = TestRequest(form={'seq': [seqWidget.noValueToken]})
+ >>> seqWidget.extract()
+ ['--NOVALUE--']
+
+Since the value of the widget is a tuple of tokens, when displaying the
+values, they have to be converted to the title of the term:
+
+ >>> seqWidget.value = ('v1', 'v2')
+ >>> seqWidget.displayValue
+ [u'Value 1', u'Value 2']
+
+When input forms are directly switched to display forms within the same
+request, it can happen that the value contains the "--NOVALUE--" token
+entry. This entry should be silently ignored:
+
+ >>> seqWidget.value = (seqWidget.noValueToken,)
+ >>> seqWidget.displayValue
+ []
+
+To demonstrate how the terms is automatically chosen by a widget, we should
+instantiate a field widget. Let's do this with a choice field:
+
+ >>> seqField = zope.schema.Choice(
+ ... title=u'Sequence Field',
+ ... vocabulary=terms)
+
+Let's now create the field widget:
+
+ >>> seqWidget = widget.FieldWidget(seqField, widget.SequenceWidget(request))
+ >>> seqWidget.terms
+
+The terms should be available as soon as the widget is updated:
+
+ >>> seqWidget.update()
+ Traceback (most recent call last):
+ ...
+ ComponentLookupError: ((...), <InterfaceClass ...ITerms>, u'')
+
+This failed, because we did not register an adapter for the terms yet. After
+the adapter is registered, everything should work as expected:
+
+ >>> from z3c.form import term
+ >>> zope.component.provideAdapter(term.ChoiceTermsVocabulary)
+ >>> zope.component.provideAdapter(term.ChoiceTerms)
+
+ >>> seqWidget.update()
+ >>> seqWidget.terms
+ <z3c.form.term.ChoiceTermsVocabulary object at ...>
+
+So that's it. Everything else is the same from then on.
+
+
+Multi Widget
+------------
+
+A common use case in user interfaces is to ask the user to define one or more
+items. The ``widget`` module provides a basic widget implementation to support
+this use case.
+
+The `MultiWidget` allows to store none, one or more values for a sequence
+field. Don't get confused by the term sequence. The sequence used in
+`SequenceWidget` means that the widget can choose from a sequence of values
+which is really a collection. The `MultiWidget` can collect values to build
+and store a sequence of values like those used in `ITuple` or `IList` field.
+
+ >>> request = TestRequest()
+ >>> multiWidget = widget.MultiWidget(request)
+ >>> multiWidget.name = 'multi.name'
+ >>> multiWidget.id = 'multi-id'
+
+ >>> multiWidget.value
+ []
+
+Let's define a field for our multi widget:
+
+ >>> multiField = zope.schema.List(
+ ... value_type=zope.schema.Int(default=42))
+ >>> multiWidget.field = multiField
+
+The value of a multi widget is always list. When extracting values from the
+request, the values must be a list of valid values based on the value_type
+field used from the used sequence field. The widget also uses a counter which
+is required for processing the input from a request. The counter is a marker
+for build the right amount of enumerated widgets.
+
+If we provide no request we will get no value:
+
+ >>> multiWidget.extract()
+ <NO_VALUE>
+
+If we provide an empty counter we will get an empty list.
+This is accordance with Widget.extract(), where a missing request value
+is <NO_VALUE> and an empty ('') request value is ''.
+
+ >>> multiWidget.request = TestRequest(form={'multi.name.count':'0'})
+ >>> multiWidget.extract()
+ []
+
+If we provide real values within the request, we will get it back:
+
+ >>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
+ ... 'multi.name.0':u'42',
+ ... 'multi.name.1':u'43'})
+ >>> multiWidget.extract()
+ [u'42', u'43']
+
+If we provide a bad value we will get the bad value within the extract method.
+Our widget update process will validate this bad value later:
+
+ >>> multiWidget.request = TestRequest(form={'multi.name.count':'1',
+ ... 'multi.name.0':u'bad'})
+ >>> multiWidget.extract()
+ [u'bad']
+
+Storing a widget value forces to update the (sub) widgets. This forces also to
+validate the (sub) widget values. To show this we need to register a
+validator:
+
+ >>> from z3c.form.validator import SimpleFieldValidator
+ >>> zope.component.provideAdapter(SimpleFieldValidator)
+
+Since the value of the widget is a list of (widget) value items, when
+displaying the values, they can be used as they are:
+
+ >>> multiWidget.request = TestRequest(form={'multi.name.count':'2',
+ ... 'multi.name.0':u'42',
+ ... 'multi.name.1':u'43'})
+ >>> multiWidget.value = multiWidget.extract()
+ >>> multiWidget.value
+ [u'42', u'43']
+
+Each widget normally gets first processed by it's update method call after
+initialization. This update call forces to call extract, which first will get
+the right amount of (sub) widgets by the given counter value. Based on that
+counter value the right amount of widgets will get created. Each widget will
+return it's own value and this collected values get returned by the extract
+method. The multi widget update method will then store this values if any given
+as multi widget value argument. If extract doesn't return a value the multi
+widget update method will use it's default value. If we store a given value
+from the extract as multi widget value, this will force to setup the multi
+widget widgets based on the given values and apply the right value for them.
+After that the multi widget is ready for rendering. The good thing about that
+pattern is that it is possible to set a value before or after the update method
+is called. At any time if we change the multi widget value the (sub) widgets
+get updated within the new relevant value.
+
+ >>> multiRequest = TestRequest(form={'multi.name.count':'2',
+ ... 'multi.name.0':u'42',
+ ... 'multi.name.1':u'43'})
+
+ >>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(
+ ... multiRequest))
+ >>> multiWidget.name = 'multi.name'
+ >>> multiWidget.value
+ []
+
+ >>> multiWidget.update()
+
+ >>> multiWidget.widgets[0].value
+ u'42'
+
+ >>> multiWidget.widgets[1].value
+ u'43'
+
+ >>> multiWidget.value
+ [u'42', u'43']
+
+MultiWidget also declares the ``allowAdding`` and ``allowRemoving``
+attributes that can be used in browser presentation to control add/remove
+button availability. To ease working with common cases, the
+``updateAllowAddRemove`` method provided that will set those attributes
+in respect to field's min_length and max_length, if the field provides
+zope.schema.interfaces.IMinMaxLen interface.
+
+Let's define a field with min and max length constraints and create
+a widget for it.
+
+ >>> multiField = zope.schema.List(
+ ... value_type=zope.schema.Int(),
+ ... min_length=2,
+ ... max_length=5)
+
+ >>> request = TestRequest()
+ >>> multiWidget = widget.FieldWidget(multiField, widget.MultiWidget(request))
+
+Lets ensure that the minimum number of widgets are created.
+
+ >>> multiWidget.update()
+ >>> len(multiWidget.widgets)
+ 2
+
+Now, let's check if the function will do the right thing depending on
+the value:
+
+No value:
+
+ >>> multiWidget.updateAllowAddRemove()
+ >>> multiWidget.allowAdding, multiWidget.allowRemoving
+ (True, False)
+
+Minimum length:
+
+ >>> multiWidget.value = [u'3', u'5']
+ >>> multiWidget.updateAllowAddRemove()
+ >>> multiWidget.allowAdding, multiWidget.allowRemoving
+ (True, False)
+
+Some allowed length:
+
+ >>> multiWidget.value = [u'3', u'5', u'8', u'6']
+ >>> multiWidget.updateAllowAddRemove()
+ >>> multiWidget.allowAdding, multiWidget.allowRemoving
+ (True, True)
+
+Maximum length:
+
+ >>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42']
+ >>> multiWidget.updateAllowAddRemove()
+ >>> multiWidget.allowAdding, multiWidget.allowRemoving
+ (False, True)
+
+Over maximum length:
+
+ >>> multiWidget.value = [u'3', u'5', u'8', u'6', u'42', u'45']
+ >>> multiWidget.updateAllowAddRemove()
+ >>> multiWidget.allowAdding, multiWidget.allowRemoving
+ (False, True)
+
+I know a guy who once switched widget mode in the middle. All simple widgets
+are easy to hack, but multiWidget needs to update all subwidgets:
+
+ >>> [w.mode for w in multiWidget.widgets]
+ ['input', 'input', 'input', 'input', 'input', 'input']
+
+Switch the multiWidget mode:
+
+ >>> multiWidget.mode = interfaces.DISPLAY_MODE
+
+Yes, all subwidgets switch mode:
+
+ >>> [w.mode for w in multiWidget.widgets]
+ ['display', 'display', 'display', 'display', 'display', 'display']
+
+Widget Events
+-------------
+
+Widget-system interaction can be very rich and wants to be extended in
+unexpected ways. Thus there exists a generic widget event that can be used by
+other code.
+
+ >>> event = widget.WidgetEvent(ageWidget)
+ >>> event
+ <WidgetEvent <Widget 'age'>>
+
+These events provide the ``IWidgetEvent`` interface:
+
+ >>> interfaces.IWidgetEvent.providedBy(event)
+ True
+
+There exists a special event that can be send out after a widget has been
+updated, ...
+
+ >>> afterUpdate = widget.AfterWidgetUpdateEvent(ageWidget)
+ >>> afterUpdate
+ <AfterWidgetUpdateEvent <Widget 'age'>>
+
+which provides another special interface:
+
+ >>> interfaces.IAfterWidgetUpdateEvent.providedBy(afterUpdate)
+ True
+
+This event should be used by widget-managing components and is not created and
+sent out internally by the widget's ``update()`` method. The event was
+designed to provide an additional hook between updating the widget and
+rendering it.
+
+
+Cleanup
+-------
+
+Let's not leave temporary files lying around
+
+ >>> import os
+ >>> os.remove(textWidgetTemplate)
Property changes on: z3c.form/branches/adamg-missing-terms/src/z3c/form/widget-miss.txt
___________________________________________________________________
Added: svn:keywords
+ Date Author Id Revision
Added: svn:eol-style
+ native
More information about the checkins
mailing list