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.

Table 4-1. WTForms standard HTML fields
Field type Description

BooleanField

Checkbox with True and False values

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

DecimalField

Text field that accepts a decimal.Decimal value

FileField

File upload field

HiddenField

Hidden text field

MultipleFileField

Multiple file upload field

FieldList

List of fields of a given type

FloatField

Text field that accepts a floating-point value

FormField

Form embedded as a field in a container form

IntegerField

Text field that accepts an integer value

PasswordField

Password text field

RadioField

List of radio buttons

SelectField

Drop-down list of choices

SelectMultipleField

Drop-down list of choices with multiple selection

SubmitField

Form submission button

StringField

Text field

TextAreaField

Multiple-line text field

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

Table 4-2. WTForms validators
Validator Description

DataRequired

Validates that the field contains data after type conversion

Email

Validates an email address

EqualTo

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

InputRequired

Validates that the field contains data before type conversion

IPAddress

Validates an IPv4 network address

Length

Validates the length of the string entered

MacAddress

Validates a MAC address

NumberRange

Validates that the value entered is within a numeric range

Optional

Allows empty input in the field, skipping additional validators

Regexp

Validates the input against a regular expression

URL

Validates a URL

UUID

Validates a UUID

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

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

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 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">&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, 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.