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

Cross-Site Request Forgery (CSRF) Protection

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.

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 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.

Example 4-2. hello.py: Form class definition
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.

Table 4-1. WTForms standard HTML fields
Field typeDescription

StringField

Text field

TextAreaField

Multiple-line text field

PasswordField

Password text field

HiddenField

Hidden text field

DateField

Text field that accepts a datetime.date value in a given format

DateTimeField

Text field that accepts a datetime.datetime value in a given format

IntegerField

Text field that accepts an integer value

DecimalField

Text field that accepts a decimal.Decimal value

FloatField

Text field that accepts a floating-point value

BooleanField

Checkbox with True and False values

RadioField

List of radio buttons

SelectField

Drop-down list of choices

SelectMultipleField

Drop-down list of choices with multiple selection

FileField

File upload field

SubmitField

Form submission button

FormField

Embed a form as a field in a container form

FieldList

List of fields of a given type

The list of WTForms built-in validators is shown in Table 4-2.

Table 4-2. WTForms validators
ValidatorDescription

Email

Validates an email address

EqualTo

Compares the values of two fields; useful when requesting a password to be entered twice for confirmation

IPAddress

Validates an IPv4 network address

Length

Validates the length of the string entered

NumberRange

Validates that the value entered is within a numeric range

Optional

Allows empty input on the field, skipping additional validators

Required

Validates that the field contains data

Regexp

Validates the input against a regular expression

URL

Validates a URL

AnyOf

Validates that the input is one of a list of possible values

NoneOf

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 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.

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 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.

Example 4-4. hello.py: Route 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 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.

Flask-WTF web form
Figure 4-1. Flask-WTF web form
Web form after submission
Figure 4-2. Web form after submission

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.

Web form after failed validator
Figure 4-3. Web form after failed validator

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.

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 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.

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 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.

Example 4-7. templates/base.html: Flash message rendering
{% block content %}
<div class="container">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
        <button type="button" class="close" data-dismiss="alert">&times;</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).

Flashed message
Figure 4-4. Flashed message

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.