FORM

Consider as an example a test application with the following “default.py” controller:

  1. def display_form():
  2. form = FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
  3. return dict(form=form)

and the associated “default/display_form.html” view:

  1. {{extend 'layout.html'}}
  2. <h2>Input form</h2>
  3. <form enctype="multipart/form-data"
  4. action="{{=URL()}}" method="post">
  5. Your name:
  6. <input name="name" />
  7. <input type="submit" />
  8. </form>
  9. <h2>Submitted variables</h2>
  10. {{=BEAUTIFY(request.vars)}}

This is a regular HTML form that asks for the user’s name. When you fill the form and click the submit button, the form self-submits, and the variable request.vars.name along with its provided value is displayed at the bottom.

You can generate the same form using helpers. This can be done in the view or in the action. Since web2py processed the form in the action, it is better to define the form in the action itself.

Here is the new controller:

  1. def display_form():
  2. form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
  3. return dict(form=form)

and the associated “default/display_form.html” view:

  1. {{extend 'layout.html'}}
  2. <h2>Input form</h2>
  3. {{=form}}
  4. <h2>Submitted variables</h2>
  5. {{=BEAUTIFY(request.vars)}}

The code so far is equivalent to the previous code, but the form is generated by the statement {{=form}} which serializes the FORM object.

Now we add one level of complexity by adding form validation and processing.

Change the controller as follows:

  1. def display_form():
  2. form=FORM('Your name:',
  3. INPUT(_name='name', requires=IS_NOT_EMPTY()),
  4. INPUT(_type='submit'))
  5. if form.accepts(request, session):
  6. response.flash = 'form accepted'
  7. elif form.errors:
  8. response.flash = 'form has errors'
  9. else:
  10. response.flash = 'please fill the form'
  11. return dict(form=form)

and the associated “default/display_form.html” view:

  1. {{extend 'layout.html'}}
  2. <h2>Input form</h2>
  3. {{=form}}
  4. <h2>Submitted variables</h2>
  5. {{=BEAUTIFY(request.vars)}}
  6. <h2>Accepted variables</h2>
  7. {{=BEAUTIFY(form.vars)}}
  8. <h2>Errors in form</h2>
  9. {{=BEAUTIFY(form.errors)}}

Notice that:

  • In the action, we added the requires=IS_NOT_EMPTY() validator for the input field “name”.
  • In the action, we added a call to form.accepts(..)
  • In the view, we are printing form.vars and form.errors as well as the form and request.vars.

All the work is done by the accepts method of the form object. It filters the request.vars according to the declared requirements (expressed by validators). accepts stores those variables that pass validation into form.vars. If a field value does not meet a requirement, the failing validator returns an error and the error is stored in form.errors. Both form.vars and form.errors are gluon.storage.Storage objects similar to request.vars. The former contains the values that passed validation, for example:

  1. form.vars.name = "Max"

The latter contains the errors, for example:

  1. form.errors.name = "Cannot be empty!"

The full signature of the accepts method is the following:

  1. form.accepts(vars, session=None, formname='default',
  2. keepvalues=False, onvalidation=None,
  3. dbio=True, hideerror=False):

The meaning of the optional parameters is explained in the next sub-sections.

The first argument can be request.vars or request.get_vars or request.post_vars or simply request. The latter is equivalent to accepting as input the request.post_vars.

The accepts function returns True if the form is accepted and False otherwise. A form is not accepted if it has errors or when it has not been submitted (for example, the first time it is shown).

Here is how this page looks the first time it is displayed:

image

Here is how it looks upon invalid submission:

image

Here is how it looks upon a valid submission:

image

The process and validate methods

A shortcut for

  1. form.accepts(request.post_vars, session, ...)

is

  1. form.process(...).accepted

the latter does not need the request and session arguments (although you can specify them optionally). it also differs from accepts because it returns the form itself. Internally process calls accepts and passes its arguments to it. The value returned by accepts is stored in form.accepted.

The process function takes some extra argument that accepts does not take:

  • message_onsuccess
  • onsuccess: if equal to ‘flash’ (default) and the form is accepted it will flash the above message_onsuccess
  • message_onfailure
  • onfailure: if equal to ‘flash’ (default) and the form fails validation, it will flash the above message_onfailure
  • next indicates where to redirect the user after the form is accepted.

onsuccess and onfailure can be functions like lambda form: do_something(form).

  1. form.validate(...)

is a shortcut for

  1. form.process(..., dbio=False).accepted

Conditional fields

There are times when you only want a field to show up if a condition is met. For example, consider the following model:

  1. db.define_table('purchase', Field('have_coupon', 'boolean'), Field('coupon_code'))

You want to display the field coupon_code if and only if the have_coupon field is checked. This can be done in JavaScript. web2py can help you by generating that JavaScript for you. You just need to declare that the field is conditional to an expression using the field show_if attribute:

  1. def index():
  2. db.purchase.coupon_code.show_if = (db.purchase.have_coupon==True)
  3. form = SQLFORM(db.purchase).process()
  4. return dict(form = form)

The value of show_if is a query and uses the same DAL syntax that you use for database queries. The difference is that this query is not sent to the database but it is converted to JavaScript and sent to the browser where it is executed when the user edits the form.

Hidden fields

When the above form object is serialized by {{=form}}, and because of the previous call to the accepts method, it now looks like this:

  1. <form enctype="multipart/form-data" action="" method="post">
  2. your name:
  3. <input name="name" />
  4. <input type="submit" />
  5. <input value="783531473471" type="hidden" name="_formkey" />
  6. <input value="default" type="hidden" name="_formname" />
  7. </form>

Notice the presence of two hidden fields: “_formkey” and “_formname”. Their presence is triggered by the call to accepts and they play two different and important roles:

  • The hidden field called “_formkey” is a one-time token that web2py uses to prevent double submission of forms. The value of this key is generated when the form is serialized and stored in the session. When the form is submitted this value must match, or else accepts returns False without errors as if the form was not submitted at all. This is because web2py cannot determine whether the form was submitted correctly.
  • The hidden field called “_formname” is generated by web2py as a name for the form, but the name can be overridden. This field is necessary to allow pages that contain and process multiple forms. web2py distinguishes the different submitted forms by their names.
  • Optional hidden fields specified as FORM(..., hidden=dict(...)).

The role of these hidden fields and their usage in custom forms and pages with multiple forms is discussed in more detail later in the chapter.

If the form above is submitted with an empty “name” field, the form does not pass validation. When the form is serialized again it appears as:

  1. <form enctype="multipart/form-data" action="" method="post">
  2. your name:
  3. <input value="" name="name" />
  4. <div class="error">cannot be empty!</div>
  5. <input type="submit" />
  6. <input value="783531473471" type="hidden" name="_formkey" />
  7. <input value="default" type="hidden" name="_formname" />
  8. </form>

Notice the presence of a DIV of class “error” in the serialized form. web2py inserts this error message in the form to notify the visitor about the field that did not pass validation. The accepts method, upon submission, determines that the form is submitted, checks whether the field “name” is empty and whether it is required, and eventually inserts the error message from the validator into the form.

The base “layout.html” view is expected to handle DIVs of class “error”. The default layout uses jQuery effects to make errors appear and slide down with a red background. See Chapter 11 for more details.

keepvalues

The optional argument keepvalues tells web2py what to do when a form is accepted and there is no redirection, so the same form is displayed again. By default the form is cleared. If keepvalues is set to True, the form is pre-populated with the previously inserted values. This is useful when you have a form that is supposed to be used repeatedly to insert multiple similar records.

dbio

If the dbio argument is set to False, web2py will not perform any DB insert/update after accepting form.

hideerror

If hideerror is set to True and the form contains errors, these will not be displayed when the form is rendered (it will be up to you to display them from form.errors somehow).

onvalidation

The onvalidation argument can be None or can be a function that takes the form and returns nothing. Such a function would be called and passed the form, immediately after validation (if validation passes) and before anything else happens. This function has multiple purposes: for example, to perform additional checks on the form and eventually add errors to the form, or to compute the values of some fields based on the values of other fields, or to trigger some action (like sending an email) before a record is created/updated.

Here is an example:

  1. db.define_table('numbers',
  2. Field('a', 'integer'),
  3. Field('b', 'integer'),
  4. Field('c', 'integer', readable=False, writable=False))
  5. def my_form_processing(form):
  6. c = form.vars.a * form.vars.b
  7. if c < 0:
  8. form.errors.b = 'a*b cannot be negative'
  9. else:
  10. form.vars.c = c
  11. def insert_numbers():
  12. form = SQLFORM(db.numbers)
  13. if form.process(onvalidation=my_form_processing).accepted:
  14. session.flash = 'record inserted'
  15. redirect(URL())
  16. return dict(form=form)

Detect record change

When filling a form to edit a record there is a small probability that another user may concurrently be editing the same record. So when we save the record we want to check for possible conflicts. This can be done:

  1. db.define_table('dog', Field('name'))
  2. def edit_dog():
  3. dog = db.dog(request.args(0)) or redirect(URL('error'))
  4. form=SQLFORM(db.dog, dog)
  5. form.process(detect_record_change=True)
  6. if form.record_changed:
  7. # do something
  8. elif form.accepted:
  9. # do something else
  10. else:
  11. # do nothing
  12. return dict(form=form)

record_changed works only with a SQLFORM and not with a FORM.

Forms and redirection

The most common way to use forms is via self-submission, so that the submitted field variables are processed by the same action that generated the form. Once the form is accepted, it is unusual to display the current page again (something we are doing here only to keep things simple). It is more common to redirect the visitor to a “next” page.

Here is the new example controller:

  1. def display_form():
  2. form = FORM('Your name:',
  3. INPUT(_name='name', requires=IS_NOT_EMPTY()),
  4. INPUT(_type='submit'))
  5. if form.process().accepted:
  6. session.flash = 'form accepted'
  7. redirect(URL('next'))
  8. elif form.errors:
  9. response.flash = 'form has errors'
  10. else:
  11. response.flash = 'please fill the form'
  12. return dict(form=form)
  13. def next():
  14. return dict()

In order to set a flash on the next page instead of the current page you must use session.flash instead of response.flash. web2py moves the former into the latter after redirection. Note that using session.flash requires that you do not session.forget().

Multiple forms per page

The content of this section applies to both FORM and SQLFORM objects. It is possible to have multiple forms per page, but you must allow web2py to distinguish them. If these are derived by SQLFORM from different tables, then web2py gives them different names automatically; otherwise you need to explicitly give them different form names. Here is an example:

  1. def two_forms():
  2. form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
  3. INPUT(_type='submit'), _name='form_one')
  4. form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
  5. INPUT(_type='submit'), _name='form_two')
  6. if form1.process(formname='form_one').accepted:
  7. response.flash = 'form one accepted'
  8. if form2.process(formname='form_two').accepted:
  9. response.flash = 'form two accepted'
  10. return dict(form1=form1, form2=form2)

and here is the output it produces:

image

When the visitor submits an empty form1, only form1 displays an error; if the visitor submits an empty form2, only form2 displays an error message.

Sharing forms

The content of this section applies to both FORM and SQLFORM objects. What we discuss here is possible but not recommended, since it is always good practice to have forms that self-submit. Sometimes, though, you don’t have a choice, because the action that sends the form and the action that receives it belong to different applications.

It is possible to generate a form that submits to a different action. This is done by specifying the URL of the processing action in the attributes of the FORM or SQLFORM object. For example:

  1. form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
  2. INPUT(_type='submit'), _action=URL('page_two'))
  3. def page_one():
  4. return dict(form=form)
  5. def page_two():
  6. if form.process(session=None, formname=None).accepted:
  7. response.flash = 'form accepted'
  8. else:
  9. response.flash = 'there was an error in the form'
  10. return dict()

Notice that since both “page_one” and “page_two” use the same form, we have defined it only once by placing it outside of all the actions, in order not to repeat ourselves. The common portion of code at the beginning of a controller gets executed every time before giving control to the called action.

Since “page_one” does not call process (nor accepts), the form has no name and no key, so you must pass session=None and set formname=None in process, or the form will not validate when “page_two” receives it.

Adding buttons to FORMs

Usually a form provides a single submit button. It is common to want to add a “back” button that instead of submitting the form, directs the visitor to a different page.

This can be done with the add_button method:

  1. form.add_button('Back', URL('other_page'))

You can add more than one button to form. The arguments of add_button are the value of the button (its text) and the url where to redirect to. (See also the buttons argument for SQLFORM, which provides a more powerful approach)

More about manipulation of FORMs

As discussed in the Views chapter, a FORM is an HTML helper. Helpers can be manipulated as Python lists and as dictionaries, which enables run-time creation and modification.