FORM
Consider as an example a test application with the following “default.py” controller:
def display_form():
form = FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
return dict(form=form)
and the associated “default/display_form.html” view:
{{extend 'layout.html'}}
<h2>Input form</h2>
<form enctype="multipart/form-data"
action="{{=URL()}}" method="post">
Your name:
<input name="name" />
<input type="submit" />
</form>
<h2>Submitted variables</h2>
{{=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:
def display_form():
form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
return dict(form=form)
and the associated “default/display_form.html” view:
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=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:
def display_form():
form=FORM('Your name:',
INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.accepts(request, session):
response.flash = 'form accepted'
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill the form'
return dict(form=form)
and the associated “default/display_form.html” view:
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Accepted variables</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Errors in form</h2>
{{=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
andform.errors
as well as the form andrequest.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:
form.vars.name = "Max"
The latter contains the errors, for example:
form.errors.name = "Cannot be empty!"
The full signature of the accepts
method is the following:
form.accepts(vars, session=None, formname='default',
keepvalues=False, onvalidation=None,
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:
Here is how it looks upon invalid submission:
Here is how it looks upon a valid submission:
The process
and validate
methods
A shortcut for
form.accepts(request.post_vars, session, ...)
is
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 abovemessage_onsuccess
message_onfailure
onfailure
: if equal to ‘flash’ (default) and the form fails validation, it will flash the abovemessage_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)
.
form.validate(...)
is a shortcut for
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:
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:
def index():
db.purchase.coupon_code.show_if = (db.purchase.have_coupon==True)
form = SQLFORM(db.purchase).process()
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:
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</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 elseaccepts
returnsFalse
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:
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</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:
db.define_table('numbers',
Field('a', 'integer'),
Field('b', 'integer'),
Field('c', 'integer', readable=False, writable=False))
def my_form_processing(form):
c = form.vars.a * form.vars.b
if c < 0:
form.errors.b = 'a*b cannot be negative'
else:
form.vars.c = c
def insert_numbers():
form = SQLFORM(db.numbers)
if form.process(onvalidation=my_form_processing).accepted:
session.flash = 'record inserted'
redirect(URL())
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:
db.define_table('dog', Field('name'))
def edit_dog():
dog = db.dog(request.args(0)) or redirect(URL('error'))
form=SQLFORM(db.dog, dog)
form.process(detect_record_change=True)
if form.record_changed:
# do something
elif form.accepted:
# do something else
else:
# do nothing
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:
def display_form():
form = FORM('Your name:',
INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'))
if form.process().accepted:
session.flash = 'form accepted'
redirect(URL('next'))
elif form.errors:
response.flash = 'form has errors'
else:
response.flash = 'please fill the form'
return dict(form=form)
def next():
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:
def two_forms():
form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'), _name='form_one')
form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'), _name='form_two')
if form1.process(formname='form_one').accepted:
response.flash = 'form one accepted'
if form2.process(formname='form_two').accepted:
response.flash = 'form two accepted'
return dict(form1=form1, form2=form2)
and here is the output it produces:
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:
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
INPUT(_type='submit'), _action=URL('page_two'))
def page_one():
return dict(form=form)
def page_two():
if form.process(session=None, formname=None).accepted:
response.flash = 'form accepted'
else:
response.flash = 'there was an error in the form'
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:
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.