Working with Flask forms

by Alex

Forms are an important element of any web application, but unfortunately they are quite difficult to work with. First you need to validate the data on the client side, then on the server. And even this is not enough if the application developer is concerned about security issues such as CSRF, XSS, SQL Injection, and so on. All together, it’s a lot of work. Fortunately, there is a great library called WTForms that does most of the work for the developer. Before learning more about WTForms, you should still figure out how to work with forms without libraries and packages.

Working with forms is a tricky part

First, we create a login.html template with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% if message %}
        <p>{{ message }}</p>
    {% endif %}
    
    <form action="" method="post">
        <p>
	    <label for="username">Username</label>
	    <input type="text" name="username">
	</p>
	<p>
	    <label for="password">Password</label>
	    <input type="password" name="password">
	</p>
	<p>
	    <input type="submit">
	</p>
    </form>
    
</body>
</html>

This code should be added after the books() view function in the main2.py file:

from flask import flask, render_template, request
#...
@app.route('/login/', methods=['post', 'get'])
def login():
    message = ''
   if request.method == 'POST':
	username = request.form.get('username') # request the form data
	password = request.form.get('password')

   if username == 'root' and password == 'pass':
	message = "Correct username and password"
   else:
	message = "Wrong username or password"

   return render_template('login.html', message=message)
#...

Note that the methods argument is passed to the route() decorator. By default the request handler is only called when the request. method is GET or HEAD. You can change this by passing a list of allowed HTTP methods to the methods keyword argument. From now on, the login view function will only be called when a request to /login/ is made using GET, POST or HEAD methods. If you try to access the /login/ URL using another method, you’ll get an HTTP 405 Method Not Allowed error. In the past tutorials, we’ve discussed how the request object provides information about the current web request. The information from the form is stored in the form attribute of the request object. the request.form is an immutable dictionary type object known as ImmutableMultiDict. Next, you need to start the server and go to https://localhost:5000/login/. This form will open.Working with Flask forms The request to the page was made using the GET method, so the code inside the if block of the login() function is omitted. If you try to submit the form without entering any data, the page will look like this: Working with Flask forms This time the page was sent by POST, so the code inside the if was executed. Inside this block, the application takes the username and password and sets the message for the message. Because the form was empty, an error message was displayed. If you fill in the form with the correct username and password and press Enter, the welcome message "Correct username and password" appears:Working with Flask forms This is how you can work with forms in Flask. Now we should pay attention to the WTForms package.

WTForms

WTForms is a powerful library written in Python and is independent of frameworks. It knows how to generate forms, validate them and pre-fill them with information (handy for editing) and much more. It also offers protection against CSRF. Flask-WTF is used to install WTForms. Flask- WTF is an extension for Flask that integrates WTForms into Flask. It also offers additional features such as file uploads, reCAPTCHA, internationalization (i18n), and others. To install Flask-WTF, enter the following command.

(env) [email protected]:~/flask_app$ pip install flask-wtf

Creating the Form class

We should start by defining forms as Python classes. Each form should extend the FlaskForm class from the flask_wtf package. FlaskForm is a wrapper that contains useful methods for the original wtform.Form class, which is the main class for creating forms. Inside the form class, form fields are defined as class variables. Form fields are defined by creating an object associated with the field type. The wtform package offers several classes representing the following fields: StringField, PasswordField, SelectField, TextAreaField, SubmitField, and others. First, you need to create a forms.py file inside the flask_app dictionary and add the following code to it.

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Email

class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField("Submit")

Here you define a ContactForm form class with four fields: name, email, message, and sumbit. These variables will be used to render the form fields and assign and retrieve information from them. This form is created with two StringFields, a TextAreaField and a SumbitField. Each time a field object is created, certain arguments are passed to its constructor function. The first argument is a string containing the label that will be displayed inside the <label> tag when the field is rendered. The second optional argument is a list of validators (elements of the validation system), which are passed to the constructor as keyword arguments. Validators are functions or classes that determine whether the information entered into a field is correct. Multiple validators can be used for each field, separated by commas (,). The wtforms.validators module offers basic validators, but you can create them yourself. This form uses two built-in validators: DataRequired and Email. DataRequired: it checks if the user entered any information in the field. Email: it checks if the email address entered is valid. The entered data will not be accepted until the validator validates the data. Note: this is just the basis of the form fields and validators. A complete list is available at https://wtforms.readthedocs.io.

SECRET_KEY

setting By default, Flask-WTF prevents any variants of CSFR attacks. This is done by embedding a special token in the hidden <input> element inside the form. This token is then used to authenticate the request. Before Flask-WTF can generate a csrf-token, a secret key must be added. Set it in the main2.py file as follows:

#...
app.debug = True
app.config['SECRET_KEY'] = 'a really really really long secret key'

manager = Manager(app)
#...

The config attribute of the Flask object is used here. The config attribute works as a dictionary and is used to place Flask configuration parameters and Flask extensions, but you can also add them yourself. The secret key should be a string – one that is hard to figure out and preferably long. SECRET_KEY is not only used to create CSFR tokens. It is used in other Flask extensions as well. The secret key must be stored securely. Instead of storing it in your application, it is better to store it in an environment variable. How to do this will be explained in the following sections.

Console Forms

We open a Python shell with the following command:

(env) [email protected]:~/flask_app$ python main2.py shell

This will launch the Python shell inside the application context. Now you need to import the ContactForm class and create an instance of the new form object by passing the form data.

>>>
>>> from forms import ContactForm
>>> from werkzeug.datastructures import MultiDict
>>>
>>>
>>> form1 = ContactForm(MultiDict([('name', 'jerry'),('email', '[email protected]')])
>>>

Note that the data is passed as a MultiDict object because the wtforms.Form class constructor function takes a MutiDict type argument. If the form data is not defined when creating the form object instance and the form is sent using a POST request, wtforms.Form uses the data from the request.form attribute. It’s worth remembering that request.form returns an object of type ImmutableMultiDict. This is the same as MultiDict, but it is immutable. The validate() method validates the form. If the validation succeeds, it returns True, if not, it returns False.

>>>
>>> form1.validate()
False
>>>

The form failed because no data was passed to the mandatory message field when the form object was created. You can access form errors by using the errors attribute of the form object:

>>>
>>> form1.errors
{'message': ['This field is required.'], 'csrf_token': ['The CSRF token is missing.']}
>>>

Note that in addition to the error message for the message field, the output also contains an error message about a missing csfr token. This is because there is no POST request with a csfr-token in the form data. You can disable CSFR protection by passing csfr_enabled=False when creating an instance of the form class. Example:

>>> form3 = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')]), csrf_enabled=False)
>>>
>>> > form3.validate()
False
>>>
>>> form3.errors
{'message': ['This field is required.']}
>>>
>>>

As expected, the error now appears only for the message field. Now you can create another form object, but this time pass information for all fields to it.

>>>
>>> > form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', '[email protected]'), ('message', 'hello tom')]), csrf_enabled=False)
>>>
>>> > form4.validate()
True
>>>
>>> form4.errors
{}
>>>

Checking the form this time was successful. The next step is to render the form.

Rendering the form

There are two options for rendering:

  1. One by one.
  2. Using the loop

Rendering the fields one by one

Since templates have access to the form instance, you can use field names to render names, labels, and errors:

{# output the name of the field #}
{{ form.field_name.label() }}

{# output the field itself #}
{{ form.field_name() }}

{# output validation errors associated with the field #}
{% for error in form.field_name.errors %}
    {{ error }}
{% endfor %}

It’s worth testing this method in the console:

>>>
>>> > from forms import ContactForm
>>> from jinja2 import Template
>>>
>>> form = ContactForm()
>>>

Here the form object instance was created without request data. This is what happens when the form is displayed for the first time with a GET request.

>>>
>>>
>>> > Template("{{ form.name.label() }}").render(form=form)
'<label for="name">Name: </label>'
>>>
>>> > Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.email.label() }}").render(form=form)
'<label for="email">Email: </label>'
>>>
>>> > Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.message.label() }}").render(form=form)
'<label for="message">Message</label>'
>>>
>>> > Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
>>> > Template("{{ form.submit() }}").render(form=form)
'<input id="submit" name="submit" type="submit" value="Submit">'
>>>
>>>

Since this is the first time the form is displayed, the fields will not have validation errors. The following code demonstrates this clearly:

>>>
>>>
>>> > Template("{% for error in form.name.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> > Template("{% for error in form.email.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> > Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>
>>>

Instead of displaying validation errors for each field, you can use form.errors to access validation errors relevant to the form. forms.errors is used to display validation errors at the top of the form.

>>>
>>> > Template("{% for error in form.errors %}{{ error }}{% endfor %}").render(form=form)
''
>>>

When rendering fields and labels, you can add additional keyword arguments that will end up as key-value pairs in the HTML code. For example:

>>>
>>> > Template('{{ form.name(class="input", id="simple-input") }}').render(form=form)
'<input class="input" id="simple-input" name="name" type="text" value="">'
>>>
>>>
>>> > Template('{{ form.name.label(class='lbl') }}').render(form=form)
'<label class="lbl" for="name">Name: </label>'
>>>
>>>

Suppose the form has been submitted. Now you can try rendering the fields and see what happens.

>>>
>>> > from werkzeug.datastructures import MultiDict
>>>
>>> form = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')])
>>>
>>> form.validate()
False
>>>
>>>
>>> > Template('{{ form.name() }}').render(form=form)
'<input id="name" name="name" type="text" value="spike">'
>>>
>>>
>>> > Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="[email protected]">'
>>>
>>>
>>> > Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>

Note that the value attribute has data in the name and email fields. But the <textarea> element for the message field is empty because no data was passed to it. To access the validation error for the message field you can do the following:

>>>
>>> > Template("{% for error in form.message.errors %}{{ error }}{% endfor %}").render(form=form)
'This field is required.'
>>>

Alternatively, form.errors can be used to go through all validation errors at once.

>>>
>>> s ="""
... {% for field_name in form.errors %}
... {% for error in form.errors[field_name] %}
... <li>{{ field_name }} {{ error }}}</li>
... {% endfor %}
... {% endfor %}
... """
>>>
>>> > Template(s).render(form=form)
'<li> csrf_token: The CSRF token is missing.</li>n
<li> message: This field is required.</li>n'
>>>
>>>

Note that there is no csfr-token error because the request was sent without a token. You can render the csfr field just like any other field:

>>>
>>> > Template("{{ form.csrf_token() }}").render(form=form)
'<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">'
>>>

Rendering fields one by one can take a long time, especially if there are several of them. A loop is used for such cases.

Rendering fields with a loop

The following code demonstrates how you can render fields using a for loop.

>>>
>>> s = " ""
...     <div>
... 	    {{ form.csrf_token }}
... 	</div>
... {% for field in form if field.name != 'csrf_token' %}
... 	<div>
... 	    {{ field.label() }}
... 	    {{ field() }}
... 	    {% for error in field.errors %}
... 		<div class="error">{{ error }}</div>
... 	    {% endfor %}
... 	</div>
... {% endfor %}
... """
>>>
>>>
>>> > print(Template(s).render(form=form))
   <div>
	<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0MGMZZTY3NDc3ZTNiZxDIZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">
	
   </div>
    
   <div>
	<label for="name">Name: </label>
	<input id="name" name="name" type="text" value="spike">
	
   </div>

   <div>
	<label for="email">Email: </label>
	<input id="email" name="email" type="text" value="[email protected]">
	
   </div>
    
   <div>
	<label for="message">Message</label>
	<textarea id="message" name="message"></textarea>
	
	   <div class="error">This field is required.</div>
	    
   </div>

   <div>
	<label for="submit">Submit</label>
	<input id="submit" name="submit" type="submit" value="Submit">
	
   </div>
>>>
>>>

It is important to note that regardless of the method used, you will need to manually add a <form> tag to wrap the form fields. Now that you know how to create, validate, and render forms, you can use what you’ve learned to create real forms. First, you need to create a contact.html template with the following code:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="" method="post">

    {{ form.csrf_token() }}

    {% for field in form if field.name != "csrf_token" %}
	<p>{{ field.label() }}</p>
	<p>{{ field }}
	    {% for error in field.errors %}
		{{ error }}
	    {% endfor %}
	</p>
    {% endfor %}

</form>

</body>
</html>

The only missing piece of the puzzle is the view function, which will be created next.

Working with the form confirmation

Let’s open main2.py to add the following code after the login() view function.

from flask import Flask, render_template, request, redirect, url_for
from flask_script import Manager, Command, Shell
from forms import ContactForm
#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
   if form.validate_on_submit():
	name = form.name.data
	email = form.email.data
	message = form.message.data
	print(name)
	print(email)
	print(message)
	# here is the database logic
	print("nData received. Now redirecting ...")
	return redirect(url_for('contact'))

   return render_template('contact.html', form=form)
#...

On line 7, the form object is created. Line 8 checks the value that the validate_on_submit() method returned to execute the code inside the if instruction. Why use validate_on_submit() instead of validate(), as it was in the console? validate() only checks if the form data is correct. It does not check if the request was submitted with a POST method. This means that if the validate() method is used, a GET request to /contact/ will run the validate form and the user will see validation errors. Generally, the validation is only executed if the data was submitted using the POST method. Otherwise it returns False. The validate_on_submit() method calls the validate() method internally. Also, note that no data is passed when a form object instance is created because when the form is submitted via POST request, WTForm reads the form data from the request.form attribute. The form fields defined in the form class become attributes of the form object. To access the field data, the form field data attribute is used:

form.name.data # access the data in the name field.
form.email.data # access the data in the emailfield.

To access all of the form data at once, you must use the data attribute to the form object:

form.data # access to all data

If you use a GET request when you visit /contact/, the validate_on_sumbit() method will return False. The code inside the if will be skipped and the user will get an empty HTML form. When the form is submitted with a POST request, validate_on_sumbit() will return True, assuming the data is correct. Calls to print() within the if block will print the user’s input and redirect() will redirect the user to /contact/. On the other hand, if validate_on_sumbit() returns False, the instructions within the if body will be skipped and a validation error will be reported. If the server is not running, you have to start it and open https://localhost:5000/contact/. The following contact form will appear:Working with Flask forms If you try to click Submit without entering data, the following validation error messages will appear:Working with Flask forms You can now enter certain data in the Name and Message fields and invalid data in the Email field, and try to submit the form again.Working with Flask forms Note that all fields contain data from the previous query. Now you can enter the correct email in the Email field and click Submit. Now the check will be successful and the following output will appear in the shell:

Spike
[email protected]
A Message

Data received. Now redirecting ...

After the accepted data is displayed in the shell, the view function will redirect the user to /contact/. At this point, a blank form with no validation errors should be displayed as if the user had first opened /contact/ with a GET request. It is recommended to display feedback to the user after a successful submission. In Flask, this is done with popup messages.

Popup messages

Pop-up messages are another one of those features which depend on a secret key. It is needed because messages are stored in sessions. Sessions in Flask will be the subject of a separate lesson. Since the secret key has already been set up in this lesson, we can move on. The flash() function from the flask package is used to display the message. The flash() function takes two arguments: message and category (optional). The category indicates the type of the message: _success_, _error_, _warning_, and so on. The category can be used in the template to define the message type. Open main2.py again to add flash("Message Received", "success") just before calling redirect() in the contact() view:

from flask import flask, render_template, request, redirect, url_for, flash
#...
	# here's the database logic
	print("nData received. Now redirecting ...")
	flash("Message Received", "success")
	return redirect(url_for('contact'))
   return render_template('contact.html', form=form)

The message specified with flash() will only be available to the subsequent request, and then will be deleted. This is only a customization of the message. To display it, you must also change the template. To do this, open the file contact.html and change it as follows: Jinja offers the function get_flashed_messages() which returns a list of active messages without category. To get them together with a category, you need to pass with_category=True when you call get_flashed_messages(). When with_categories is True, get_flashed_messages() will return a list of form tuples (category, message). After these changes, you should open https://localhost:5000/contact again. Fill in the form and click Submit. A message about successful submission will appear at the top of the form.Working with Flask forms

Related Posts

LEAVE A COMMENT