============ ObjectWidget ============ The `ObjectWidget` widget is about rendering a schema's fields as a single widget. There are way too many preconditions to exercise the `ObjectWidget` as it relies heavily on the from and widget framework. It renders the sub-widgets in a sub-form. In order to not overwhelm you with our set of well-chosen defaults, all the default component registrations have been made prior to doing those examples: >>> from z3c.form import testing >>> testing.setupFormDefaults() >>> import zope.component >>> from z3c.form import datamanager >>> zope.component.provideAdapter(datamanager.DictionaryField) >>> from z3c.form.testing import IMySubObject >>> from z3c.form.testing import IMySecond >>> from z3c.form.testing import MySubObject >>> from z3c.form.testing import MySecond >>> import z3c.form.object >>> zope.component.provideAdapter(z3c.form.object.SubformAdapter) >>> zope.component.provideAdapter(z3c.form.object.ObjectConverter) >>> from z3c.form.error import MultipleErrorViewSnippet >>> zope.component.provideAdapter(MultipleErrorViewSnippet) As for all widgets, the objectwidget must provide the new `IWidget` interface: >>> from zope.interface.verify import verifyClass >>> from z3c.form import interfaces >>> import z3c.form.browser.object >>> verifyClass(interfaces.IWidget, z3c.form.browser.object.ObjectWidget) True The widget can be instantiated only using the request: >>> from z3c.form.testing import TestRequest >>> request = TestRequest() >>> widget = z3c.form.browser.object.ObjectWidget(request) Before rendering the widget, one has to set the name and id of the widget: >>> widget.id = 'widget-id' >>> widget.name = 'widget.name' Also, stand-alone widgets need to ignore the context: >>> widget.ignoreContext = True There is no life for ObjectWidget without a schema to render: >>> widget.update() Traceback (most recent call last): ... ValueError: .field is None, that's a blocking point This schema is specified by the field: >>> from z3c.form.widget import FieldWidget >>> from z3c.form.testing import IMySubObject >>> import zope.schema >>> field = zope.schema.Object( ... __name__='subobject', ... title=u'my object widget', ... schema=IMySubObject) Apply the field nicely with the helper: >>> widget = FieldWidget(field, widget) We also need to register the templates for 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('object_input.pt'), 'text/html'), ... (None, None, None, None, interfaces.IObjectWidget), ... IPageTemplate, name=interfaces.INPUT_MODE) >>> zope.component.provideAdapter( ... WidgetTemplateFactory(getPath('object_display.pt'), 'text/html'), ... (None, None, None, None, interfaces.IObjectWidget), ... IPageTemplate, name=interfaces.DISPLAY_MODE) We can now render the widget: >>> widget.update() >>> print widget.render()
As you see all sort of default values are rendered. Let's provide a more meaningful value: >>> from z3c.form.testing import MySubObject >>> v = MySubObject() >>> v.foofield = 42 >>> v.barfield = 666 >>> v.__marker__ = "ThisMustStayTheSame" >>> widget.ignoreContext = False >>> widget.value = dict(foofield=42, barfield=666) >>> widget.update() >>> print widget.render()
The widget's value is NO_VALUE until it gets a request: >>> widget.value Let's fill in some values via the request: >>> widget.request = TestRequest(form={'subobject.widgets.foofield':u'2', ... 'subobject.widgets.barfield':u'999', ... 'subobject-empty-marker':u'1'}) >>> widget.update() >>> print widget.render()
Widget value comes from the request: >>> wv = widget.value >>> wv {'foofield': 2, 'barfield': 999} But our object will not be modified, since there was no "apply"-like control. >>> v >>> v.foofield 42 >>> v.barfield 666 The marker must stay (we have to modify the same object): >>> v.__marker__ 'ThisMustStayTheSame' >>> converter = interfaces.IDataConverter(widget) >>> value = converter.toFieldValue(wv) Traceback (most recent call last): ... ValueError: No IObjectFactory adapter registered for z3c.form.testing.IMySubObject We have to register object factory adapters to allow the objectwidget to create objects: >>> from z3c.form.object import registerFactoryAdapter >>> registerFactoryAdapter(IMySubObject, MySubObject) >>> registerFactoryAdapter(IMySecond, MySecond) >>> value = converter.toFieldValue(wv) >>> value >>> value.foofield 2 >>> value.barfield 999 This is a different object: >>> value.__marker__ Traceback (most recent call last): ... AttributeError: 'MySubObject' object has no attribute '__marker__' Setting missing values on the widget works too: >>> widget.value = converter.toWidgetValue(field.missing_value) >>> widget.update() Default values get rendered: >>> print widget.render()
But on the return we get default values back: >>> widget.value {'foofield': 2, 'barfield': 999} >>> value = converter.toFieldValue(widget.value) >>> value HMMMM.... do we have to test error handling here? I'm tempted to leave it out as no widgets seem to do this. #Error handling is next. Let's use the value "bad" (an invalid integer literal) #as input for our internal (sub) widget. # # >>> widget.request = TestRequest(form={'subobject.widgets.foofield':u'55', # ... 'subobject.widgets.barfield':u'bad', # ... 'subobject-empty-marker':u'1'}) # # # >>> widget.update() # >>> print widget.render() # # #
#
# #
#
# #
#
# #
#
The entered value is not a valid integer literal.
#
# #
# #
# # # #Getting the widget value raises the widget errors: # # >>> widget.value # Traceback (most recent call last): # ... # MultipleErrors # #Our object still must retain the old values: # # >>> v # # >>> v.foofield # 42 # >>> v.barfield # 666 # >>> v.__marker__ # 'ThisMustStayTheSame' In forms ======== Do all that fun in add and edit forms too: We have to provide an adapter first: >>> zope.component.provideAdapter(z3c.form.browser.object.ObjectFieldWidget) Forms and our objectwidget fire events on add and edit, setup a subscriber for those: >>> eventlog = [] >>> import zope.lifecycleevent >>> @zope.component.adapter(zope.lifecycleevent.ObjectModifiedEvent) ... def logEvent(event): ... eventlog.append(event) >>> zope.component.provideHandler(logEvent) >>> @zope.component.adapter(zope.lifecycleevent.ObjectCreatedEvent) ... def logEvent2(event): ... eventlog.append(event) >>> zope.component.provideHandler(logEvent2) >>> def printEvents(): ... for event in eventlog: ... print str(event) ... if isinstance(event, zope.lifecycleevent.ObjectModifiedEvent): ... for attr in event.descriptions: ... print attr.interface ... print attr.attributes We define an interface containing a subobject, and an addform for it: >>> from z3c.form import form, field >>> from z3c.form.testing import MyObject, IMyObject Note, that creating an object will print some information about it: >>> class MyAddForm(form.AddForm): ... fields = field.Fields(IMyObject) ... def create(self, data): ... print "MyAddForm.create", str(data) ... return MyObject(**data) ... def add(self, obj): ... self.context[obj.name] = obj ... def nextURL(self): ... pass We create the form and try to update it: >>> request = TestRequest() >>> myaddform = MyAddForm(root, request) >>> myaddform.update() As usual, the form contains a widget manager with the expected widget >>> myaddform.widgets.keys() ['subobject', 'name'] >>> myaddform.widgets.values() [, ] The addform has our ObjectWidget which in turn contains a subform: >>> myaddform.widgets['subobject'].subform Which in turn contains the sub-widgets: >>> myaddform.widgets['subobject'].subform.widgets.keys() ['foofield', 'barfield'] If we want to render the addform, we must give it a template: >>> import os >>> from zope.app.pagetemplate import viewpagetemplatefile >>> from z3c.form import tests >>> def addTemplate(form): ... form.template = viewpagetemplatefile.BoundPageTemplate( ... viewpagetemplatefile.ViewPageTemplateFile( ... 'simple_edit.pt', os.path.dirname(tests.__file__)), form) >>> addTemplate(myaddform) Now rendering the addform renders the subform as well: >>> print myaddform.render()
We don't have the object (yet) in the root: >>> root['first'] Traceback (most recent call last): ... KeyError: 'first' Let's try to add an object: >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'66', ... 'form.widgets.subobject.widgets.barfield':u'99', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.add':'Add'}) >>> myaddform.request = request >>> myaddform.update() MyAddForm.create {'subobject': , 'name': u'first'} Wow, it got added: >>> root['first'] >>> root['first'].subobject Field values need to be right: >>> root['first'].subobject.foofield 66 >>> root['first'].subobject.barfield 99 Let's see our event log: >>> len(eventlog) 4 >>> printEvents() ('foofield', 'barfield') >>> eventlog=[] Let's try to edit that newly added object: >>> class MyEditForm(form.EditForm): ... fields = field.Fields(IMyObject) >>> editform = MyEditForm(root['first'], TestRequest()) >>> addTemplate(editform) >>> editform.update() Watch for the widget values in the HTML: >>> print editform.render()
Let's modify the values: >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'43', ... 'form.widgets.subobject.widgets.barfield':u'55', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.apply':'Apply'}) They are still the same: >>> root['first'].subobject.foofield 66 >>> root['first'].subobject.barfield 99 >>> editform.request = request >>> editform.update() Until we have updated the form: >>> root['first'].subobject.foofield 43 >>> root['first'].subobject.barfield 55 Let's see our event log: >>> len(eventlog) 2 >>> printEvents() ('foofield', 'barfield') ('subobject',) >>> eventlog=[] After the update the form says that the values got updated and renders the new values: >>> print editform.render() Data successfully updated.
Let's see if the widget keeps the old object on editing: We add a special property to keep track of the object: >>> root['first'].__marker__ = "ThisMustStayTheSame" >>> root['first'].subobject.foofield 43 >>> root['first'].subobject.barfield 55 Let's modify the values: >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'666', ... 'form.widgets.subobject.widgets.barfield':u'999', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.apply':'Apply'}) >>> editform.request = request >>> editform.update() Let's check what are ther esults of the update: >>> root['first'].subobject.foofield 666 >>> root['first'].subobject.barfield 999 >>> root['first'].__marker__ 'ThisMustStayTheSame' Let's make a nasty error, by typing 'bad' instead of an integer: >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'99', ... 'form.widgets.subobject.widgets.barfield':u'bad', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.apply':'Apply'}) >>> editform.request = request >>> eventlog=[] >>> editform.update() Eventlog must be clean: >>> len(eventlog) 0 Watch for the error message in the HTML: it has to appear at the field itself and at the top of the form: >>> print editform.render() There were some errors.
  • my object:
    The entered value is not a valid integer literal.
The entered value is not a valid integer literal.
The entered value is not a valid integer literal.
The object values must stay at the old ones: >>> root['first'].subobject.foofield 666 >>> root['first'].subobject.barfield 999 Let's make more errors: Now we enter 'bad' and '999999', where '999999' hits the upper limit of the field. >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'999999', ... 'form.widgets.subobject.widgets.barfield':u'bad', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.apply':'Apply'}) >>> editform.request = request >>> editform.update() Both errors must appear at the top of the form: >>> print editform.render() There were some errors.
  • my object:
    Value is too big
    The entered value is not a valid integer literal.
Value is too big
The entered value is not a valid integer literal.
Value is too big
The entered value is not a valid integer literal.
And of course, the object values do not get modified: >>> root['first'].subobject.foofield 666 >>> root['first'].subobject.barfield 999 #Let's make an other error, by typing a newline into name: # # >>> request = TestRequest(form={ # ... 'form.widgets.subobject.widgets.foofield':u'1', # ... 'form.widgets.subobject.widgets.barfield':u'2', # ... 'form.widgets.name':u'first\nline', # ... 'form.widgets.subobject-empty-marker':u'1', # ... 'form.buttons.apply':'Apply'}) # # >>> editform.request = request # >>> eventlog=[] # >>> editform.update() THIS IS BAD: #>>> len(eventlog) #0 #>>> printEvents() # # #('foofield', 'barfield') Simple but often used use-case is the display form: >>> editform = MyEditForm(root['first'], TestRequest()) >>> addTemplate(editform) >>> editform.mode = interfaces.DISPLAY_MODE >>> editform.update() >>> print editform.render()
666
999
first
Let's see what happens in HIDDEN_MODE: (not quite sane thing, but we want to see the objectwidget rendered in hidden mode) >>> zope.component.provideAdapter( ... WidgetTemplateFactory(getPath('object_hidden.pt'), 'text/html'), ... (None, None, None, None, interfaces.IObjectWidget), ... IPageTemplate, name=interfaces.HIDDEN_MODE) >>> editform = MyEditForm(root['first'], TestRequest()) >>> addTemplate(editform) >>> editform.mode = interfaces.HIDDEN_MODE >>> editform.update() Note, that the labels and the button is there because the form template for testing does/should not care about the form being hidden. What matters is that the objectwidget is rendered hidden. >>> print editform.render()
Editforms might use dicts as context: >>> newsub = MySubObject() >>> newsub.foofield = 78 >>> newsub.barfield = 87 >>> class MyEditFormDict(form.EditForm): ... fields = field.Fields(IMyObject) ... def getContent(self): ... return {'subobject': newsub, 'name': u'blooki'} >>> editform = MyEditFormDict(None, TestRequest()) >>> addTemplate(editform) >>> editform.update() Watch for the widget values in the HTML: >>> print editform.render()
Let's modify the values: >>> request = TestRequest(form={ ... 'form.widgets.subobject.widgets.foofield':u'43', ... 'form.widgets.subobject.widgets.barfield':u'55', ... 'form.widgets.name':u'first', ... 'form.widgets.subobject-empty-marker':u'1', ... 'form.buttons.apply':'Apply'}) They are still the same: >>> newsub.foofield 78 >>> newsub.barfield 87 Until updating the form: >>> editform.request = request >>> eventlog=[] >>> editform.update() >>> newsub.foofield 43 >>> newsub.barfield 55 >>> len(eventlog) 2 >>> printEvents() ('foofield', 'barfield') ('subobject', 'name') Object in an Object situation ============================= We define an interface containing a subobject, and an addform for it: >>> from z3c.form import form, field >>> from z3c.form.testing import IMyComplexObject Note, that creating an object will print some information about it: >>> class MyAddForm(form.AddForm): ... fields = field.Fields(IMyComplexObject) ... def create(self, data): ... print "MyAddForm.create", str(data) ... return MyObject(**data) ... def add(self, obj): ... self.context[obj.name] = obj ... def nextURL(self): ... pass We create the form and try to update it: >>> request = TestRequest() >>> myaddform = MyAddForm(root, request) >>> myaddform.update() As usual, the form contains a widget manager with the expected widget >>> myaddform.widgets.keys() ['subobject', 'name'] >>> myaddform.widgets.values() [, ] The addform has our ObjectWidget which in turn contains a subform: >>> myaddform.widgets['subobject'].subform Which in turn contains the sub-widgets: >>> myaddform.widgets['subobject'].subform.widgets.keys() ['subfield', 'moofield'] >>> myaddform.widgets['subobject'].subform.widgets['subfield'].subform.widgets.keys() ['foofield', 'barfield'] If we want to render the addform, we must give it a template: >>> addTemplate(myaddform) Now rendering the addform renders the subform as well: >>> print myaddform.render()
Coverage happiness ------------------ Converting interfaces.NO_VALUE holds None: >>> converter.toFieldValue(interfaces.NO_VALUE) is None True This is a complicated case. Happens when the context is a dict, and the dict misses the field. (Note, we're making ``sub__object`` instead of ``subobject``) >>> context = dict(sub__object=None, foo=123, bar=456) All the story the create a widget: >>> field = zope.schema.Object( ... __name__='subobject', ... title=u'my object widget', ... schema=IMySubObject) >>> wv = {'foofield': 2, 'barfield': 999} >>> request = TestRequest() >>> widget = z3c.form.browser.object.ObjectWidget(request) >>> widget = FieldWidget(field, widget) >>> widget.context = context >>> widget.value = wv >>> widget.update() >>> converter = interfaces.IDataConverter(widget) And still we get a MySubObject, no failure: >>> value = converter.toFieldValue(wv) >>> value >>> value.foofield 2 >>> value.barfield 999 Easy (after the previous). In case the previous value on the context is None (or missing). We need to create a new object to be able to set properties on. >>> context['subobject'] = None >>> value = converter.toFieldValue(wv) >>> value >>> value.foofield 2 >>> value.barfield 999 In case there is something that cannot be adapted to the right interface, it just burps: (might be an idea to create in this case also a new blank object) >>> context['subobject'] = u'brutal' >>> converter.toFieldValue(wv) Traceback (most recent call last): ... TypeError: ('Could not adapt', u'brutal', ) >>> context['subobject'] = None One more. Value to convert misses a field. Should never happen actually: >>> wv = {'foofield': 2} >>> value = converter.toFieldValue(wv) Known property is set: >>> value.foofield 2 Unknown sticks ti it's default value: >>> value.barfield 2222