Using CMFFormController

Description

How to create and validate forms in Plone using its CMFFormController. Be sure to also read the CMFFormController tutorial in the Products/CMFFormController/documentation directory, included with your copy of Plone. This how-to is also available in Products/CMFFormController/www/ as the file docs.stx, included with Plone.

The CMFFormController package helps developers by simplifying the process of validating forms. It also makes it easier for site administrators to override some of the behavior of packages without modifying code, making it easier to upgrade packages without disturbing the modifications.

How it works:

  • Developers associate a set of default variables for their Page Templates. These variables control the validation that takes place after the form is submitted and the actions that occur after validation. The variables are stored on the filesystem in the .metadata properties file.
  • Site administrators can override the default validations and actions using the ZMI. Once a set of validations or actions has been specified in the ZMI, the default validations and actions will be ignored.

Forms

To take advantage of CMFFormController, you need to use Controller Page Templates rather than ordinary Page Templates. Controller Page Templates act just like ordinary Page Templates, but they do some extra work when they are viewed.

Here is a basic form that uses CMFFormController:

<form tal:define="errors options/state/getErrors"
      tal:attributes="action string:${here/absolute_url}/${template/id};"
     method="post">
   <input type="hidden" name="form.submitted" value="1" />
   <p tal:define="err errors/foo|nothing" tal:condition="err" tal:content="err" />
   <input type="text"
          name="foo"
          tal:define="val request/foo|nothing"
          tal:attributes="value val" />
   <input type="submit" name="submit" value="submit" />
</form>

Let's take a look.

  • First, we note that the form is set up to submit to itself. Forms must submit to themselves.
  • Second, we see the special hidden variable form.submitted. The controlled page template checks the REQUEST for form.submitted to see if the form has been submitted or if, instead, it has just been accessed, e.g. via a link. Forms must contain the hidden variable ``form.submitted``
  • At the beginning of the form we set the variable errors. The errors dictionary comes from the state object which is passed in the template options. The state object lets validators and scripts pass information to each other and to forms. For our purposes, the most important information is the errors dictionary, which has entries of the form {field_name:error_message}.

Before we can use this form we need to specify the validators that will be used to check the form values, and we need to specify the action that will occur after validation.

Specifying Validators

There are two basic ways to specify a form's validators.

  1. You can specify the validators in the .metadata properties for filesystem-based Controller Page Templates.
  2. You can specify the validators via the ZMI (or programmatically). These values will be stored in the ZODB as attributes of the portal_form_controller object.

If you specify validators in both places, the validators specified in the ZMI will take precedence over those specified in the .metadata file.

Specifying Validators on the Filesystem

You can specify validators on the filesystem using an objects .metadata properties file.

To create a .metadata file, simply create a file with the same name as your page template, and then append .metadata to the end of the name of the file. For instance, you might have a Controller Page Template called document_edit_form.cpt. The properties for that file would be stored in a file called document_edit_form.cpt.metadata

The .metadata file uses the standard python ConfigParser syntax. The validator section of the .metadata file would look like:

[validators]
validators = validate_script1, validate_script2

The validation scripts validate_script1 and validate_script2 will be called in order.

Type-Specific Validators

Suppose you want different validators to be called, depending on the type of context the form has.

You can do so as follows:

[validators]
validators = validate_script1
validators.Document = validate_script2

In the above example, if the context is a Document object, validate_script2 will be called for validation; for everything else, only validate_script1 will be called.

Note that the order in which the variables are specified does not matter; the type-specific validators override non-specific validators if both are applicable.

Button-Specific Validators

Suppose instead that you have two different buttons on your form, and you want different validation sequences to occur depending on which button is pressed. You can accomplish this as follows:

First, name your buttons button1 and button2:

<input type="submit"
       name="form.button.button1"
       value="First Button" />
<input type="submit"
       name="form.button.button2"
       value="Second Button" />

Next, specify validators in the .metadata file for button1 and for button2:

[validators]
validators..button1 = validate_script1, validate_script3
validators..button2 = validate_script2, validate_script4

Note the presence of the ... This is a placeholder for a type specifier. You could further specify that validate_script5 is called if button2 is pressed and the context is a Document by adding:

[validators]
validators.Document.button2 = validate_script5

Remember that button specific validators take precedence over non-specific validators.

Specifying Validators in the ZMI

If you look at a Controller Page Template in the ZMI, you will see that it looks just like an ordinary Page Template with two extra tabs, Validation and Actions. Click on the Validation tab.

The Validation tab shows all the validators for the page template in question. You can specify validators with the same kind of specialization options as above via a web form.

The validator information for all forms is stored in the portal_form_controller tool in your portal. This means that you can specify validators for filesystem objects with no problems, since the information is persisted in the ZODB. Note that the validator information is bound to the form's Id, so all forms with the same Id use the same validators. This keeps things simple when you have multiple skins:

Forms with the same Id use the same validators, no matter what skin they are in.

When a form is submitted, it first checks to see if there are any applicable validators that have been specified via the ZMI. If it finds one, it uses it. If it does not find a validator via the ZMI, it then checks the REQUEST object to see if validators have been specified in hidden variables. As a result, validators specified in the ZMI take precedence over those specified in forms.

Specifying Validators Programmatically

The portal's portal_form_controller tool has methods you can use to specify the validators for a given ControllerPageTemplate. The API is as follows:

portal_form_controller.addFormValidators(id,
                                         context_type,
                                         button,
                                         validators)

Here id is the Id of the ControllerPageTemplate, context_type is the class name for the class of the context object, button is the name of the button pressed, and validators is a comma-delimited string or a list of strings. If you want a validator to act for any class, set context_type to None. Similarly, you want a validator to act for any button, set button to None.

Specifying Actions

The sequence of validators that is executed returns a status in the state object. The default status is success, i.e. if no validators are executed, the status will be success. If a validator encounters an error, it will typically set the status to failure. The next thing we need to do in your form is to specify what happens when a given status is returned.

As with validators, there are two basic ways to specify a form's actions.

  1. You can specify the actions in the .metadata properties for filesystem-based Controller Page Templates and Controller Python Scripts.
  2. You can specify the actions via the ZMI (or programmatically). These values will be stored in the ZODB as attributes of the portal_form_controller object.

If you specify actions in both places, the actions specified in the ZMI will take precedence over those specified in the form.

Specifying Actions on the Filesystem

You can specify actions on the filesystem using an objects .metadata properties file.

Actions are stored in the same .metadata file as the validators. The syntax for the actions section of your file would look like:

[actions]
action.success = traverse_to:string:script1

In the above example, when the form is submitted and the validation scripts return a status of success, the traverse_to action is called with the argument string:script1, i.e. if the form data is valid, we run the script script1. Alternatively, we could specify action.success = redirect_to:string:http://my_url_here, which would cause the browser to be redirected to http://my_url_here.

The default action for the failure status is to reload the current form. The form will have access to all the error messages, via the state object in its options.

Type-Specific Actions

Suppose you want different actions to occur depending on the type of context the form has.

You can do so as follows:

[actions]
action.success = traverse_to:string:script1
action.success.Document = traverse_to:string:document_script

In the above example, if the context is a Document object, document_script will be executed upon successful validation; for everything else, script1 will be executed. Note that the order in which the variables are specified does not matter; the type-specific actions will override non-specific actions if both are applicable.

Button-Specific Actions

Suppose instead that you have two different buttons on your form, and you want different actions to occur depending on which button is pressed. You can accomplish this as follows:

First, name your buttons button1 and button2:

<input type="submit"
       name="form.button.button1"
       value="First Button" />
<input type="submit"
       name="form.button.button2"
       value="Second Button" />

Next, specify actionss for button1 and for button2:

[actions]
action.success..button1 = traverse_to:string:script1
action.success..button2 = traverse_to:string:script2

Note the presence of the ... This is a placeholder for a type specifier. You could further specify that document_script2 is called if button2 is pressed and the context is a Document by adding:

[actions]
action.success.Documnet.button2 = traverse_to:string:document_script2

Specifying Actions in the ZMI

If you look at a Controller Page Template in the ZMI, you will see that it looks just like an ordinary Page Template with two extra tabs, Validation and Actions. Click on the Actions tab.

The Actions tab shows all the actions for the page template in question. You can specify actions with the same kind of specialization options as above via a web form.

The action information for all forms is stored in the portal_form_controller tool in your portal. This means that you can specify actions for filesystem objects with no problems, since the information is persisted in the ZODB. Note that the action information is bound to the form's Id, so all forms with the same Id use the same actions. This keeps things simple when you have multiple skins: forms with the same Id use the same actions, no matter what skin they are in.

When a form is submitted, it first checks to see if there are any applicable actions that have been specified via the ZMI. If it finds one, it uses it. If it does not find an action via the ZMI, it then checks the REQUEST object to see if actions have been specified in hidden variables. As a result, actions specified in the ZMI take precedence over those specified in forms.

Specifying Actions Programmatically

The portal's portal_form_controller tool has methods you can use to specify the actions for a given ControllerPageTemplate. The API is as follows:

portal_form_controller.addFormAction(id,
                                     status,
                                     context_type,
                                     button,
                                     action_type,
                                     args)

Here id is the Id of the ControllerPageTemplate, status is the status for which the action will be executed, context_type is the class name for the class of the context object, button is the name of the button pressed, action_type is the type of action that will occur, and args is a string (typically a TALES expression) that will be passed to the action. If you want an action to be executed for any class, set context_type to None. Similarly, you want an action to be executed for any button, set button to None.

Validation Scripts

When writing validation scripts, use Controller Validators instead of Python Scripts. Controller Validators are just like ordinary Scripts with the addition of a ZMI Actions tab. On the file system, Controller Validators use the extension .vpy rather than .py.

Let's take a look at a basic validation script that tests the REQUEST value n to see if it is an integer:

n = context.REQUEST.get('n', None)
if not n:
   state.setError('n', 'Please enter a value', new_status='failure')
else:
   try:
      int(n)
   except ValueError:
      state.setError('n', 'Please enter an integer',
                     new_status='failure')

if state.getErrors():
   state.set(portal_status_message='Please correct the errors shown.')
return state

The first thing to note is that Controller Validators have a built-in state object called state. This state object (of class ControllerState) contains basic information about what has happened during the validation chain.

The state object has a status attribute which contains the current validation status. The initial status is success. If errors are detected by validators, they set the status to something else, typically failure.

The state object also stores errors that have been detected. The setError method is used to set an error message for a particular variable. The setError method has the optional new_status argument that can be used to both set an error message as well as to update the status. You can see if an error message has already been stored for a particular variable by calling state.getError(variable_name).

The set method lets you set multiple attributes of the state object all at once, e.g.:

state.set(status='my_new_status')

You can also pass keyword arguments to the state object via the set method. These arguments will get passed along by the action. The traverse_to action places these keyword arguments in the REQUEST. The redirect_to action adds them to the query string of the URL to which it is redirecting.

Finally, we return the state object.

Another interesting example is email validation:

from Products.CMFDefault.utils import checkEmailAddress
from Products.CMFDefault.exceptions import EmailAddressInvalid

email = context.REQUEST.get('email', None)
if not email:
    state.setError('email', 'No e-mail address')
else:
# Do try-catch here because checkEmailAddress will throw an exception
# instead of saying "no, not valid".
try:
    checkEmailAddress(email)
    email_ok = True
except EmailAddressInvalid:
    email_ok = False
if not email_ok:
    state.setError('email', 'Invalid e-mail address.')

Scripts

When writing scripts that do some processing after a validated form, you can use Controller Python Scripts instead of ordinary Python Scripts to let site managers override their actions via the ZMI. On the file system, Controller Python Scripts use the extension .cpy rather than .py. Note that Controller Validators and Controller Python Scripts differ in signficant ways. Be sure to use the appropriate script type (Controller Validator or Controller Python Script) and/or the appropriate file extension (.cpy or .vpy).

Let's take a look at a basic script that sets a context attribute to the value n that is passed in via the 'REQUEST':

context.n = context.REQUEST.get('n')

# Optionally set the default next action (this can be overridden
# in the ZMI)

state.setNextAction('redirect_to:string:view')

# Optionally pass a message to display to the user
state.setKwargs({'portal_status_message':'You set context.n to %s.' % str(context.n)})
return state

Note that you will usually want to use the traverse_to action to call your script. This will ensure that form variables set in the REQUEST object are available to your script.

This script sets its action to redirect to the relative url view for the current context object. The status has not been set, so it is the default status, success.

The state.setNextAction directive above is analogous to having the following line in your .metadata file:

[actions]
action.success = redirect_to:string:view

As with the .metadata file, the default action specified in the script can be overridden via the ZMI. This allows site managers to override post-script actions without having to customize your code.

Finally, we return the state object.

Validation for Scripts

Having separate validation scripts typically means that validation is moved out of scripts. This simplifies scripts, but means that it is possible to call them directly with invalid data. We can prevent this problem by adding validators to scripts. Controller Python Scripts use the same ZMI and/or .metadata file mechanisms for adding validators as do Controller Page Templates.

Each time a validator is called, it logs the call in the state object. Validation is smart enough that if a validator is called by a form, it will not be called again by the script.

Note that if you associate validators with a script, you will need to set a sensible failure status action, since scripts do not set such an action by default. You may wish to define a different failure status for failures that occur within your script, e.g. script_failure. Then you can specify a behavior for failures that occur as a result of invalid parameters coming in and for failures that occur within the script.