Description
z3c.form is flexible, powerful and complex form library for Zope 3. It is recommended way to create complex, Python driven, forms for Plone 4 onwards.
This document gives you
z3c.form uses zope.schema package to define form data model. Then it applies its own form specific data extraction (HTTP request processing), field, widget and form button logic on the top of this.
Note
You can use z3c.form as standalone package. But if you want to make the development more easier for you you'd probably prefer shortcut bindings and directives provided with Dexterity subsystem
Read more about creating schema-driven forms with Dexterity
Form model consist of
Form call chain goes like
Form.update() is called
[plone.autoform based forms only] Calls Form.updateFields() - this will set widget factory methods for fields. If you want to customize the type of the widget associated with the field do it here. If your form is not plone.autoform based you need to edit form.schema widget factories on the module level code after the class has been constructed. The logic mapping widget hints to widgets is in plone.autoform.utils.
Calls Form.updateWidgets() - you can customize widgets in this point if you override this method. self.widgets instance is created based on self.fields property.
Calls Form.updateActions()
- Calls the action handler (button handler which was pressed)
- If it's edit form, action handler calls applyChanges() to store new values on the object and return True if any value was changed.
Form.render() is called
- Outputs form HTML based on widgets and their templates
Here is a minimal form implementation using z3c.form and Dexterity
form.py:
"""
Simple sample form
"""
from five import grok
from plone.directives import form
from zope import schema
from z3c.form import button
from Products.CMFCore.interfaces import ISiteRoot
from Products.statusmessages.interfaces import IStatusMessage
class IMyForm(form.Schema):
""" Define form fiels """
name = schema.TextLine(
title=u"Your name",
)
class MyForm(form.SchemaForm):
""" Define Form handling
This form can be accessed as http://yoursite/@@my-form
"""
grok.name('my-form')
grok.require('zope2.View')
grok.context(ISiteRoot)
schema = IMyForm
ignoreContext = True
@button.buttonAndHandler(u'Ok')
def handleApply(self, action):
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
# Do something with valid data here
# Set status on this form page
# (this status message is not bind to the session and does not go thru redirects)
self.status = "Thank you very much!"
@button.buttonAndHandler(u"Cancel")
def handleCancel(self, action):
"""User cancelled. Redirect back to the front page.
"""
Form global status message tells whether the form action succeeded or not.
Form status message will be rendered only on the form. If you want to set a message which will be visible even if the user renders other page after form, you need to use Products.statusmessage.
To set the form status message:
form.status = u"My message"
HTTP request must have field at least one of buttons filled
Form widget naming must match HTTP post values. Usually widgets have form.widgets prefix.
which automatically converts string input to Python primitives. For example, all choice/select values are Python lists.
interger 1 to be processed
Usually you can get the dummy HTTP request object via acquisition self.portal.REQUEST
Example (incomplete):
layout = "accommondationsummary_view"
# Zope publisher uses Python list to mark <select> values
self.portal.REQUEST["form.widgets.area"] = [SAMPLE_AREA]
self.portal.REQUEST["form.buttons.search"] = u"Search"
view = self.portal.cards.restrictedTraverse(layout)
# Call update() for form
view.process_form()
print view.form.render()
# Always check form errors after update()
errors = view.errors
self.assertEqual(len(errors), 0, "Got errors:" + str(errors))
If you want to change the page template producing <form>...</form> part of the HTML code, follow the instructions below.
Note
Generally, when you have a template which extends Plone main_template you need to use the Products.Five.browser.pagetemplatefile.ViewPageTemplateFile class.
Example:
# Do not mix with Products.Five.browser.pagetemplatefile.ViewPageTemplateFile
from zope.app.pagetemplate import ViewPageTemplateFile as Zope3PageTemplateFile
class AddHeaderAnimationForm(crud.AddForm):
""" Present form for adding a header animation """
template = Zope3PageTemplateFile("custom-form-template.pt")
If you want to change the surroundings around the z3c.form form, like Plone main template, text above and below the form, you can do as in the following example:
from Products.Five.browser import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile as FiveViewPageTemplateFile
from plone.directives import form
from plone.z3cform.layout import FormWrapper, wrap_form
class EditHeaderBehaviorForm(form.EditForm):
""" Form which displays options to edit header animation.
"""
...
class EditHeaderBehaviorView(FormWrapper):
""" Render Plone frame around our form with little modifications """
# We need to define form and index attributes for custom FormWrapper
# form points to our Form class
form = EditHeaderBehaviorForm
# Index is Zope 2 page template file which renders the frame around the form
index = FiveViewPageTemplateFile("edit_header.pt")
def __init__(self, context, request):
# We can optionally set some variables in the constructor
FormWrapper.__init__(self, context, request)
self.header_animation_helper = self.context.restrictedTraverse("@@header_animation_helper")
# Our view exposes two custom functions to the template
def getAnimationCount(self):
""" Return how many animations are availabe in the context """
return len(self.header_animation_helper.header.alternatives)
def getHeadeDefiner(self):
""" Return the parent object defining animations in this context """
return self.header_animation_helper.defining_context
And corresponding template edit_header.pt:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal"
xmlns:metal="http://xml.zope.org/namespaces/metal"
xmlns:i18n="http://xml.zope.org/namespaces/i18n"
lang="en"
metal:use-macro="here/main_template/macros/master"
i18n:domain="plone.app.headeranimation">
<body>
<metal:main fill-slot="main">
<tal:main-macro metal:define-macro="main">
<h1 class="documentFirstHeading" tal:content="view/label">Title</h1>
<div id="skel-contents">
<span tal:replace="structure view/contents" />
</div>
<!-- Custom section goes here below the form -->
<h2>Available animations</h2>
<div id="animations">
<span>
We have <b tal:content="view/getAnimationCount"> animations or images</b>
defined by <a tal:attributes="href view/getHeaderDefiner/absolute_url" tal:content="view/getHeadeDefiner/title_or_id" />
</span>
</div>
</tal:main-macro>
</metal:main>
Note
Generally, when you have a template which extends Plone main_template you need to use the Products.Five.browser.pagetemplatefile.ViewPageTemplateFile class.
Field is responsible for 1) prepopulating form values from context 2) storing data to context after succesful POST.
Form fields are stored in form.fields variable which is instance of Fields class (ordered dictionary like).
Fields are created by adapting one or more zope.schema fields for z3c.form using Fields() constructor.
Example of creating one field:
import zope.schema
import z3c.form.field
schema_field = zope.schema.TextLine()
form_fields = z3c.form.field.Fields(schema_field)
# This is a reference to newly created z3c.form.field.Field object
one_form_field = zfields.values()[0]
Another example:
import zope.schema
import z3c.form.field
...
field = zope.schema.Bool(__name__ = "death_autofill",
title=_(u"Fill missing timepoints"),
description=_(u"Automatically fill information in missing timepoints if they occur after the death time"),
required=False,
default=True)
# Construct z3c.form field
fields_objects = z3c.form.field.Fields(field)
# We can perform autofill only if we know the treatment time
form.fields += fields_objects
Use overridden += operator of Fields instance. Fields instances can be added to the existing Fields instances.
Example:
self.form.fields += z3c.form.Fields(schema_field)
Fields can be accessed by their name in form.fields. Example:
self.form.fields["myfieldname"].name = u"Foobar"
zope.schema Field is stored as a field attribute of a field. Example:
textline = self.form.fields["myfieldname"].field # zope.schema.TextLine
There is field.readonly flag.
Example code:
class AREditForm(crud.EditForm):
""" Form whose fields are dynamically constructed """
def ar_editable(self):
""" Arbitary condition deciding whether fields on this form are
patient=self.__parent__.__parent__
if patient.getConfirmedAR() in (None,'','EDITABLE_AR'):
return True
return False
@property
def fields(self):
"""
Dynamically create field data based on run-time constructed schema.
Instead using static ``fields`` attribute, we use Python property
which allows us to generate z3c.form.fields.Fields instance for the
for run-time.
"""
constructor = ARFormConstructor(self.context, self.context.context, self.request)
# Create z3c.form.field.Fields object instance
fields = constructor.getFields()
if not self.ar_editable():
# Disable all fields in edit mode if this form is locked out
for f in fields.values():
f.mode = z3c.form.interfaces.DISPLAY_MODE
return fields
You might also want to disable edit button if none if the fields are editable:
# Make edit button conditional AREditSubForm.buttons["apply"].condition = lambda form: form.has_edit_button()
Note
You can also set = z3c.form.interfaces.DISPLAY_MODE in updateWidgets() if you are not dynamically poking form fields themselves.
Warning
Do not modify fields on singleton instances (form or fields objects are shared between all forms). This causes problems on concurrent access.
Note
zope.schema.Field has readonly propertly. z3c.form.field.Field does not have this property, but has mode property. Do not confuse these two.
Below is an example how to include new schemas in fly:
class EditForm(dexterity.EditForm, Helper):
grok.context(IFlexibleContent)
def updateFields(self):
super(dexterity.EditForm, self).updateFields()
sections = self.getSections()
# See plone.app.z3cform.fieldsets.extensible for more examples
for s in sections:
# s = {'schema': <InterfaceClass your.app.content.flexiblecontent.IBodyText>, 'id': u'title', 'name': u'Title'}
if s == None:
# This section has been removed from available flexi_blocks
continue
# convert zope schema interface to z3c.form.Fields instance
schema = s["schema"]
if not schema.providedBy(self.context):
# We need to force the content item to provide
# custom for interfaces or datamanger is not happy
# Module z3c.form.datamanager, line 51, in adapted_context
# TypeError: ('Could not adapt', <Item at /xxx/tydryd>, <InterfaceClass xxx.app.content.flexiblecontent.IColumns>)
alsoProvides(self.context, schema) # XXX: This is persistent change?
# We need to manually apply hints from plone.directives.form, as
# updateFields() does it for base schema earlier
processFields(self, schema, permissionChecks=True)
print "Final results"
for name, field in self.fields.items():
print str(name) + " " + str(field)
Example:
class IDeal(form.Schema):
"""
Deals and discounts item
"""
validUntil = schema.Datetime(title=u"Valid until")
See
E.g. to make "Accept Terms and Conditions" checkbox
Widget is responsible for 1) rendering HTML code for input 2) parsing HTTP post input.
Widgets are stored as widgets attribute of a form. It is presented by ordered dict like Widgets class.
Widgets are not available until form's update() and updateWidgets() methods have been called. updateWidgets() will bind() widgets to the form context. For example, vocabularies defined by name are resolved in this point.
Widget has two names:
- widget.__name__ is the name of the corresponding field. Look ups from form.widgets[] can be done using this name.
- widget.name is the decorated name used in HTML code. It is in format ${form name}.${field set name}.${widget.__name__}.
Zope publisher will also mangle widget names based on what kind of input the widget takes. When HTTP POST request comes in, Zope publisher automatically converts <select> dropdowns to lists and so on.
Widget can be accessed by its field's name. Example:
class MyForm(z3c.form.Form):
def update(self):
z3c.form.Form.update(self)
widget = form.widgets["myfieldname"] # Get one wiget
for w in wiget.items(): print w # Dump all widgets
Example:
from z3c.form import form
class MyForm(form.Form):
def updateWidgets(self):
""" Customize widget options before rendering the form. """
form.Form.updateWidgets(self)
# Dump out all widgets - note that each <fieldset> is a subform and this function only
# concerns the current fieldset
for i in self.widgets.items():
print i
With Dexterity forms you can use plone.directives.fotm:
from z3c.form.interfaces import IAddForm, IEditForm
class IFlexibleContent(form.Schema):
"""
Description of the Example Type
"""
# -*- Your Zope schema definitions here ... -*-
form.order_before(sections='title')
form.mode(sections='hidden')
form.mode(IEditForm, sections='input')
form.mode(IAddForm, sections='input')
sections = schema.TextLine(title=u"Sections")
Widgets are stored in form.widgets dictionary. Mapping is field name -> widget. Widget label can be different than field name.
Example:
from z3c.form import form
class MyForm(form.Form):
def updateWidgets(self):
""" Customize widget options before rendering the form. """
self.widgets["myfield"].label = u"Foobar"
If you want to have a complete different Python class for widget you need to override field's widget factory in module body code after fields have been constructed in the class or in update() for dynamically constructed fields:
def update(self):
self.fields["animation"].widgetFactory = HeaderFileFieldWidget
plone.z3cform allows you to reorder the field widgets by overriding the update method of the form class.
Example:
from z3c.form import form
from plone.z3cform.fieldsets.utils import move
class MyForm(form.Form):
def update(self):
super(MyForm, self).update()
move(self, 'fullname', before='*')
move(self, 'username', after='fullname')
super(ProfileRegistrationForm, self).update()
For more information about how to reorder fields see the plone.z3cform pypi page:
<http://pypi.python.org/pypi/plone.z3cform#fieldsets-and-form-extenders>`_
If you want to avoid hardwired required on fields and toggle then conditionally you need to supplied dynamically modified schema field to z3c.form.field.Fields instance of the form.
Example:
class ShippingAddressForm(CheckoutSubform):
ignoreContext = True
label = _(u"Shipping address")
# Distinct fields on same <form> HTML element
prefix = "shipping"
def __init__(self, optional, content, request, parentForm):
"""
@param optional: Whether shipping address should be validated or not.
"""
subform.EditSubForm.__init__(self, content, request, parentForm)
self.optional = optional
@property
def fields(self):
""" Get the field definition for this form.
Form class's fields attribute does not have to
be fixed, it can be property also.
"""
# Construct the Fields instance as we would
# normally do in more static way
fields = z3c.form.field.Fields(ICheckoutAddress)
# We need to override the actual required from the
# schema field which is litte tricky.
# Schema fields are shared between instances
# by default, so we need to create a copy of it
if self.optional:
for f in fields.values():
# Create copy of a schema field
# and force it unrequired
schema_field = copy.copy(f.field) # shallow copy of an instance
schema_field.required = False
f.field = schema_field
return fields
By default, widgets for form fields are determined by FieldWidget adapters (defined in ZCML). You can override adapters per field using field's widgetFactory property.
Below is an example which creates a custom widget, its FieldWidget factory and uses it for one field in one form:
from zope.component import adapter, getMultiAdapter
from zope.interface import implementer, implements, implementsOnly
from z3c.form.interfaces import IFieldWidget
from z3c.form.widget import FieldWidget
from plone.formwidget.namedfile.widget import NamedFileWidget, NamedImageWidget
class HeaderFileWidget(HeaderWidgetMixin, NamedFileWidget):
# Get download url for HeaderAnimation object's file.
# Download URL is set externally by edit sub form and
download_url = None
class HeaderImageWidget(HeaderWidgetMixin, NamedImageWidget):
pass
@implementer(IFieldWidget)
def HeaderFileFieldWidget(field, request):
""" Factory for creating HeaderFileWidget which is bound to one field """
return FieldWidget(field, HeaderFileWidget(request))
class EditHeaderAnimationSubForm(crud.EditSubForm):
"""
"""
def updateWidgets(self):
""" Enforce custom widget types which get file/image attachment URL right """
# Custom widget types are provided by FieldWidget factories
# before updateWidgets() is called
self.fields["animation"].widgetFactory = HeaderFileFieldWidget
crud.EditSubForm.updateWidgets(self)
# Make edit form aware of correct image download URLs
self.widgets["animation"].download_url = "http://mymagicalurl.com"
Alternatively, you can use plone.directives.form to add widget hints to form schema.
After form.update() if the request was save request and all data was valid form applyChanges(data) is called.
By default widgets use datamanger.AttributeField and tries to store its value as a member attribute of the object returned by form.getContent().
Todo
How do add custom DataManager
Widget value, either from form POST or previous context data, is available in widget.value after form.update() call.
Widgets have a method addClass() to add extra CSS classes. This is useful if you have Javascript/JQuery associated with your special form:
widget.addClass("myspecialwidgetclass")
Note that these classes are directly applied to <input>, <select> etc. itself and not the wrapping <div> element.
zope.schema Field is stored as a field attribute of a widget. Example:
textline = form.widgets["myfieldname"].field # zope.schema.TextLine
Warning
Widget.field is not z3c.form.field.Field object.
Example:
widget = self.widgets["myselectionlist"]
token = widget.value[0] # widget.value is list of unicode strings, each is token for the vocabulary
user_readable = widget.terms.getTermByToken(token).title
Example (page template):
<td tal:define="widget view/widgets/myselectionlist">
<span tal:define="token python:widget.value[0]" tal:content="python:widget.terms.getTermByToken(token).title" />
</td>
You might want to customize the template of a widget to have custom HTML code for a specific use case.
First copy the existing page template code of the widget. For basic widgets you can find the template in the z3c.form source tree.
yourwidget.pt (text area widget copied over an example text)
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:tal="http://xml.zope.org/namespaces/tal"
tal:omit-tag="">
<!-- Sections widget custom templates -->
<textarea
id="" name="" class="" cols="" rows=""
tabindex="" disabled="" readonly="" accesskey=""
tal:attributes="id view/id;
name view/name;
class view/klass;
style view/style;
title view/title;
lang view/lang;
onclick view/onclick;
ondblclick view/ondblclick;
onmousedown view/onmousedown;
onmouseup view/onmouseup;
onmouseover view/onmouseover;
onmousemove view/onmousemove;
onmouseout view/onmouseout;
onkeypress view/onkeypress;
onkeydown view/onkeydown;
onkeyup view/onkeyup;
disabled view/disabled;
tabindex view/tabindex;
onfocus view/onfocus;
onblur view/onblur;
onchange view/onchange;
cols view/cols;
rows view/rows;
readonly view/readonly;
accesskey view/accesskey;
onselect view/onselect"
tal:content="view/value" />
</html>
from z3c.form.ptcompat import ViewPageTemplateFile
from z3c.form.interfaces import INPUT_MODE
class AddForm(DefaultAddForm):
def updateWidgets(self):
""" """
# Call parent to set-up initial widget data
DefaultAddForm.updateWidgets(self)
# Note we need to be discreet to different form modes (view, edit, hidden)
if self.fields["sections"].mode == INPUT_MODE:
# Modify a widget with certain name for our purposes
widget = self.widgets["sections"]
# widget.template is a template factory -
# Widget.render() will associate later this factory with the widget
widget.template = ViewPageTemplateFile("templates/sections.pt")
You can also interact with your form class instance from the widget template
<!-- Some hidden JSON data for our Javascripts by calling a method on our form class -->
<span style="display:none" tal:content="view/form/getBlockPlanJSON" />
You can set the widget template is using <z3c:widgetTemplate> ZCML directive
<z3c:widgetTemplate
mode="display"
widget=".interfaces.INamedFileWidget"
layer="z3c.form.interfaces.IFormLayer"
template="file_display.pt"
/>
You can also enforce widget template in the render() method of the widget class:
from zope.component import adapter, getMultiAdapter
from zope.interface import implementer, implements, implementsOnly
from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
from z3c.form.interfaces import IFieldWidget, INPUT_MODE, DISPLAY_MODE, HIDDEN_MODE
from z3c.form.widget import FieldWidget
from plone.formwidget.namedfile.widget import NamedFileWidget, NamedImageWidget
class HeaderFileWidget(NamedFileWidget):
""" Subclass widget a use a custom template """
display_template = ViewPageTemplateFile("header_file_display.pt")
def render(self):
"""See z3c.form.interfaces.IWidget."""
if self.mode == DISPLAY_MODE:
# Enforce template and do not query it from the widget template factory
template = self.display_template
return NamedFileWidget.render(self)
Widget template example:
<span id="" class="" i18n:domain="plone.formwidget.namedfile"
tal:attributes="id view/id;
class view/klass;
style view/style;
title view/title;
lang view/lang;
onclick view/onclick;
ondblclick view/ondblclick;
onmousedown view/onmousedown;
onmouseup view/onmouseup;
onmouseover view/onmouseover;
onmousemove view/onmousemove;
onmouseout view/onmouseout;
onkeypress view/onkeypress;
onkeydown view/onkeydown;
onkeyup view/onkeyup"
tal:define="value view/value;
exists python:value is not None">
<span tal:define="fieldname view/field/__name__ | nothing;
filename view/filename;
filename_encoded view/filename_encoded;"
tal:condition="python: exists and fieldname">
<a tal:content="filename"
tal:attributes="href string:${view/download_url}">Filename</a>
<span class="discreet"> — <span tal:define="sizekb view/file_size" tal:replace="sizekb">100</span> KB</span>
</span>
<span tal:condition="not:exists" class="discreet" i18n:translate="no_file">
No file
</span>
</span>
Buttons enable actions in forms. AddForm and EditForm base classes come with default buttons (Save).
More information in z3c.form documentation
The easiest way to add buttons their handlers is to use a function decorator z3c.form.button.buttonAndHandler().
The first parameter is user visible label and the second one is <input> name.
Example:
from z3c.form import button
class Form(...):
@button.buttonAndHandler(_('Add'), name='add')
def handle_add(self, action):
data, errors = self.extractData()
if errors:
self.status = "Please correct errors"
return
self.applyChanges(data)
self.status = _(u"Item added successfully.")
The default z3c.form.form.AddForm and z3c.form.form.EditForm Add and Save button handler calls are good code examples.
You want to manipulate buttons if you want to hide buttons dynamically, manipulate labels, etc.
Buttons are stored in buttons class attribute.
Warning
Button storage is shared between all form instances, so do not mutate its content. Instead create a copy of it if you wish to have form specific changes.
Example:
self.mobile_form_instance = MobileForm(self.context, self.request)
for i in self.mobile_form_instance.buttons.items(): print i
('apply', <Button 'apply' u'Apply'>)
Here is an example how to hide all buttons from a certain form instance.
Example:
import copy
def update(self):
# Hide form buttons
# Create immutable copy which you can manipulate
self.mobile_form_instance.buttons = copy.deepcopy(self.mobile_form_instance.buttons)
# Remove button using dictionary style delete
for button_id in self.mobile_form_instance.buttons.keys():
del self.mobile_form_instance.buttons[button_id]
In the example below Buttons array is already constructed dynamically and we can manipulate it:
def setActions(self):
""" Add button to the form based on dynamic conditions. """
if self.isSaveEnabled():
but = button.Button("save", title=u"Save")
self.form.buttons += button.Buttons(but)
self.form.buttons._data_keys.reverse() # Fix Save button to left
handler = button.Handler(but, self.form.__class__.handleSave)
self.form.handlers.addHandler(but, handler)
Subforms are embedded z3c forms inside a master form.
Subforms may have their own buttons or use the controls from the maste form. You need to call update() manually for subforms.
More info
Parent and subform actions must be linked.
Example:
class CheckoutForm(z3c.form.form.EditForm):
@button.buttonAndHandler(_('Continue'), name='continue')
def handleContinue(self, action):
""" Extract the checkout data to session and redirect to payment processer checkout screen.
Note:
"""
# Following has been copied from z3c.form.form.EditForm
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
changes = self.applyChanges(data)
if changes:
self.status = self.successMessage
else:
self.status = self.noChangesMessage
class CheckoutSubform(subform.EditSubForm):
""" Add support for continue action. """
def execute(self):
"""
Make sure that the form is refreshed when parent
form Continue is pressed.
"""
data, errors = self.extractData()
if errors:
self.errors = errors
self.status = self.formErrorsMessage
return errors
content = self.getContent()
z3c.form.form.applyChanges(self, content, data)
return None
@button.handler(CheckoutForm.buttons['continue'])
def handleContinue(self, action):
""" What happens when the parent form button is pressed """
self.execute()
Below is an example how to convert existing form instance to be used as an subform in another form:
def convertToSubForm(self, form_instance):
"""
Make existing form object behave like subform object.
* Do not render <form> frame
* Do not render actions
@param form_instance: Constructed z3c.form.form.Form object
"""
# Create mutable copy which you can manipulate
form_instance.buttons = copy.deepcopy(form_instance.buttons)
# Remove subform action buttons using dictionary style delete
for button_id in form_instance.buttons.keys():
del form_instance.buttons[button_id]
if HAS_WRAPPER_FORM:
# Plone 4 / Plone 3 compatibility
zope.interface.alsoProvides(form_instance, IWrappedForm)
# Use subform template - this prevents getting embedded <form>
# elements inside the master <form>
import plone.z3cform
#from zope.pagetemplatefile import ViewPageTemplateFile as Zope3PageTemplateFile
from zope.app.pagetemplate import ViewPageTemplateFile as Zope3PageTemplateFile
from zope.app.pagetemplate.viewpagetemplatefile import BoundPageTemplate
template = Zope3PageTemplateFile('subform.pt', os.path.join(os.path.dirname(plone.z3cform.__file__), "templates"))
form_instance.template = BoundPageTemplate(template, form_instance)
Note
If it's possible try to base class your form class hiearchy so that you can use the same class mix-in for normal forms and subforms.
CRUD (Create, read, update, delete) forms manage list of objects.
CRUD form elements
Notes: context attribute of add and edit form is the parent CRUD form. Context attribute of edit sub form is the edit form.
By default, the status message is rendered inside plone.app.z3cform macros.pt above the form:
<metal:define define-macro="titlelessform">
<tal:status define="status view/status" condition="status">
<dl class="portalMessage error" tal:condition="view/widgets/errors">
<dt i18n:domain="plone" i18n:translate="">
Error
</dt>
<dd tal:content="status" />
</dl>
<dl class="portalMessage info" tal:condition="not: view/widgets/errors">
<dt i18n:domain="plone" i18n:translate="">
Info
</dt>
<dd tal:content="status" />
</dl>
</tal:status>
We can decouple the status message from the form, without overriding all the templates, by copying status message variable to another variable and then playing around with it in our wrapper view template.
Form class:
class HolidayServiceSearchForm(form.Form):
"""
"""
@button.buttonAndHandler(_(u"Search"))
def searchHandler(self, action):
""" Search form submit handler for product card search.
"""
data, errors = self.extractData()
if len(self.search_results) == 0:
self.status = _(u"No holiday services found.")
else:
msgid = _("found_results", default=u"Found ${results} holiday services.", mapping={u"results" : len(self.search_results)})
self.status = self.context.translate(msgid)
...
# Use non-standard location to display the status
# for success messages
if len(self.widgets.errors) == 0:
self.result_message = self.status
self.status = None
class HolidayServiceSearchView(FormWrapper):
"""
HolidayService browser view
"""
form = HolidayServiceSearchForm
def result_message(self):
""" Display result message in non-standard location """
if len(self.form_instance.widgets.errors) == 0:
# Do not display form highlight errors here
return self.form_instance.result_message
... and then we can use a special result_message view accessor in our view template code
By default, z3c.form reads incoming context values as the object attributes. This behavior can be customized using data managers.
You can, for example, use Python dictionaries to read and store form data.
The following hack can be used if you have an object which does not conform your form interface and you want to explose only certain object attribute to the form to be edited.
Example:
class ISettings(zope.interface.Interface):
# This maps to Archetypes field confirmedAR on SitsPatient
confirmedAR = zope.schema.Choice(title=_(u"Confirm adherse reactions"),
description=_(u"Confirm that all adherse reactions regarding the patient life cycle have been entered here and there will be no longer adherse reaction data"),
vocabulary=make_zope_schema_vocabulary(ADVERSE_STATUS_VOCABULARY))
class ARSettingsForm(form.Form):
""" General settings for all adherse reactions """
fields = Fields(ISettings)
def getContent(self):
""" """
# Create a temporary object holding the settings values out of the patient
class TemporarySettingsContext(object):
zope.interface.implements(ISettings)
obj = TemporarySettingsContext()
# Copy values we want to expose to the form from Plone context item to the temporary object
obj.confirmedAR = self.context.confirmedAR
return obj
Note
Since getContent() is also used in applyChanges() you need to override applyChanges() too to save values correctly back to non-temporary object.
The default behavior of z3c.form edit form is to write incoming data as the attributes of the object returned by getContent().
You can override this behavior by overriding applyChanges() method.
Example:
def applyChanges(self, data):
"""
Reflect confirmed status to Archetypes schema.
@param data: Dictionary of cleaned form data, keyed by field
"""
# This is the context given to the form when the form object was constructed
patient = self.context
assert ISitsPatient.providedBy(patient) # safety check
# Call archetypes field mutator to store the value on the patient object
patient.setConfirmedAR(data["confirmedAR"])
By using plone.directives.form and plone.app.z3cform packages you can do:
from plone.app.z3cform.wysiwyg import WysiwygFieldWidget
from mfabrik.plonezohointegration import _
class ISettings(form.Schema):
""" Define schema for settings of the add-on product """
form.widget(contact_form_prefix=WysiwygFieldWidget)
contact_form_prefix = schema.Text(title=_(u"Contact form top text"),
description=_(u"Custom text for the long contact form upper part"),
required=False,
default=u"")
More information
z3c.form.form.Form object is "wrapped" when it is rendered inside Plone page frame and having acquisition chain in intact.
Since plone.app.z3cform 0.5.0 the behavior goes like this
Wrapper is a plone.z3cform.interfaces.IWrappedForm marker interface on the form object, applied it after the form instance has been constructed. If this marker interface is not applied, plone.z3cform.ZopeTwoFormTemplateFactory tries to embed form into Plone page frame. If the form is indended not be rendered as full page form, this usually leads to the following exception:
*** ContentProviderLookupError: plone.htmlhead
The form tries to render the full Plone page. Rendering this page needs an acquisition chain set-up for the view and the template. Embedded forms do not have this, or it would lead to recursion error.
If you are constructing form instances manually and want to render them without Plone page decoration, you must make sure that automatic form wrapping does not take place:
import zope.interface
from plone.z3cform.interfaces import IWrappedForm
class SomeView(BrowserView):
def init(self):
""" Constructor embedded sub forms """
# Construct few embedded forms
self.mobile_form_instance = MobileForm(self.context, self.request)
zope.interface.alsoProvides(self.mobile_form_instance, IWrappedForm)
self.publishing_form_instance = PublishingForm(self.context, self.request)
zope.interface.alsoProvides(self.publishing_form_instance, IWrappedForm)
self.override_form_instance = getMultiAdapter((self.context, self.request), IOverrideForm)
zope.interface.alsoProvides(self.override_form_instance, IWrappedForm)
By default, when plone.app.z3cform is installed through the add-on installer, all forms have full Plone page frame. If you are rendering forms inside non-full-page objects, you need to change the default template.
Below is an example how to put z3c.form based form into a portlet.
Note
plone.app.z3cform version 0.5.1 or later is needed, as older versions do not support overriding form.action property.
You need following
Portlet code:
from plone.z3cform.layout import FormWrapper
class PortletFormView(FormWrapper):
""" Form view which renders z3c.forms embedded in a portlet.
Subclass FormWrapper so that we can use custom frame template. """
index = ViewPageTemplateFile("formwrapper.pt")
class Renderer(base.Renderer):
""" z3c.form portlet renderer.
Instiate form and wrap it to a special layout template
which will give the form suitable frame to be used in the portlet.
We also set a form action attribute, so that
the browser goes to another page after the form has been submitted
(we really don't know what kind of page the portlet is displayed
and is it safe to submit forms there, so we do this to make sure).
The action page points to a browser:page view where the same
form is displayed as full-page form, giving the user to better
user experience to fix validation errors.
"""
render = ViewPageTemplateFile('zohocrmcontact.pt')
def __init__(self, context, request, view, manager, data):
base.Renderer.__init__(self, context, request, view, manager, data)
self.form_wrapper = self.createForm()
def createForm(self):
""" Create a form instance.
@return: z3c.form wrapped for Plone 3 view
"""
context = self.context.aq_inner
returnURL = self.context.absolute_url()
# Create a compact version of the contact form
# (not all fields visible)
form = ZohoContactForm(context, self.request, returnURLHint=returnURL, full=False)
# Wrap a form in Plone view
view = PortletFormView(context, self.request)
view = view.__of__(context) # Make sure acquisition chain is respected
view.form_instance = form
return view
def getContactFormURL(self):
""" For rendering the form link at the bottom of the portlet.
@return: URL leading to the full contact form
"""
return self.form_wrapper.form_instance.action
formwrapper.pt is just a dummy form view template which wraps the form. This differs from standard form wrapper by not rendering Plone main layout around the form.
<div class="portlet-form">
<div tal:replace="structure view/contents" />
</div>
Then the portlet template itself (zohoportlet.pt) renders the portlet. Form is referred by syntax <form tal:replace="structure view/form_wrapper" />.
<dl class="portlet portletZohoCRMContact"
i18n:domain="mfabrik.plonezohointegration">
<dt class="portletHeader">
<span class="portletTopLeft"></span>
<span i18n:translate="portlet_title">
Contact Us
</span>
<span class="portletTopRight"></span>
</dt>
<dd class="portletItem odd">
<form tal:replace="structure view/form_wrapper" />
</dd>
<dd class="portletFooter">
<span class="portletBottomLeft"></span>
<a href=""
tal:attributes="href view/getContactFormURL"
i18n:translate="box_more_news_link">
Longer contact form…
</a>
<span class="portletBottomRight"></span>
</dd>
</dl>
Note
Viewlet behave little different, since they do automatically some acquisition chain mangling when you assign variables to self. Thus you should never have self.view = view or self.form = form in viewlet.
Template example for viewlet (don't do sel.form_wrapper)
<div id="my-viewlet">
<form tal:replace="structure python:view.createForm()()" />
</div>
Then the necessary parts of form itself:
class IZohoContactForm(zope.interface.Interface):
""" Form field definitions for Zoho contact forms """
first_name = schema.TextLine(title=_(u"First name"))
last_name = schema.TextLine(title=_(u"Last name"))
company = schema.TextLine(title=_(u"Company / organization"), description=_(u"The organization which you represent"))
email = schema.TextLine(title=_(u"Email address"), description=_(u"Email address we will use to contact you"))
phone_number = schema.TextLine(title=_(u"Phone number"),
description=_(u"Your phone number in international format. E.g. +44 12 123 1234"),
required=False,
default=u"")
returnURL = schema.TextLine(title=_(u"Return URL"),
description=_(u"Where the user is taken after the form is succesfully submitted"),
required=False,
default=u"")
class ZohoContactForm(Form):
""" z3c.form used to handle the new lead submission.
This form can be rendered
* standalone (@@zoho-contact-form view)
* embedded into the portlet
..note::
It is recommended to use a CSS rule
to hide form descriptions when rendered in the portlet to save
some screen estate.
Example CSS::
.portletZohoCRMContact .formHelp {
display: none;
}
"""
fields = Fields(IZohoContactForm)
label = _(u"Contact Us")
description = _(u"If you are interested our services leave your contact information below and our sales representatives will contact you.")
ignoreContext = True
def __init__(self, context, request, returnURLHint=None, full=True):
"""
@param returnURLHint: Should we enforce return URL for this form
@param full: Show all available fields or just required ones.
"""
Form.__init__(self, context, request)
self.all_fields = full
self.returnURLHint = returnURLHint
@property
def action(self):
""" Rewrite HTTP POST action.
If the form is rendered embedded on the others pages we
make sure the form is posted through the same view always,
instead of making HTTP POST to the page where the form was rendered.
"""
return self.context.portal_url() + "/@@zoho-contact-form"
def updateWidgets(self):
""" Make sure that return URL is not visible to the user.
"""
Form.updateWidgets(self)
# Use the return URL suggested by the creator of this form
# (if not acting standalone)
self.widgets["returnURL"].mode = z3c.form.interfaces.HIDDEN_MODE
if self.returnURLHint:
self.widgets["returnURL"].value = self.returnURLHint
# Prepare compact version of this formw
if not self.all_fields:
# Hide fields which we don't want to bother user with
self.widgets["phone_number"].mode = z3c.form.interfaces.HIDDEN_MODE
@button.buttonAndHandler(_('Send contact request'), name='ok')
def send(self, action):
""" Form button hander. """
data, errors = self.extractData()
if not errors:
settings = self.getZohoSettings()
if settings is None:
self.status = _(u"Zoho is not configured in Site Setup. Please contact the site administration.")
return
crm = CRM(settings.username, settings.password, settings.apikey)
# Fill in data going to Zoho CRM
lead = {
"First Name" : data["first_name"],
"Last Name" : data["last_name"],
"Company" : data["company"],
"Email" : data["email"],
}
phone = data.get("phone_number", "")
if phone != "":
# Only pass phone number to Zoho if it's set
lead["Phone"] = phone
# Pass in all prefilled lead fields configured in the site setup
lead.update(self.parseExtraFields(settings.crm_lead_extra_data))
# Open Zoho API connection
try:
# This will raise ZohoException and nuke the request
# if Zoho credentials are wrong
crm.open()
# Make sure that wfTrigger is true
# and Zoho does workflow actions for the new leads
# (like informing sales about the availability of the lead)
crm.insert_records([lead], {"wfTrigger" : "true"})
except IOError:
# Network down?
self.status = _(u"Cannot connect to Zoho servers. Please contact web site administration")
return
ok_message = _(u"Thank you for contacting us. Our sales representatives will come back to you in few days")
# Check whether this form was submitted from another page
returnURL = data.get("returnURL", "")
if returnURL != "" and returnURL is not None:
# Go to page where we were sent and
# pass the confirmation message as status message (in session)
# as we are not in the control of the destination page
from Products.statusmessages.interfaces import IStatusMessage
messages = IStatusMessage(self.request)
messages.addStatusMessage(ok_message, type="info")
self.request.response.redirect(returnURL)
else:
# Act standalone
self.status = ok_message
else:
# errors on the form
self.status = _(u"Please fill in all the fields")
This example code was taken from mfabrik.plonezohointegration product which is in Plone collective SVN.
Another tutorial
Validators
Validators are best to be added in the schema itself.
How to use widget specific validators with z3c.form example:
from z3c.form import validator
import zope.component
class IZohoContactForm(form.Schema):
""" Form field definitions for Zoho contact forms """
phone_number = schema.TextLine(title=_(u"Phone number"),
description=_(u"Your phone number in international format. E.g. +44 12 123 1234"),
required=False,
default=u"")
class PhoneNumberValidator(validator.SimpleFieldValidator):
""" z3c.form validator class for international phone numbers """
def validate(self, value):
""" Validate international phone number on input """
allowed_characters = "+- () / 0123456789"
if value != None:
value = value.strip()
if value == "":
# Assume empty string = no input
return
# The value is not required
for c in value:
if c not in allowed_characters:
raise zope.interface.Invalid(_(u"Phone number contains bad characters"))
if len(value) < 7:
raise zope.interface.Invalid(_(u"Phone number is too short"))
# Set conditions for which fields the validator class applies
validator.WidgetValidatorDiscriminators(PhoneNumberValidator, field=IZohoContactForm['phone_number'])
# Register the validator so it will be looked up by z3c.form machinery
zope.component.provideAdapter(PhoneNumberValidator)
More info
If you want to custom error messages on per field level:
from zope.schema._bootstrapinterfaces import RequiredMissing
RequiredMissingErrorMessage = error.ErrorViewMessage(_(u'Required value is missing.'), error=RequiredMissing, field=IEmailFormSchema['email'])
zope.component.provideAdapter(RequiredMissingErrorMessage, name='message')
Leave field parameter away if you want the new error message apply to all fields.
Read-only fields are not rendered in form edit mode:
courseModeAccordion = schema.TextLine(title=u"Courses by mode accordion",
default=u"Automatically from database",
readonly=True
)
If widget mode is display then it is rendered, but user cannot edit (the output as in form view mode):
form.mode(courseModeAccordion="display")
courseModeAccordion = schema.TextLine(title=u"Courses by mode accordion",
default=u"Automatically from database",
)
The source code of this file is hosted on GitHub. Everyone can update and fix errors in this document with few clicks - no downloads needed.
For basic information about updating this manual and Sphinx format please see Writing and updating the manual guide.