Chapter 4. Web Forms
The templates that you worked with in Chapter 3 are unidirectional, in the sense that they allow information to flow from the server to the user. For most applications, however, there is also a need to have information that flows in the other direction, with the user providing data that the server accepts and processes.
With HTML, it is possible to create web forms, in which users can enter information. The form data is then submitted by the web browser to the server, typically in the form of a POST
request. The Flask request object, introduced in Chapter 2, exposes all the information sent by the client in a request and, in particular for POST
requests containing form data, provides access to the user information through request.form
.
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 the 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
Configuration
Unlike most other extensions, Flask-WTF does not need to be initialized at the application level, but it expects the application to have a secret key configured. A secret key is a string with any random and unique content that is used as an encryption or signing key to improve the security of the application in several ways. Flask uses this key to protect the contents of the user session against tampering. You should pick a different secret key in each application that you build and make sure that this string is not known by anyone. Example 4-1 shows how to configure a secret key in a Flask application.
Example 4-1. hello.py: Flask-WTF configuration
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 Flask, 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. A more practical way to manage configuration values for a larger application will be discussed in Chapter 7.
Flask-WTF requires a secret key to be configured in the application because this key is part of the mechanism the extension uses to protect all forms against cross-site request forgery (CSRF) attacks. A CSRF attack occurs when a malicious website sends requests to the application server on which the user is currently logged in. Flask-WTF generates security tokens for all forms and stores them in the user session, which is protected with a cryptographic signature generated from the secret key.
Note
For added security, the secret key should be stored in an environment variable instead of being embedded in the source code. This technique is described in Chapter 7.
Form Classes
When using Flask-WTF, each web form is represented in the server by a class that inherits from the class FlaskForm
. 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. A validator is a function that checks whether the data submitted by the user is valid.
Example 4-2 shows a simple web form that has a text field and a submit button.
Example 4-2. hello.py: form class definition
from
flask_wtf
import
FlaskForm
from
wtforms
import
StringField
,
SubmitField
from
wtforms.validators
import
DataRequired
class
NameForm
(
FlaskForm
):
name
=
StringField
(
'What is your name?'
,
validators
=
[
DataRequired
()])
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 this example, the NameForm
form has a text field called name
and a submit button called submit
. The StringField
class represents an HTML <input>
element with a type="text"
attribute. The SubmitField
class represents an HTML <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 DataRequired()
validator ensures that the field is not submitted empty.
Note
The FlaskForm
base class is defined by the Flask-WTF extension, so it is imported from flask_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 |
---|---|
|
Checkbox with |
|
Text field that accepts a |
|
Text field that accepts a |
|
Text field that accepts a |
|
File upload field |
|
Hidden text field |
|
Multiple file upload field |
|
List of fields of a given type |
|
Text field that accepts a floating-point value |
|
Form embedded as a field in a container form |
|
Text field that accepts an integer value |
|
Password text field |
|
List of radio buttons |
|
Drop-down list of choices |
|
Drop-down list of choices with multiple selection |
|
Form submission button |
|
Text field |
|
Multiple-line text field |
The list of WTForms built-in validators is shown in Table 4-2.
Validator | Description |
---|---|
|
Validates that the field contains data after type conversion |
|
Validates an email address |
|
Compares the values of two fields; useful when requesting a password to be entered twice for confirmation |
|
Validates that the field contains data before type conversion |
|
Validates an IPv4 network address |
|
Validates the length of the string entered |
|
Validates a MAC address |
|
Validates that the value entered is within a numeric range |
|
Allows empty input in the field, skipping additional validators |
|
Validates the input against a regular expression |
|
Validates a URL |
|
Validates a UUID |
|
Validates that the input is one of a list of possible values |
|
Validates that the input is none of a list of possible values |
HTML Rendering of Forms
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, used by Flask-WTF to implement CSRF protection.
Of course, the result of rendering a web form in this way is extremely bare. Any keyword arguments added to 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 for them:
<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 and make it look good is significant, so it is best to leverage Bootstrap’s own set of form styles whenever possible. The Flask-Bootstrap extension provides a 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.
Example 4-3. templates/index.html: using Flask-WTF and Flask-Bootstrap to render a form
{% 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 added to the rendered template. If the condition evaluates to False
, then what’s between the else
and endif
is rendered instead. The purpose of this is to render Hello, {{ name }}!
when the name
template variable is defined, or the string Hello, Stranger!
when it is not. The second section of the content renders the NameForm
form using the wtf.quick_form()
function.
Form Handling in View Functions
In the new version of hello.py, the index()
view function will have two tasks. First it will render the form, and then it will receive the form data entered by the user. Example 4-4 shows the updated index()
view function.
Example 4-4. hello.py: handle a web form with GET and POST request methods
@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 was accepted by all the field validators. In all other cases, validate_on_submit()
returns False
. The return value of this method effectively serves to determine 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 DataRequired()
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, so that the field is blanked when the form is rendered to the page again. 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 multiple times with different names if desired. Figure 4-2 shows the application in this state.
If the user submits the form with an empty name, the DataRequired()
validator 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 in 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 sent when they are asked to refresh a 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. For that reason, the browser asks for confirmation from the user.
Many users do not understand this warning from the browser. Consequently, it is considered good practice for web applications to never leave a POST
request as the last request sent by the browser.
This is achieved by responding to POST
requests with a redirect instead of a normal response. A redirect is a special type of response that contains a URL instead of a string with HTML code. When the browser receives a redirect response, it issues a GET
request for the redirect URL, and that is the page that it displays. 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, a 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.
Example 4-5. hello.py: 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 Flask 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()
, introduced in Chapter 3, is used.
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. The get()
method 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 always 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.
Example 4-6. hello.py: flashed messages
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 will 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.
Example 4-7. templates/base.html: rendering of flashed messages
{% 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, 2nd Edition 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.