Chapter 4. Web Forms
The request object, introduced in Chapter 2, exposes all the information sent by the client with a request. In particular, request.form
provides access to form data submitted in POST
requests.
Although the support provided in Flask’s request object is sufficient for the handling of web forms, there are a number of tasks that can become tedious and repetitive. Two good examples are the generation of HTML code for forms and the validation of the submitted form data.
The Flask-WTF extension makes working with web forms a much more pleasant experience. This extension is a Flask integration wrapper around the framework-agnostic WTForms package.
Flask-WTF and its dependencies can be installed with pip
:
(
venv)
$
pip install flask-wtf
By default, Flask-WTF protects all forms against Cross-Site Request Forgery (CSRF) attacks. A CSRF attack occurs when a malicious website sends requests to a different website on which the victim is logged in.
To implement CSRF protection, Flask-WTF needs the application to configure an encryption key. Flask-WTF uses this key to generate encrypted tokens that are used to verify the authenticity of requests with form data. Example 4-1 shows how to configure an encryption key.
app
=
Flask
(
__name__
)
app
.
config
[
'SECRET_KEY'
]
=
'hard to guess string'
The app.config
dictionary is a general-purpose place to store configuration variables used by the framework, the extensions, or the application itself. Configuration values can be added to the app.config
object using standard dictionary syntax. The configuration object also has methods to import configuration values from files or the environment.
The SECRET_KEY
configuration variable is used as a general-purpose encryption key by Flask and several third-party extensions. As its name implies, the strength of the encryption depends on the value of this variable being secret. Pick a different secret key in each application that you build and make sure that this string is not known by anyone.
Note
For added security, the secret key should be stored in an environment variable instead of being embedded in the code. This technique is described in Chapter 7.
Form Classes
When using Flask-WTF, each web form is represented by a class that inherits from class Form
. The class defines the list of fields in the form, each represented by an object. Each field object can have one or more validators attached; validators are functions that check whether the input submitted by the user is valid.
Example 4-2 shows a simple web form that has a text field and a submit button.
from
flask.ext.wtf
import
Form
from
wtforms
import
StringField
,
SubmitField
from
wtforms.validators
import
Required
class
NameForm
(
Form
):
name
=
StringField
(
'What is your name?'
,
validators
=
[
Required
()])
submit
=
SubmitField
(
'Submit'
)
The fields in the form are defined as class variables, and each class variable is assigned an object associated with the field type. In the previous example, the NameForm
form has a text field called name
and a submit button called submit
. The StringField
class represents an <input>
element with a type="text"
attribute. The SubmitField
class represents an <input>
element with a type="submit"
attribute. The first argument to the field constructors is the label that will be used when rendering the form to HTML.
The optional validators
argument included in the StringField
constructor defines a list of checkers that will be applied to the data submitted by the user before it is accepted. The Required()
validator ensures that the field is not submitted empty.
Note
The Form
base class is defined by the Flask-WTF extension, so it is imported from flask.ext.wtf
. The fields and validators, however, are imported directly from the WTForms package.
The list of standard HTML fields supported by WTForms is shown in Table 4-1.
Field type | Description |
| Text field |
| Multiple-line text field |
| Password text field |
| Hidden text field |
| Text field that accepts a |
| Text field that accepts a |
| Text field that accepts an integer value |
| Text field that accepts a |
| Text field that accepts a floating-point value |
| Checkbox with |
| List of radio buttons |
| Drop-down list of choices |
| Drop-down list of choices with multiple selection |
| File upload field |
| Form submission button |
| Embed a form as a field in a container form |
| List of fields of a given type |
The list of WTForms built-in validators is shown in Table 4-2.
Validator | Description |
| Validates an email address |
| Compares the values of two fields; useful when requesting a password to be entered twice for confirmation |
| Validates an IPv4 network address |
| Validates the length of the string entered |
| Validates that the value entered is within a numeric range |
| Allows empty input on the field, skipping additional validators |
| Validates that the field contains data |
| Validates the input against a regular expression |
| Validates a URL |
| Validates that the input is one of a list of possible values |
| Validates that the input is none of a list of possible values |
Form fields are callables that, when invoked from a template, render themselves to HTML. Assuming that the view function passes a NameForm
instance to the template as an argument named form
, the template can generate a simple HTML form as follows:
<form
method=
"POST"
>
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name() }} {{ form.submit() }}</form>
Note that in addition to the name
and submit
fields, the form has a form.hidden_tag()
element. This element defines an extra form field that is hidden from view, used by Flask-WTF to implement CSRF protection.
Of course, the result is extremely bare. To improve the look of the form, any arguments sent into the calls that render the fields are converted into HTML attributes for the field; so, for example, you can give the field id
or class
attributes and then define CSS styles:
<form
method=
"POST"
>
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name(id='my-text-field') }} {{ form.submit() }}</form>
But even with HTML attributes, the effort required to render a form in this way is significant, so it is best to leverage Bootstrap’s own set of form styles whenever possible. Flask-Bootstrap provides a very high-level helper function that renders an entire Flask-WTF form using Bootstrap’s predefined form styles, all with a single call. Using Flask-Bootstrap, the previous form can be rendered as follows:
{% import "bootstrap/wtf.html" as wtf %} {{ wtf.quick_form(form) }}
The import
directive works in the same way as regular Python scripts do and allows template elements to be imported and used in many templates. The imported bootstrap/wtf.html file defines helper functions that render Flask-WTF forms using Bootstrap. The wtf.quick_form()
function takes a Flask-WTF form object and renders it using default Bootstrap styles. The complete template for hello.py is shown in Example 4-3.
{% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Flasky{% endblock %} {% block page_content %}<div
class=
"page-header"
>
<h1>
Hello, {% if name %}{{ name }}{% else %}Stranger{% endif %}!</h1>
</div>
{{ wtf.quick_form(form) }} {% endblock %}
The content area of the template now has two sections. The first section is a page header that shows a greeting. Here a template conditional is used. Conditionals in Jinja2 have the format {% if condition %}...{% else %}...{% endif %}
. If the condition evaluates to True
, then what appears between the if
and else
directives is rendered to the template. If the condition evaluates to False
, then what’s between the else
and endif
is rendered. The example template will render the string “Hello, Stranger!” when the name
template argument is undefined. The second section of the content renders the NameForm
object using the wtf.quick_form()
function.
Form Handling in View Functions
In the new version of hello.py, the index()
view function will be rendering the form and also receiving its data. Example 4-4 shows the updated index()
view function.
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
name
=
None
form
=
NameForm
()
if
form
.
validate_on_submit
():
name
=
form
.
name
.
data
form
.
name
.
data
=
''
return
render_template
(
'index.html'
,
form
=
form
,
name
=
name
)
The methods
argument added to the app.route
decorator tells Flask to register the view function as a handler for GET
and POST
requests in the URL map. When methods
is not given, the view function is registered to handle GET
requests only.
Adding POST
to the method list is necessary because form submissions are much more conveniently handled as POST
requests. It is possible to submit a form as a GET
request, but as GET
requests have no body, the data is appended to the URL as a query string and becomes visible in the browser’s address bar. For this and several other reasons, form submissions are almost universally done as POST
requests.
The local name
variable is used to hold the name received from the form when available; when the name is not known the variable is initialized to None
. The view function creates an instance of the NameForm
class shown previously to represent the form. The validate_on_submit()
method of the form returns True
when the form was submitted and the data has been accepted by all the field validators. In all other cases, validate_on_submit()
returns False
. The return value of this method effectively serves to decide whether the form needs to be rendered or processed.
When a user navigates to the application for the first time, the server will receive a GET
request with no form data, so validate_on_submit()
will return False
. The body of the if
statement will be skipped and the request will be handled by rendering the template, which gets the form object and the name
variable set to None
as arguments. Users will now see the form displayed in the browser.
When the form is submitted by the user, the server receives a POST
request with the data. The call to validate_on_submit()
invokes the Required()
validator attached to the name field. If the name is not empty, then the validator accepts it and validate_on_submit()
returns True
. Now the name entered by the user is accessible as the data
attribute of the field. Inside the body of the if
statement, this name is assigned to the local name
variable and the form field is cleared by setting that data
attribute to an empty string. The render_template()
call in the last line renders the template, but this time the name
argument contains the name from the form, so the greeting will be personalized.
Tip
If you have cloned the application’s Git repository on GitHub, you can run git checkout 4a
to check out this version of the application.
Figure 4-1 shows how the form looks in the browser window when a user initially enters the site. When the user submits a name, the application responds with a personalized greeting. The form still appears below it, so a user can submit it with a new name if desired. Figure 4-2 shows the application in this state.
If the user submits the form with an empty name, the Required()
validatior catches the error, as seen in Figure 4-3. Note how much functionality is being provided automatically. This is a great example of the power that well-designed extensions like Flask-WTF and Flask-Bootstrap can give to your application.
Redirects and User Sessions
The last version of hello.py has a usability problem. If you enter your name and submit it and then click the refresh button on your browser, you will likely get an obscure warning that asks for confirmation before submitting the form again. This happens because browsers repeat the last request they have sent when they are asked to refresh the page. When the last request sent is a POST
request with form data, a refresh would cause a duplicate form submission, which in almost all cases is not the desired action.
Many users do not understand the warning from the browser. For this reason, it is considered good practice for web applications to never leave a POST
request as a last request sent by the browser.
This practice can be achieved by responding to POST
requests with a redirect instead of a normal response. A redirect is a special type of response that has a URL instead of a string with HTML code. When the browser receives this response, it issues a GET
request for the redirect URL, and that is the page that is displayed. The page may take a few more milliseconds to load because of the second request that has to be sent to the server, but other than that, the user will not see any difference. Now the last request is a GET
, so the refresh command works as expected. This trick is known as the Post/Redirect/Get pattern.
But this approach brings a second problem. When the application handles the POST
request, it has access to the name entered by the user in form.name.data
, but as soon as that request ends the form data is lost. Because the POST
request is handled with a redirect, the application needs to store the name so that the redirected request can have it and use it to build the actual response.
Applications can “remember” things from one request to the next by storing them in the user session, private storage that is available to each connected client. The user session was introduced in Chapter 2 as one of the variables associated with the request context. It’s called session
and is accessed like a standard Python dictionary.
Note
By default, user sessions are stored in client-side cookies that are cryptographically signed using the configured SECRET_KEY
. Any tampering with the cookie content would render the signature invalid, thus invalidating the session.
Example 4-5 shows a new version of the index()
view function that implements redirects and user sessions.
from
flask
import
Flask
,
render_template
,
session
,
redirect
,
url_for
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
form
=
NameForm
()
if
form
.
validate_on_submit
():
session
[
'name'
]
=
form
.
name
.
data
return
redirect
(
url_for
(
'index'
))
return
render_template
(
'index.html'
,
form
=
form
,
name
=
session
.
get
(
'name'
))
In the previous version of the application, a local name
variable was used to store the name entered by the user in the form. That variable is now placed in the user session as session['name']
so that it is remembered beyond the request.
Requests that come with valid form data will now end with a call to redirect()
, a helper function that generates the HTTP redirect response. The redirect()
function takes the URL to redirect to as an argument. The redirect URL used in this case is the root URL, so the response could have been written more concisely as redirect('/')
, but instead Flask’s URL generator function url_for()
is used. The use of url_for()
to generate URLs is encouraged because this function generates URLs using the URL map, so URLs are guaranteed to be compatible with defined routes and any changes made to route names will be automatically available when using this function.
The first and only required argument to url_for()
is the endpoint name, the internal name each route has. By default, the endpoint of a route is the name of the view function attached to it. In this example, the view function that handles the root URL is index()
, so the name given to url_for()
is index
.
The last change is in the render_template()
function, which now obtains the name
argument directly from the session using session.get('name')
. As with regular dictionaries, using get()
to request a dictionary key avoids an exception for keys that aren’t found, because get()
returns a default value of None
for a missing key.
Tip
If you have cloned the application’s Git repository on GitHub, you can run git checkout 4b
to check out this version of the application.
With this version of the application, you can see that refreshing the page in your browser results in the expected behavior.
Message Flashing
Sometimes it is useful to give the user a status update after a request is completed. This could be a confirmation message, a warning, or an error. A typical example is when you submit a login form to a website with a mistake and the server responds by rendering the login form again with a message above it that informs you that your username or password is invalid.
Flask includes this functionality as a core feature. Example 4-6 shows how the flash()
function can be used for this purpose.
from
flask
import
Flask
,
render_template
,
session
,
redirect
,
url_for
,
flash
@app.route
(
'/'
,
methods
=
[
'GET'
,
'POST'
])
def
index
():
form
=
NameForm
()
if
form
.
validate_on_submit
():
old_name
=
session
.
get
(
'name'
)
if
old_name
is
not
None
and
old_name
!=
form
.
name
.
data
:
flash
(
'Looks like you have changed your name!'
)
session
[
'name'
]
=
form
.
name
.
data
return
redirect
(
url_for
(
'index'
))
return
render_template
(
'index.html'
,
form
=
form
,
name
=
session
.
get
(
'name'
))
In this example, each time a name is submitted it is compared against the name stored in the user session, which would have been put there during a previous submission of the same form. If the two names are different, the flash()
function is invoked with a message to be displayed on the next response sent back to the client.
Calling flash()
is not enough to get messages displayed; the templates used by the application need to render these messages. The best place to render flashed messages is the base template, because that will enable these messages in all pages. Flask makes a get_flashed_messages()
function available to templates to retrieve the messages and render them, as shown in Example 4-7.
{% block content %}<div
class=
"container"
>
{% for message in get_flashed_messages() %}<div
class=
"alert alert-warning"
>
<button
type=
"button"
class=
"close"
data-dismiss=
"alert"
>
×
</button>
{{ message }}</div>
{% endfor %} {% block page_content %}{% endblock %}</div>
{% endblock %}
In this example, messages are rendered using Bootstrap’s alert CSS styles for warning messages (one is shown in Figure 4-4).
A loop is used because there could be multiple messages queued for display, one for each time flash()
was called in the previous request cycle. Messages that are retrieved from get_flashed_messages()
will not be returned the next time this function is called, so flashed messages appear only once and are then discarded.
Tip
If you have cloned the application’s Git repository on GitHub, you can run git checkout 4c
to check out this version of the application.
Being able to accept data from the user through web forms is a feature required by most applications, and so is the ability to store that data in permanent storage. Using databases with Flask is the topic of the next chapter.
Get Flask Web Development now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.