2012-05-13T06:26:42Z

The Flask Mega-Tutorial, Part III: Web Forms

(Great news! There is a new version of this tutorial!)

This is the third article in the series in which I document my experience writing web applications in Python using the Flask microframework.

The goal of the tutorial series is to develop a decently featured microblogging application that demonstrating total lack of originality I have decided to call microblog.

NOTE: This article was revised in September 2014 to be in sync with current versions of Python and Flask.

Here is an index of all the articles in the series that have been published to date:

Recap

In the previous chapter of the series we defined a simple template for the home page and used fake objects as placeholders for things we don't have yet, like users or blog posts.

In this article we are going to fill one of those many holes we still have in our app, we will be looking at how to work with web forms.

Web forms are one of the most basic building blocks in any web application. We will be using forms to allow users to write blog posts, and also for logging in to the application.

To follow this chapter along you need to have the microblog app as we left it at the end of the previous chapter. Please make sure the app is installed and running.

Configuration

To handle our web forms we are going to use the Flask-WTF extension, which in turn wraps the WTForms project in a way that integrates nicely with Flask apps.

Many Flask extensions require some amount of configuration, so we are going to setup a configuration file inside our root microblog folder so that it is easily accessible if it needs to be edited. Here is what we will start with (file config.py):

WTF_CSRF_ENABLED = True
SECRET_KEY = 'you-will-never-guess'

Pretty simple, it's just two settings that our Flask-WTF extension needs. The WTF_CSRF_ENABLED setting activates the cross-site request forgery prevention (note that this setting is enabled by default in current versions of Flask-WTF). In most cases you want to have this option enabled as it makes your app more secure.

The SECRET_KEY setting is only needed when CSRF is enabled, and is used to create a cryptographic token that is used to validate a form. When you write your own apps make sure to set the secret key to something that is difficult to guess.

Now that we have our config file we need to tell Flask to read it and use it. We can do this right after the Flask app object is created, as follows (file app/__init__.py):

from flask import Flask

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

from app import views

The user login form

Web forms are represented in Flask-WTF as classes, subclassed from base class Form. A form subclass simply defines the fields of the form as class variables.

Now we will create a login form that users will use to identify with the system. The login mechanism that we will support in our app is not the standard username/password type, we will have our users login using their OpenID. OpenIDs have the benefit that the authentication is done by the provider of the OpenID, so we don't have to validate passwords, which makes our site more secure to our users.

The OpenID login only requires one string, the so called OpenID. We will also throw a 'remember me' checkbox in the form, so that users can choose to have a cookie installed in their browsers that remembers their login when they come back.

Let's write our first form (file app/forms.py):

from flask_wtf import Form
from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired

class LoginForm(Form):
    openid = StringField('openid', validators=[DataRequired()])
    remember_me = BooleanField('remember_me', default=False)

I believe the class is pretty much self-explanatory. We imported the Form class, and the two form field classes that we need, StringField and BooleanField.

The DataRequired import is a validator, a function that can be attached to a field to perform validation on the data submitted by the user. The DataRequired validator simply checks that the field is not submitted empty. There are many more validators included with Flask-WTF, we will use some more in the future.

Form templates

We will also need a template that contains the HTML that produces the form. The good news is that the LoginForm class that we just created knows how to render form fields as HTML, so we just need to concentrate on the layout. Here is our login template (file app/templates/login.html):

<!-- extend from base layout -->
{% extends "base.html" %}

{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" name="login">
      {{ form.hidden_tag() }}
      <p>
          Please enter your OpenID:<br>
          {{ form.openid(size=80) }}<br>
      </p>
      <p>{{ form.remember_me }} Remember Me</p>
      <p><input type="submit" value="Sign In"></p>
  </form>
{% endblock %}

Note that in this template we are reusing the base.html template through the extends template inheritance statement. We will actually do this with all our templates, to ensure a consistent layout across all pages.

There are a few interesting differences between a regular HTML form and our template. This template expects a form object instantiated from the form class we just defined stored in a template argument named form. We will take care of sending this template argument to the template next, when we write the view function that renders this template.

The form.hidden_tag() template argument will get replaced with a hidden field that implements the CSRF prevention that we enabled in the configuration. This field needs to be in all your forms if you have CSRF enabled. The good news is that Flask-WTF handles it for us, we just need to make sure it is included in the form.

The actual fields of our form are rendered by the field objects, we just need to refer to a {{form.field_name}} template argument in the place where each field should be inserted. Some fields can take arguments. In our case, we are asking the text field to generate our openid field with a width of 80 characters.

Since we have not defined the submit button in the form class we have to define it as a regular field. The submit field does not carry any data so it doesn't need to be defined in the form class.

Form views

The final step before we can see our form is to code a view function that renders the template.

This is actually quite simple since we just need to pass a form object to the template. Here is our new view function (file app/views.py):

from flask import render_template, flash, redirect
from app import app
from .forms import LoginForm

# index view function suppressed for brevity

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    return render_template('login.html', 
                           title='Sign In',
                           form=form)

So basically, we have imported our LoginForm class, instantiated an object from it, and sent it down to the template. This is all that is required to get form fields rendered.

Let's ignore for now the flash and redirect imports. We'll use them a bit later.

The only other thing that is new here is the methods argument in the route decorator. This tells Flask that this view function accepts GET and POST requests. Without this the view will only accept GET requests. We will want to receive the POST requests, these are the ones that will bring in the form data entered by the user.

At this point you can try the app and see the form in your web browser. After you start the application you will want to open http://localhost:5000/login in your web browser, as this is the route we have associated with the login view function.

We have not coded the part that accepts data yet, so pressing the submit button will not have any effect at this time.

Receiving form data

Another area where Flask-WTF makes our job really easy is in the handling of the submitted form data. Here is an updated version of our login view function that validates and stores the form data (file app/views.py):

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for OpenID="%s", remember_me=%s' %
              (form.openid.data, str(form.remember_me.data)))
        return redirect('/index')
    return render_template('login.html', 
                           title='Sign In',
                           form=form)

The validate_on_submit method does all the form processing work. If you call it when the form is being presented to the user (i.e. before the user got a chance to enter data on it) then it will return False, so in that case you know that you have to render the template.

When validate_on_submit is called as part of a form submission request, it will gather all the data, run all the validators attached to fields, and if everything is all right it will return True, indicating that the data is valid and can be processed. This is your indication that this data is safe to incorporate into the application.

If at least one field fails validation then the function will return False and that will cause the form to be rendered back to the user, and this will give the user a chance to correct any mistakes. We will see later how to show an error message when validation fails.

When validate_on_submit returns True our login view function calls two new functions, imported from Flask. The flash function is a quick way to show a message on the next page presented to the user. In this case we will use it for debugging, since we don't have all the infrastructure necessary to log in users yet, we will instead just display a message that shows the submitted data. The flash function is also extremely useful on production servers to provide feedback to the user regarding an action.

The flashed messages will not appear automatically in our page, our templates need to display the messages in a way that works for the site layout. We will add these messages to the base template, so that all our templates inherit this functionality. This is the updated base template (file app/templates/base.html):

<html>
  <head>
    {% if title %}
    <title>{{ title }} - microblog</title>
    {% else %}
    <title>microblog</title>
    {% endif %}
  </head>
  <body>
    <div>Microblog: <a href="/index">Home</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>

The technique to display the flashed message is hopefully self-explanatory. One interesting property of flash messages is that once they are requested through the get_flashed_messages function they are removed from the message list, so these messages appear in the first page requested by the user after the flash function is called, and then they disappear.

The other new function we used in our login view is redirect. This function tells the client web browser to navigate to a different page instead of the one requested. In our view function we use it to redirect to the index page we developed in previous chapters. Note that flashed messages will display even if a view function ends in a redirect.

This is a great time to start the app and test how the form works. Make sure you try submitting the form with the openid field empty, to see how the DataRequired validator halts the submission process.

Improving field validation

With the app in its current state, forms that are submitted with invalid data will not be accepted. Instead, the form will be presented back to the user to correct. This is exactly what we want.

What we are missing is an indication to the user of what is wrong with the form. Luckily, Flask-WTF also makes this an easy task.

When a field fails validation Flask-WTF adds a descriptive error message to the form object. These messages are available to the template, so we just need to add a bit of logic that renders them.

Here is our login template with field validation messages (file app/templates/login.html):

<!-- extend base layout -->
{% extends "base.html" %}

{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" name="login">
      {{ form.hidden_tag() }}
      <p>
          Please enter your OpenID:<br>
          {{ form.openid(size=80) }}<br>
          {% for error in form.openid.errors %}
            <span style="color: red;">[{{ error }}]</span>
          {% endfor %}<br>
      </p>
      <p>{{ form.remember_me }} Remember Me</p>
      <p><input type="submit" value="Sign In"></p>
  </form>
{% endblock %}

The only change we've made is to add a for loop that renders any messages added by the validators below the openid field. As a general rule, any fields that have validators attached will have errors added under form.field_name.errors. In our case we use form.openid.errors. We display these messages in a red style to call the user's attention.

Dealing with OpenIDs

In practice, we will find that a lot of people don't even know that they already have a few OpenIDs. It isn't that well known that a number of major service providers on the Internet support OpenID authentication for their members. For example, if you have an account with Google, you have an OpenID with them. Likewise with Yahoo, AOL, Flickr and many other providers. (Update: Google is shutting down their OpenID service on April 15 2015).

To make it easier for users to login to our site with one of these commonly available OpenIDs, we will add links to a short list of them, so that the user does not have to type the OpenID by hand.

We will start by defining the list of OpenID providers that we want to present. We can do this in our config file (file config.py):

WTF_CSRF_ENABLED = True
SECRET_KEY = 'you-will-never-guess'

OPENID_PROVIDERS = [
    {'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id'},
    {'name': 'Yahoo', 'url': 'https://me.yahoo.com'},
    {'name': 'AOL', 'url': 'http://openid.aol.com/<username>'},
    {'name': 'Flickr', 'url': 'http://www.flickr.com/<username>'},
    {'name': 'MyOpenID', 'url': 'https://www.myopenid.com'}]

Now let's see how we use this array in our login view function:

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        flash('Login requested for OpenID="%s", remember_me=%s' %
              (form.openid.data, str(form.remember_me.data)))
        return redirect('/index')
    return render_template('login.html', 
                           title='Sign In',
                           form=form,
                           providers=app.config['OPENID_PROVIDERS'])

Here we grab the configuration by looking it up in app.config with its key. The array is then added to the render_template call as a template argument.

As I'm sure you guessed, we have one more step to be done with this. We now need to specify how we would like to render these provider links in our login template (file app/templates/login.html):

<!-- extend base layout -->
{% extends "base.html" %}

{% block content %}
<script type="text/javascript">
function set_openid(openid, pr)
{
    u = openid.search('<username>')
    if (u != -1) {
        // openid requires username
        user = prompt('Enter your ' + pr + ' username:')
        openid = openid.substr(0, u) + user
    }
    form = document.forms['login'];
    form.elements['openid'].value = openid
}
</script>
<h1>Sign In</h1>
<form action="" method="post" name="login">
    {{ form.hidden_tag() }}
    <p>
        Please enter your OpenID, or select one of the providers below:<br>
        {{ form.openid(size=80) }}
        {% for error in form.openid.errors %}
          <span style="color: red;">[{{error}}]</span>
        {% endfor %}<br>
        |{% for pr in providers %}
          <a href="javascript:set_openid('{{ pr.url }}', '{{ pr.name }}');">{{ pr.name }}</a> |
        {% endfor %}
    </p>
    <p>{{ form.remember_me }} Remember Me</p>
    <p><input type="submit" value="Sign In"></p>
</form>
{% endblock %}

The template got somewhat long with this change. Some OpenIDs include the user's username, so for those we have to have a bit of javascript magic that prompts the user for the username and then composes the OpenID with it. When the user clicks on an OpenID provider link and (optionally) enters the username, the OpenID for that provider is inserted in the text field.

Below is a screenshot of our login screen after clicking the Google OpenID link:

Sign In screenshot

Final Words

While we have made a lot of progress with our login form, we haven't actually done anything to login users into our system, all we've done so far had to do with the GUI aspects of the login process. This is because before we can do real logins we need to have a database where we can record our users.

In the next chapter we will get our database up and running, and shortly after we will complete our login system, so stay tuned for the follow up articles.

The microblog application in its current state is available for download here:

Download microblog-0.3.zip.

Remember that the Flask virtual environment is not included in the zip file. For instructions on how to set it up see the first chapter of the series.

Feel free to leave comments or questions below. I hope to see you in the next chapter.

Miguel

246 comments

  • #126 Jona said 2014-04-17T17:54:18Z

    Hi Can I use validate_on_submit if I have two forms on one page? Bacause I'm trying to do this and have a situation that pressing on any of the "submit" buttons leads to executing in both forms. So I can see the result of the last one. Thanx

  • #127 Miguel Grinberg said 2014-04-18T14:12:31Z

    @Jona: you need to give each form a prefix, so that the names are unique. See the documentation here: http://wtforms.simplecodes.com/docs/0.6.1/forms.html

  • #128 Bryan said 2014-04-20T12:08:35Z

    Every page I embed the code to gives me an error. "Either the server overloaded or there is an error in your code"

  • #129 Miguel Grinberg said 2014-04-20T17:34:12Z

    @Bryan: if you are running in debug mode you should have a stack trace in your console. That will give you some idea of where the error is coming from.

  • #130 Hasini Varma said 2014-04-20T18:20:31Z

    Hi Miguel! Your tutorials are amazing and they have helped me so much in developing my own website. I have a quick question. One aspect of my website allows users to upload recipes. But I am having trouble preserve the white spaces and line breaks that user puts in. I don't how to do this using wtf forms and css pre-wrap is not working. Do you have any suggestions? Thank you!

  • #131 Miguel Grinberg said 2014-04-21T00:21:29Z

    @Hasini: I'm not sure this is a problem with wtforms. The white space entered in a text area should be preserved. The problem could be that this white space is removed when you render that text back to a page. One way to avoid that is to put the text inside a block.

  • #132 Preet said 2014-04-22T19:04:51Z

    Great tutorial, Miguel! Had a quick q if you have the time: form.validate_on_submit() is called twice: Once on the initial GET (with no data to validate), and then on the POST (with data to validate). But the form=LoginForm() is a new variable in the 2nd case - how is it accessing data from the first instantiation of LoginForm? Thanks for the tut!

  • #133 Bryan said 2014-04-22T19:10:12Z

    @Miguel Grinberg

    When I try to use the debug mode, it simply says "NameError: global name 'LoginForm' is not defined". Since I followed this tutorial twice I don't know where to look for it.

  • #134 Miguel Grinberg said 2014-04-23T14:41:36Z

    @Bryan: did you compare your files with mine? Download my version and do the comparison, you probably missed something.

  • #135 Tom said 2014-04-29T22:21:24Z

    @ Miguel Grinberg

    Thanks for the tutorial! I am getting the following error: AttributeError: 'module' object has no attribute 'hybridmethod' with this traceback: File "run.py", line 2, in from app import app File "/Users/Tom/Box Sync/Work/Berkeley/Projects/Cumulative Project/Code/web/microblog/app/init.py", line 6, in from app import views File "/Users/Tom/Box Sync/Work/Berkeley/Projects/Cumulative Project/Code/web/microblog/app/views.py", line 3, in from forms import LoginForm File "/Users/Tom/Box Sync/Work/Berkeley/Projects/Cumulative Project/Code/web/microblog/app/forms.py", line 1, in from flask.ext.wtf import Form File "/Users/Tom/anaconda/lib/python2.7/site-packages/flask/exthook.py", line 61, in load_module import(realname) File "/Users/Tom/anaconda/lib/python2.7/site-packages/flask_wtf/init.py", line 13, in import sqlalchemy File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/init.py", line 52, in from sqlalchemy.types import ( File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/types.py", line 27, in from sqlalchemy import exc, schema File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/schema.py", line 33, in from sqlalchemy import event, events File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/event/init.py", line 7, in from .api import CANCEL, NO_RETVAL, listen, listens_for, remove, contains File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/event/api.py", line 13, in from .base import _registrars File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/event/base.py", line 42, in class _Dispatch(object): File "/Users/Tom/anaconda/lib/python2.7/site-packages/sqlalchemy/event/base.py", line 104, in _Dispatch @util.hybridmethod

    It happens with both my code and the code downloaded from your website, so I'm guessing something isn't installed correctly, but I have gone over your install list from step one several times so am at a loss as to what is missing. Thanks again.

  • #136 Miguel Grinberg said 2014-04-30T06:27:12Z

    @Tom: your SQLAlchemy installation is probably bad. I suggest you reinstall it.

  • #137 Tom said 2014-04-30T21:06:12Z

    @ Miguel Grinberg

    Thanks for the reply. I have uninstalled and reinstalled everything from Part I and am still getting the same error. SQLAlchemy seems to be installing fine, here's the output:

    Downloading/unpacking sqlalchemy==0.7.9 Downloading SQLAlchemy-0.7.9.tar.gz (2.6MB): 2.6MB downloaded Running setup.py (path:/private/var/folders/vh/fd36_sdx4kd3ygb_13fykmkc0000gn/T/pip_build_Tom/sqlalchemy/setup.py) egg_info for package sqlalchemy

    warning: no files found matching '*.jpg' under directory 'doc' no previously-included directories found matching 'doc/build/output'

    Installing collected packages: sqlalchemy Running setup.py install for sqlalchemy building 'sqlalchemy.cprocessors' extension gcc -fno-strict-aliasing -I/Users/Tom/anaconda/include -arch x86_64 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -I/Users/Tom/anaconda/include/python2.7 -c lib/sqlalchemy/cextension/processors.c -o build/temp.macosx-10.5-x86_64-2.7/lib/sqlalchemy/cextension/processors.o gcc -bundle -undefined dynamic_lookup -L/Users/Tom/anaconda/lib -arch x86_64 -arch x86_64 build/temp.macosx-10.5-x86_64-2.7/lib/sqlalchemy/cextension/processors.o -L/Users/Tom/anaconda/lib -o build/lib.macosx-10.5-x86_64-2.7/sqlalchemy/cprocessors.so building 'sqlalchemy.cresultproxy' extension gcc -fno-strict-aliasing -I/Users/Tom/anaconda/include -arch x86_64 -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -I/Users/Tom/anaconda/include/python2.7 -c lib/sqlalchemy/cextension/resultproxy.c -o build/temp.macosx-10.5-x86_64-2.7/lib/sqlalchemy/cextension/resultproxy.o gcc -bundle -undefined dynamic_lookup -L/Users/Tom/anaconda/lib -arch x86_64 -arch x86_64 build/temp.macosx-10.5-x86_64-2.7/lib/sqlalchemy/cextension/resultproxy.o -L/Users/Tom/anaconda/lib -o build/lib.macosx-10.5-x86_64-2.7/sqlalchemy/cresultproxy.so

    warning: no files found matching '*.jpg' under directory 'doc' no previously-included directories found matching 'doc/build/output'

    Successfully installed sqlalchemy Cleaning up...

    If it is at all related, I installed with "pip install sqlalchemy==0.7.9" from the flask/bin directory as calling "flask/bin/pip install sqlalchemy==0.7.9" from the microblog folder causes this error: "bad interpreter: No such file or directory". It seems to break when it has to navigate through a folder that has a space in its name (in my case it is "Box Sync" and it tries to find the folder "Box"). Also potentially related is that I can't run run.py with the command "./run.py" and instead use "python run.py". If I use the former, I get the following:

    Traceback (most recent call last): File "./run.py", line 2, in from app import app File "/Users/Tom/Box Sync/Work/Berkeley/Projects/Cumulative Project/Code/web/microblog/app/init.py", line 1, in from flask import Flask ImportError: No module named flask

    I hope some of this helps, and thanks again.

  • #138 Shahriyar Rzayev said 2014-05-09T10:32:58Z

    Thanks for a great series of tutorials. I have trouble with getting value from form When i click to Sign In i am getting:

    Method Not Allowed The method is not allowed for the requested URL.

    How to fix? Thank you.

  • #139 Miguel Grinberg said 2014-05-09T14:10:04Z

    @Shahriyar: you are missing the "methods=['GET', 'POST']" argument in your route declaration.

  • #140 Ken said 2014-05-11T03:56:44Z

    Many thanks for the clear tutorial. I think teaching is one of your strong points. Everything is working btw :) Question though: We put "app.config.from_object('config')" in init.py. Is this calling a method on the app/Flask object (I didn't see one in source)? I did find 'from_object' in flask's config.py; is our init statement accessing this? How? Thank you.

  • #141 Miguel Grinberg said 2014-05-11T19:37:52Z

    @Ken: the Flask instance has a config member, which is an instance of Flask's Config class. This is a dict with some additional features, like the ability to import key/value pairs from a module. This is what the from_object() method does.

  • #142 Ken said 2014-05-12T00:28:11Z

    Thank you Miguel. I was wondering why we are not just 'import'ing our config.py. Doing that would make OUR app aware of the config list/dict, but then I suppose the flask app would not be, hence 'from_object'. Thanks!

  • #143 Bill said 2014-05-14T06:58:58Z

    I'm only seeing the javascript popup for entering the OpenID username come up for AOL and Flickr. It appears as though the javascript only triggers when the variable is included in the config.py file under each provider and AOL and Flickr are the only ones with that variable included. Not such a big deal now as we're not actually authenticating but how would we format the OpenID URL correctly for the other providers without the variable?

  • #144 Miguel Grinberg said 2014-05-15T04:29:41Z

    @Bill: For many OpenID providers the connection URL does not have the username in it.

  • #145 Alex Leonhardt said 2014-05-20T20:34:11Z

    hi, am going through this tutorial too - but the JS doesnt seem to work ? am getting this in firebug/firefox:

    ReferenceError: set_openid is not defined id',%20'Google');()

    Any clues ?

  • #146 Miguel Grinberg said 2014-05-21T05:10:14Z

    @Alex: the set_openid() is shown above in this article, so it is defined. Compare your code to mine to see what's different.

  • #147 Ben Shoemaker said 2014-05-24T20:22:23Z

    I keep getting the error:

    "AttributeError: 'TextField' object has no attribute 'date'"

    Any idea what's going on with that?

  • #148 Miguel Grinberg said 2014-05-25T23:57:18Z

    @Ben: you need to look at your stack trace to determine where is this error occurring.

  • #149 Alex Leonhardt said 2014-05-26T18:00:40Z

    @Miguel - yup figured it out - I actually made another block {% block js %} for "custom" JS for pages to import when / as necessary - that worked then .. :)

  • #150 Zion said 2014-05-27T11:48:21Z

    Good work, thanks!