Web Forms (2024 Edition)

This is the third part of the Flask mega tutorial series in which I'm going to tell you how to work with web forms.

Table of contents
  • Chapter 1: Hello world!

  • Chapter 2: Templates

  • Chapter 3: Web Forms (This article)

  • Chapter 4: Database

  • Chapter 5: User Logins

  • Chapter 6: Profile Page and Avatars

  • Chapter 7: Error Handling

  • Chapter 8: Followers

  • Chapter 9: Pagination

  • Chapter 10: Email Support

  • Chapter 11: Facelift

  • Chapter 12: Dates and Times

  • Chapter 13: I18n and L10n

  • Chapter 14: Ajax

  • Chapter 15: Improved Application Structure

  • Chapter 16: Full Text Search

  • Chapter 17: Linux Deployment

  • Chapter 18: Deployment to Heroku

  • Chapter 19: Deploying to Docker Containers

  • Chapter 20: A Little JavaScript Magic

  • Chapter 21: User Notifications

  • Chapter 22: Background Jobs

  • Chapter 23: Application Programming Interfaces (APIs)

IN Chapter 2 I created a simple template for the app's home page and used fake objects as placeholders in place of things I don't already have, such as users and blog posts. In this chapter, I'm going to address one of the many shortcomings I still have with this application, specifically how to accept input from users via web forms.

Web forms are one of the most basic building blocks in any web application. I'll be using forms to allow users to post to the blog and also to login to the application.

Before you begin this chapter, make sure you have installed the application microblog as I left it in the previous chapter and that you can run it without any errors.

GitHub links for this chapter: Browse, Zip, Diff.

Introduction to Flask-WTF

To work with web forms in this application I am going to use the extension Flask-WTFwhich is a thin shell around the package WTForms, which integrates it nicely with Flask. This is the first Flask extension I'm introducing to you, but it certainly won't be the last. Extensions are a very important part of the Flask ecosystem because they provide solutions to problems that Flask is deliberately silent on.

Flask extensions are regular Python packages that are installed using pip. You can go ahead and install Flask-WTF in your virtual environment:

(venv) $ pip install flask-wtf

So far the application is very simple and for this reason I did not need to worry about it configurations. But for any but the simplest applications, you'll find that Flask (and perhaps also the Flask extensions you use) offer some leeway and you need to make some decisions, which you pass to the framework as a list of configuration variables.

There are several formats for specifying configuration parameters for an application. The simplest solution is to define your variables as keys in app.config, which uses a dictionary style for working with variables. For example, you could do something like this:

app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed

While the above syntax is sufficient for creating configuration options for Flask, I prefer to use the principle division of tasksso instead of putting my configuration in the same place where I build my application, I'll use a slightly more complex structure that allows me to store my configuration in a separate file.

A solution that I really like because it's very extensible is to use a Python class to store configuration variables. To keep everything nicely organized, I'm going to create a configuration class in a separate Python module. Below you can see the new Config class for this application stored in a module config.py in the top level directory.

config.py:Setting a secret key

import os

class Config:
  SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

Pretty simple, right? Configuration options are defined as class variables within a class Config. As the application requires more configuration items, they can be added to this class, and later if I find that I need to have more than one set of configuration, I can subclass it. But don't worry about it just yet.

Configuration Variable SECRET_KEY, which I added as the only configuration item, is an essential part of most Flask applications. Flask and some of its extensions use the secret key value as a cryptographic key useful for generating signatures or tokens. The Flask-WTF extension uses it to protect web forms from a malicious attack called cross-site request forgery or CSRF (pronounced “seasurf”). As the name suggests, the private key must be secret because the strength of the tokens and signatures generated with it depends on the fact that no one other than the application's trusted maintainers knows about it.

The secret key value is specified as a two-object expression concatenated with the operator or. The first object looks up the value of an environment variable, also called SECRET_KEY. The second object is just a hardcoded string. You'll see that I repeat this pattern often for configuration variables. The idea is that the value derived from the environment variable is preferred, but if the environment does not define a variable, then the hardcoded string is used by default. Security requirements are low when developing this application, so you can simply ignore this option and allow the hardcoded string to be used. But when this application is deployed to the production server, I will set a unique and hard to guess value in the environment so that the server has a secure key that no one else knows.

Now that I have the config file, I need to tell Flask to read it and apply it. This can be done immediately after instantiating the Flask application using the method app.config.from_object():

app/init.py: Setting up Flask

from flask import Flask
from config import Config

app = Flask(__name__)
app.config.from_object(Config)

from app import routes

The way I import the class Configmay seem confusing at first glance, but if you look at how the class Flask (capital “F”) imported from package flask (lowercase “f”), you'll notice that I do the same thing with the configuration. The lowercase “config” is the name of the Python module config.pyand obviously one where the capital “C” is the actual class.

As I mentioned above, configuration items can be accessed using dictionary syntax from app.config. Here you can see a quick Python session where I check what the value of the secret key is:

>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'

User login form

The Flask-WTF extension uses Python classes to represent web forms. The Form class simply defines the form fields as class variables.

Once again keeping separation of concerns in mind, I'm going to use the new module app/forms.py to store my web forms classes. First, let's define a user login form that asks the user to enter a username and password. The form will also include a “remember me” checkbox and a submit button:

app/forms.py: Login form

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
  username = StringField('Username', validators=[DataRequired()])
  password = PasswordField('Password', validators=[DataRequired()])
  remember_me = BooleanField('Remember Me')
  submit = SubmitField('Sign In')

Most Flask extensions use the convention flask_<name> about naming for top-level import objects. In this case, Flask-WTF contains all its objects under flask_wtf. Here at the top FlaskForm base class is imported app/forms.py.

The four classes representing the field types I'm using for this form are imported directly from the WTForms package, since the Flask-WTF extension doesn't provide custom versions. For each field an object is created as a class variable in the class LoginForm. Each field is given a description or label as the first argument.

Optional argument validators, which you see in some fields, is used to bind validation behavior to the fields. Checker DataRequired it simply checks that the field is not sent empty. There are many more validators available, some of which will be used in other forms.

Form templates

The next step is to add the form to the HTML template so that it can be displayed on the web page. The good news is that the fields defined in the class LoginForm, know how to display themselves in HTML format, so this task is quite easy. Below you can see the login template that I am going to save in a file app/templates/login.html:

app/templates/login.html: Login form template

{% extends "base.html" %}

{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" novalidate>
    {{ form.hidden_tag() }}
    <p>
      {{ form.username.label }}<br>
      {{ form.username(size=32) }}
    </p>
    <p>
      {{ form.password.label }}<br>
      {{ form.password(size=32) }}
    </p>
    <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
    <p>{{ form.submit() }}</p>
  </form>
{% endblock %}

For this template I am reusing the template base.htmlas shown in Chapter 2using the template inheritance instruction extends . In fact, I'll do this with all the templates to ensure a consistent layout that includes a top navigation bar across all pages of the app.

This template expects a form object created from the class LoginFormwill be given as an argument, which as you can see is given as form. This argument will be sent by the login view function, which I haven't written yet.

HTML element <form> used as a container for a web form. Form attribute action is used to tell the browser the URL to use when submitting information entered by the user into a form. When an action is set to an empty string, the form is submitted to the URL that is currently in the address bar, which is the URL that displayed the form on the page. Attribute method defines the HTTP request method to use when submitting the form to the server. By default it is sent with the request GETbut in almost all cases the use of the query POST improves the user experience because requests of this type can submit form data in the request body, while requests GET add form fields to the URL, cluttering the browser's address bar. Attribute novalidate is used to instruct the web browser not to apply validation to fields on this form, effectively leaving that task to the Flask application running on the server. Usage novalidate It's completely optional, but it's important to set it up for this first form because it will allow you to test server-side validation later in the chapter.

Template parameter form.hidden_tag() generates a hidden field that includes a token that is used to protect the form from CSRF attacks. All you have to do to secure the form is enable this hidden field and set SECRET_KEY a variable defined in the Flask configuration. If you take care of these two things, Flask-WTF will do the rest for you.

If you've written HTML web forms in the past, you may have found it strange that there are no HTML fields in this template. This is because the fields from the form object know how to render themselves in HTML format. All I had to do was turn on {{ form.<field_name>.label }} where I wanted the field label, and {{ form.<field_name>() }} where I wanted the field. For fields that require additional HTML attributes, they can be passed as arguments. The username and password fields in this template accept size as an argument to be added to the HTML element <input> as an attribute. In the same way, you can also attach CSS classes or IDs to form fields.

Form views

The last step before you can see this form in the browser is to write a new view function in the application that displays the template from the previous section.

So let's write a new view function mapped to a URL /login, which creates the form and passes it to the template for rendering. This view feature can also be included in the module app/routes.py along with the previous one:

app/routes.py: Login view function

from flask import render_template
from app import app
from app.forms import LoginForm
# ...
@app.route("https://habr.com/login")
def login():
  form = LoginForm()
  return render_template('login.html', title="Sign In", form=form)

What I did here is import the class LoginForm from forms.py, created an object instance from it and sent it to the template. Syntax form=form may seem strange, but it just passes the object formcreated on the line above (and shown on the right), into a template named form (shown on the left). This is all that is required to display the form fields.

To make the login form easier to access, the base template can extend the element <div> V base.htmlto include a link to it in the navigation bar:

app/templates/base.html: Login link in navigation bar

<div>
  Microblog:
  <a href="https://habr.com/index">Home</a>
  <a href="https://habr.com/login">Login</a>
</div>

At this point, you can launch the application and view the form in your web browser. When the application is running, enter http://localhost:5000/ in your browser's address bar, and then click the “Sign In” link in the top navigation bar to see your new login form. Pretty cool, right?

Retrieving form data

If you try to click the submit button, the browser will display a “Method not allowed” error. This is because the login view function from the previous section is only doing half the work for now. It can display a form on a web page, but does not yet have the logic to process data submitted by the user. This is another area where Flask-WTF really makes things easier. Here's an updated version of the view function that accepts and validates user-supplied data:

app/routes.py: Obtaining login credentials

from flask import render_template, flash, redirect

@app.route("https://habr.com/login", methods=['GET', 'POST'])
def login():
  form = LoginForm()
  if form.validate_on_submit():
    flash('Login requested for user {}, remember_me={}'.format(
      form.username.data, form.remember_me.data))
    return redirect("https://habr.com/index")
  return render_template('login.html', title="Sign In", form=form)

The first new thing in this version is the argument methods in the route decorator. It tells Flask that this view function is accepting requests GET And POSToverriding the default which should only accept requests GET. The HTTP protocol states that requests GET are the ones that return information to the client (in this case, the web browser). All requests in the application are currently of this type. Requests POST typically used when the browser sends form data to the server (actually requests GET can also be used for this purpose, but it is not a recommended practice). The “Method not allowed” error that the browser showed you earlier is because the browser was trying to send a request POST, and the application was not configured to accept it. Providing an argument methodsyou tell Flask which request methods should be accepted.

Method form.validate_on_submit() does all the form processing work. When the browser sends a request GET to get a web page with a form, this method will return Falseso in this case the function skips the if statement and goes directly to displaying the template on the last line of the function.

When the browser sends a request POST as a result of the user clicking the submit button, form.validate_on_submit() is going to collect all the data, run all the validators attached to the fields, and if everything is ok, it will return True, indicating that the data is valid and can be processed by the application. But if at least one field fails the test, the function will return Falseand this will cause the form to be returned to the user, as is the case with the request GET. Later I'm going to add an error message when validation fails.

Upon return form.validate_on_submit() values True The login view function calls two new functions imported from Flask. Function flash() – a useful way to show a message to the user. Many applications use this method to tell the user whether some action was successful or not. In this case, I'm going to use this mechanism as a temporary solution because I don't have all the infrastructure needed to actually log users in yet. The best I can do at the moment is to show a message confirming that the application has received the credentials.

The second new feature used in the Login View feature is redirect(). This function instructs the client's web browser to automatically navigate to another page specified as an argument. This browsing feature uses it to redirect the user to the app's index page.

When you call a function flash()Flask saves the message, but the displayed messages don't magically appear on web pages. Application templates must render these flash messages in a way that matches the site's layout. I'm going to add these messages to the base template so that all templates will inherit this functionality. This is the updated base template:

app/templates/base.html: Display messages in the base template

<html>
  <head>
    {% if title %}
      <title>{{ title }} - microblog</title>
    {% else %}
      <title>microblog</title>
    {% endif %}
  </head>
  <body>
    <div>
      Microblog:
      <a href="https://habr.com/index">Home</a>
      <a href="https://habr.com/login">Login</a>
    </div>
    <hr>
    {% with messages = get_flashed_messages() %}
      {% if messages %}
        <ul>
          {% for message in messages %}
            <li>{{ message }}</li>
          {% endfor %}
        </ul>
      {% endif %}
    {% endwith %}
    {% block content %}{% endblock %}
  </body>
</html>

Here I am using the construct with to assign the result of the call get_flashed_messages() variable messages, and all this in the context of a template. Function get_flashed_messages() comes from Flask and returns a list of all messages that were previously logged in flash(). The following condition checks whether messages some content, in which case the element <ul> appears with each message as a list item <li>. This rendering style is not very suitable for status messages, but the topic of styling a web application will be covered later.

An interesting property of these display messages is that after a single request through the function get_flashed_messages they are removed from the message list so they only appear once after the function is called flash().

Now is the time to try the application again and see how the form works. Be sure to try submitting the form with empty username or password fields to see how the validator DataRequired stops the sending process.

Improved field validation

Validators attached to form fields prevent invalid data from being accepted into the application. The way the application handles invalid form input is by redisplaying the form to allow the user to make the necessary corrections.

If you have tried to submit invalid data, I'm sure you have noticed that although the validation mechanisms work well, the user is not given any indication that there is anything wrong with the form, the user simply receives the form back. The next challenge is to improve the user experience by adding a meaningful error message next to each field that fails validation.

In fact, form validators already generate these descriptive error messages, so all that's missing is some extra logic in the template to display them.

Here's a login template with added field validation messages in the username and password fields:

app/templates/login.html: Validation errors in login form template

{% extends "base.html" %}
{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" novalidate>
    {{ form.hidden_tag() }}
    <p>
      {{ form.username.label }}<br>
      {{ form.username(size=32) }}<br>
      {% for error in form.username.errors %}
        <span style="color: red;">[{{ error }}]</span>
      {% endfor %}
    </p>
    <p>
      {{ form.password.label }}<br>
      {{ form.password(size=32) }}<br>
      {% for error in form.password.errors %}
        <span style="color: red;">[{{ error }}]</span>
      {% endfor %}
    </p>
    <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
    <p>{{ form.submit() }}</p>
  </form>
{% endblock %}

The only change I made was adding for loops right after the username and password fields, which display error messages added by validators in red. Typically, any fields that have validation tools attached to them will have any error messages that occur as a result of the validation added to them in the section form.<field_name>.errors. This will be a list because fields can have multiple validators attached, and more than one can produce error messages to display to the user.

If you try to submit a form with a blank username or password, you'll get a nice error message highlighted in red.

Link Creation

The login form is pretty complete at this point, but before I close this chapter, I want to discuss the proper way to include links in templates and redirects. So far, you've seen several examples where links are defined. For example, this is the current navigation bar in the base template:

    <div>
        Microblog:
        <a href="https://habr.com/index">Home</a>
        <a href="https://habr.com/login">Login</a>
    </div>

The login view function also defines a link that is passed to the function redirect():

@app.route("https://habr.com/login", methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # ...
        return redirect("https://habr.com/index")
    # ...

One of the problems with writing links directly in templates and source files is that if one day you decide to reorganize your links, you will have to search and replace those links throughout your entire application.

To better control these references, Flask provides a function called url_for(), which generates URLs using its internal URL mapping to view functions. For example, the expression url_for('login') returns /login And url_for('index') returns /index. Argument for url_for() is the name end pointwhich is the name of the view function.

You may ask why it is better to use function names instead of URLs. The point is that URLs are much more likely to change than view function names, which are completely internal. The second reason is that, as you will learn later, some URLs contain dynamic components, so manually creating these URLs will require combining multiple elements, which is tedious and error-prone. Function url_for() is also capable of generating those complex URLs with a much more elegant syntax.

So from now on I'm going to use url_for() every time I need to generate an application url. The navigation bar in the base template becomes:

app/templates/base.html: Use the url_for() function for links

        <div>
            Microblog:
            <a href="https://habr.com/ru/articles/805997/{{ url_for("index') }}">Home</a>
            <a href="https://habr.com/ru/articles/805997/{{ url_for("login') }}">Login</a>
        </div>

And here is the updated one login() view function:

app/routes.py: Use the url_for() function for links

from flask import render_template, flash, redirect, url_for
# ...
@app.route("https://habr.com/login", methods=['GET', 'POST'])
def login():
  form = LoginForm()
  if form.validate_on_submit():
    # ...
    return redirect(url_for('index'))
  # ...

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *