[Zope3-checkins] CVS: Zope3/src/zope/app/browser/form - configure.zcml:1.18 editview.py:1.28 widget.py:1.38
Richard Jones
richard@commonground.com.au
Sun, 13 Jul 2003 02:47:50 -0400
Update of /cvs-repository/Zope3/src/zope/app/browser/form
In directory cvs.zope.org:/tmp/cvs-serv31581/src/zope/app/browser/form
Modified Files:
configure.zcml editview.py widget.py
Log Message:
Implemented Object field types, and ObjectWidget to go with it.
Object fields have Fields on them, and when included in a form view, their
sub-fields fully participate in the form generate and editing. You'd use
this kinda thing for generating an Address field.
The change required the removal of the apply_update method on EditView. It is
replaced by the calling of applyChanges on each widget (facilitated by the
applyWidgetsChanges function of zope.app.form.utility).
To make this sane, the ObjectWidget must be used via CustomWidget so we can
indicate which factory is to be called to generate the container for the
Object's fields. ObjectWidget and SequenceWidget also allow overriding of
the widgets used to render their sub-fields.
If this is all confusing, then see the new widgets.txt help file in the
zope/app/browser/form/ directory, and it may or may not help :)
=== Zope3/src/zope/app/browser/form/configure.zcml 1.17 => 1.18 ===
--- Zope3/src/zope/app/browser/form/configure.zcml:1.17 Fri Jul 11 21:28:59 2003
+++ Zope3/src/zope/app/browser/form/configure.zcml Sun Jul 13 02:47:16 2003
@@ -97,6 +97,14 @@
<browser:page
permission="zope.Public"
allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
+ for="zope.schema.interfaces.IObject"
+ name="edit"
+ class="zope.app.browser.form.widget.ObjectWidget"
+ />
+
+ <browser:page
+ permission="zope.Public"
+ allowed_interface="zope.app.interfaces.browser.form.IBrowserWidget"
for="zope.schema.interfaces.IPassword"
name="edit"
class="zope.app.browser.form.widget.PasswordWidget"
=== Zope3/src/zope/app/browser/form/editview.py 1.27 => 1.28 ===
--- Zope3/src/zope/app/browser/form/editview.py:1.27 Wed Jul 2 18:10:37 2003
+++ Zope3/src/zope/app/browser/form/editview.py Sun Jul 13 02:47:16 2003
@@ -23,6 +23,8 @@
from zope.schema.interfaces import ValidationError
from zope.schema import getFieldNamesInOrder
+from zope.interface import classProvides, implements
+
from zope.configuration.action import Action
from zope.app.context import ContextWrapper
from zope.publisher.interfaces.browser import IBrowserPresentation
@@ -33,7 +35,7 @@
from zope.app.interfaces.form import WidgetsError
from zope.app.component.metaconfigure import resolveInterface
-from zope.app.form.utility import setUpEditWidgets, getWidgetsData
+from zope.app.form.utility import setUpEditWidgets, applyWidgetsChanges
from zope.app.browser.form.submit import Update
from zope.app.event import publish
from zope.app.event.objectevent import ObjectModifiedEvent
@@ -79,57 +81,6 @@
return [getattr(self, name+'_widget')
for name in self.fieldNames]
- def apply_update(self, data):
- """Apply data updates
-
- Return true if data were unchanged and false otherwise.
- This sounds backwards, but it allows lazy implementations to
- avoid tracking changes.
- """
-
- content = self.adapted
-
- errors = []
- unchanged = True
-
- for name in data:
- field = self.schema[name]
- try:
- newvalue = data[name]
-
- # We want to see if the data changes. Unfortunately,
- # we don't know enough to know that we won't get some
- # strange error, so we'll carefully ignore errors and
- # assume we should update the data if we can't be sure
- # it's the same.
-
- change = True
-
- # Use self as a marker
- change = field.query(content, self) != newvalue
-
- if change:
- field.set(content, data[name])
- unchanged = False
-
- except ValidationError, v:
- errors.append(v)
-
- if not unchanged:
- # XXX need better error handling here. We should catch
- # and display errors for which there are views.
- self.changed()
-
- if errors:
- raise WidgetsError(*errors)
-
- # We should not generate events whan an adapter is used. That's the
- # adapter's job.
- if not unchanged and self.context is self.adapted:
- publish(content, ObjectModifiedEvent(content))
-
- return unchanged
-
def changed(self):
# This method is overridden to execute logic *after* changes
# have been made.
@@ -143,24 +94,20 @@
status = ''
+ content = self.adapted
+
if Update in self.request:
- unchanged = True
+ changed = False
try:
- data = getWidgetsData(self, self.schema,
- strict=False,
- set_missing=True,
- names=self.fieldNames,
- exclude_readonly=True)
- unchanged = self.apply_update(data)
+ changed = applyWidgetsChanges(self, content, self.schema,
+ names=self.fieldNames, exclude_readonly=True)
except WidgetsError, errors:
self.errors = errors
status = u"An error occured."
else:
setUpEditWidgets(self, self.schema, force=1,
names=self.fieldNames)
-
- if not unchanged:
-
+ if changed:
status = "Updated %s" % datetime.utcnow()
self.update_status = status
=== Zope3/src/zope/app/browser/form/widget.py 1.37 => 1.38 ===
--- Zope3/src/zope/app/browser/form/widget.py:1.37 Fri Jul 11 22:47:07 2003
+++ Zope3/src/zope/app/browser/form/widget.py Sun Jul 13 02:47:16 2003
@@ -20,17 +20,23 @@
import re
import warnings
from zope.app import zapi
-from zope.interface import implements
+
+from zope.component import getService
+from zope.component.interfaces import IViewFactory
+
+from zope.interface import implements, directlyProvides
from zope.proxy import removeAllProxies
from zope.publisher.browser import BrowserView
from zope.app.interfaces.browser.form import IBrowserWidget
from zope.app.form.widget import Widget
+from zope.app.form.utility import setUpEditWidgets, applyWidgetsChanges
+from zope.app.form.utility import setUpWidget
from zope.app.interfaces.form import ConversionError, WidgetInputError
from zope.app.interfaces.form import MissingInputError
from zope.app.datetimeutils import parseDatetimetz
from zope.app.datetimeutils import DateTimeError
+from zope.schema import getFieldNamesInOrder
from zope.schema.interfaces import ValidationError
-from zope.component import getService
ListTypes = list, tuple
@@ -109,7 +115,6 @@
value = self._convert(value)
if value is not None and not optional:
-
try:
field.validate(value)
except ValidationError, v:
@@ -118,6 +123,17 @@
return value
+ def validate(self):
+ self.getData()
+
+ def applyChanges(self, content):
+ field = self.context
+ value = self.getData()
+ change = field.query(content, self) != value
+ if change:
+ field.set(content, value)
+ return change
+
def _convert(self, value):
if value == self._missing:
return None
@@ -131,7 +147,7 @@
def _showData(self):
if self._data is None:
if self.haveData():
- data = self.getData(1)
+ data = self.getData(optional=1)
else:
data = self._getDefault()
else:
@@ -937,14 +953,19 @@
class SequenceWidget(BrowserWidget):
"""A sequence of fields.
- Contains a sequence of *Widgets which have a numeric __name__ which
- represents their position in the sequence.
+ subwidget - Optional CustomWidget used to generate widgets for the
+ items in the sequence
"""
_type = tuple
_stored = () # pre-existing sequence items (from setData)
_sequence = () # current list of sequence items (existing & request)
_sequence_generated = False
+ def __init__(self, context, request, subwidget=None):
+ super(SequenceWidget, self).__init__(context, request)
+
+ self.subwidget = None
+
def __call__(self):
"""Render the widget
"""
@@ -958,9 +979,6 @@
render = []
r = render.append
- # prefix for form elements
- prefix = self._prefix + self.context.__name__
-
# length of sequence info
sequence = list(self._sequence)
num_items = len(sequence)
@@ -975,14 +993,12 @@
# generate each widget from items in the _sequence - adding a
# "remove" button for each one
- field = self.context.value_type
for i in range(num_items):
value = sequence[i]
r('<tr><td>')
if num_items > min_length:
- r('<input type="checkbox" name="%s.remove_%d">'%(prefix, i))
- widget = zapi.getView(field, 'edit', self.request, self.context)
- widget.setPrefix('%s.%d.'%(prefix, i))
+ r('<input type="checkbox" name="%s.remove_%d">'%(self.name, i))
+ widget = self._getWidget(i)
widget.setData(value)
r(widget()+'</td></tr>')
@@ -991,17 +1007,25 @@
if render and num_items > min_length:
s += '<input type="submit" value="Remove Selected Items">'
if max_length is None or num_items < max_length:
- s += '<input type="submit" name="%s.add" value="Add %s">'%(prefix,
- field.title or field.__name__)
+ field = self.context.value_type
+ s += '<input type="submit" name="%s.add" value="Add %s">'%(
+ self.name, field.title or field.__name__)
if s:
r('<tr><td>%s</td></tr>'%s)
return '<table border="0">' + ''.join(render) + '</table>'
+ def _getWidget(self, i):
+ field = self.context.value_type
+ if self.subwidget:
+ widget = self.subwidget(field, self.request)
+ else:
+ widget = zapi.getView(field, 'edit', self.request, self.context)
+ widget.setPrefix('%s.%d.'%(self.name, i))
+ return widget
def hidden(self):
''' Render the list as hidden fields '''
- prefix = self._prefix + self.context.__name__
# length of sequence info
sequence = list(self._sequence)
num_items = len(sequence)
@@ -1015,12 +1039,10 @@
num_items = len(sequence)
# generate hidden fields for each value
- field = self.context.value_type
s = ''
for i in range(num_items):
value = sequence[i]
- widget = zapi.getView(field, 'edit', self.request, self.context)
- widget.setPrefix('%s.%d.'%(prefix, i))
+ widget = self._getWidget(i)
widget.setData(value)
s += widget.hidden()
return s
@@ -1037,7 +1059,6 @@
A WidgetInputError is returned in the case of one or more
errors encountered, inputting, converting, or validating the data.
"""
- # XXX enforce required
if not self._sequence_generated:
self._generateSequenceFromRequest()
# validate the input values
@@ -1045,6 +1066,15 @@
self.context.value_type.validate(value)
return self._type(self._sequence)
+ # XXX applyChanges isn't reporting "change" correctly
+ def applyChanges(self, content):
+ field = self.context
+ value = self.getData()
+ change = field.query(content, self) != value
+ if change:
+ field.set(content, value)
+ return change
+
def haveData(self):
"""Is there input data for the field
@@ -1069,8 +1099,7 @@
This is kinda expensive, so we only do it once.
"""
- prefix = self._prefix + self.context.__name__
- len_prefix = len(prefix)
+ len_prefix = len(self.name)
adding = False
removing = []
subprefix = re.compile(r'(\d+)\.(.+)')
@@ -1087,31 +1116,26 @@
found[i] = entry
# now look through the request for interesting values
- have_request_data = False
for k, v in self.request.items():
- if not k.startswith(prefix):
+ if not k.startswith(self.name):
continue
s = k[len_prefix+1:] # skip the '.'
if s == 'add':
# append a new blank field to the sequence
adding = True
- have_request_data = True
elif s.startswith('remove_'):
# remove the index indicated
removing.append(int(s[7:]))
- have_request_data = True
else:
m = subprefix.match(s)
if m is None:
continue
# key refers to a sub field
i = int(m.group(1))
- have_request_data = True
# find a widget for the sub-field and use that to parse the
# request data
- widget = zapi.getView(field, 'edit', self.request, self.context)
- widget.setPrefix('%s.%d.'%(prefix, i))
+ widget = self._getWidget(i)
value = widget.getData()
found[i] = value
@@ -1136,6 +1160,124 @@
class ListSequenceWidget(SequenceWidget):
_type = list
+class ObjectWidget(BrowserWidget):
+ """A widget over an Interface that contains Fields.
+
+ "factory" - factory used to create content that this widget (field)
+ represents
+ *_widget - Optional CustomWidgets used to generate widgets for the
+ fields in this widget
+ """
+ _object = None # the object value (from setData & request)
+ _request_parsed = False
+
+ def __init__(self, context, request, factory, **kw):
+ super(ObjectWidget, self).__init__(context, request)
+
+ # factory used to create content that this widget (field)
+ # represents
+ self.factory = factory
+
+ # handle foo_widget specs being passed in
+ self.names = getFieldNamesInOrder(self.context.schema)
+ for k, v in kw.items():
+ if k.endswith('_widget'):
+ setattr(self, k, v)
+
+ # set up my subwidgets
+ self._setUpEditWidgets()
+
+ def setPrefix(self, prefix):
+ super(ObjectWidget, self).setPrefix(prefix)
+ self._setUpEditWidgets()
+
+ def _setUpEditWidgets(self):
+ # subwidgets need a new name
+ setUpEditWidgets(self, self.context.schema, content=None,
+ prefix=self.name, names=self.names, context=self.context)
+
+ def __call__(self):
+ """Render the widget
+ """
+ render = []
+ r = render.append
+
+ # XXX see if there's some widget layout already
+
+ # generate each widget from fields in the schema
+ field = self.context
+ title = field.title or field.__name__
+ r('<fieldset><legend>%s</legend>'%title)
+ for name, widget in self.getSubWidgets():
+ r(widget.row())
+ r('</fieldset>')
+
+ return '\n'.join(render)
+
+ def getSubWidgets(self):
+ l = []
+ for name in self.names:
+ l.append((name, getattr(self, '%s_widget'%name)))
+ return l
+
+ def hidden(self):
+ ''' Render the list as hidden fields '''
+ for name, widget in self.getSubWidgets():
+ s += widget.hidden()
+ return s
+
+ def getData(self):
+ """Return converted and validated widget data.
+
+ The value for this field will be represented as an ObjectStorage
+ instance which holds the subfield values as attributes. It will
+ need to be converted by higher-level code into some more useful
+ object (note that the default EditView calls applyChanges, which
+ does this).
+ """
+ content = self.factory()
+ for name, widget in self.getSubWidgets():
+ setattr(content, name, widget.getData())
+ return content
+
+ def applyChanges(self, content):
+ field = self.context
+
+ # create our new object value
+ value = field.query(content, None)
+ if value is None:
+ value = self.factory()
+
+ # apply sub changes, see if there *are* any changes
+ changes = applyWidgetsChanges(self, value, field.schema,
+ names=self.names, exclude_readonly=True)
+
+ # if there's changes, then store the new value on the content
+ if changes:
+ field.set(content, value)
+
+ return changes
+
+ def haveData(self):
+ """Is there input data for the field
+
+ Return True if there is data and False otherwise.
+ """
+ for name, widget in self.getSubWidgets():
+ if widget.haveData():
+ return True
+ return False
+
+ def setData(self, value):
+ """Set the default data for the widget.
+
+ The given value should be used even if the user has entered
+ data.
+ """
+ # re-call setupwidgets with the content
+ self._setUpEditWidgets()
+ for name, widget in self.getSubWidgets():
+ widget.setData(getattr(value, name, None))
# XXX Note, some HTML quoting is needed in renderTag and renderElement.