Posting With Flask WTForms

The application does not do much at the moment. In this post we will add a form to the Agile Estimation application to allow a user to pick a nickname.

Posting With Flask WTForms

The application does not do much at the moment. In this post we will add a form to the Agile Estimation application to allow a user to pick a nickname.

To do this we will use another Flask extension, flask-wtf. It needs to be installed in the virtual environment with pip install flask-wtf and also added to the development and production requirement files.

The application will need a new property, SECRET_KEY, at this point which is used to prevent Cross Site Request Forgery (CSRF) attacks. The code creates tokens with a cryptographic signature generated using the secret. Should anyone gain access to the secret key you are using, it could allow them to attack your site.

For the moment, since we are in the development phase of the project, the key does not have to be so secure. It can be added to config.py. Here I’ve added it to the development and test configurations. In production we will use a different strategy to protect the key as it should not appear in source control.

class DevelopmentConfig(Config):
	DEBUG = True
	SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(base_dir, 'data.sqlite')
	SECRET_KEY = 'Temporary not-very-secret key'

class TestConfig(Config):
	TESTING = True
	SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:'
	SECRET_KEY = 'Temporary not-very-secret key'

Forms

The new package allows many short cuts to creating forms on your website. No need to wrestle with the HTML, simply add an object that extends FlaskForm, add attributes and return it to with the rendered template.

Here is an example form that will allow the entry of a nickname, giving the text to prompt the user with and a button to submit the form to the server.

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

class NickNameForm(FlaskForm):
    name = StringField('Please enter a nickname', validators=[DataRequired()])
    submit = SubmitField('Submit')

Adding it to a template is easy by using a bootstrap template and a wtf function. Here I’ve added it to the index.html file:

<div class="container">
	{% import "bootstrap/wtf.html" as wtf %}
	{{ wtf.quick_form(form) }}
</div>

The controller has to create an instance of the form and return it with the template so that it can be rendered on the web page. First import the form, instantiate it, and then return it with the template:

form = NickNameForm()
return render_template('index.html', form=form)

By default it will try to post this to the same URL that received the request. This is how WTF renders the form:

form_added_with_wtf

It does this by generating a block of HTML as follows:

<form action="" method="post" class="form" role="form">
  <input id="csrf_token" name="csrf_token" type="hidden" value="ImExZjUyMmExNzA2ZDdmZDIyZTA2NzU2NTUyMTBiNjBmMzQxOWQ3N2Ei.DzNqdQ.lC5MRIIeeSvy9whG7ePW9tO4xcc">  
	<div class="form-group  required">
		<label class="control-label" for="name">Please enter a nickname</label>
		<input class="form-control" id="name" name="name" required type="text" value="">
	</div>
    <input class="btn btn-default" id="submit" name="submit" type="submit" value="Submit">
</form>

The hidden csrf_token in the form which is used to prevent CSRF attacks. Note that this is not encrypted; the value contains both data and a cryptographic signature. Without knowing the secret key, it would not be possible to create the signature. Flask can validate that the request is valid, and that the values have not been manipulated by comparing the signature to one only it can generate.

Additionally WTF adds validation to the form. If the submit button is pressed with no value in the text field, then a warning is presented to the user. The validators=[DataRequired() attribute achieves the following warning when the submit is clicked without any text entered.

form_warning

Accepting a Post

This does not do much at the moment beyond validating the input since there is no controller method to accept a post from the form. I will add another function that responds to the / route, but this time for the POST command. Another approach is to use one method that accepts both and uses an if statement to decide on what behaviour to apply.

The method to accept a nickname is going to return back to the same page for now (I’ll redirect rather than render the template directly later, see further down) but this time with a new variable nickname that has been set to the value entered in the form.

@web.route('/', methods=['POST'])
def accept_nickname():
	form = NickNameForm()
	if form.validate_on_submit():
		nickname = form.name.data
		form.name.data = ''
	return render_template('index.html', form=form, nickname=nickname)

To use the new variable in the index page, I also need to set it in the hello function. I can set it as follows: nickname = None. Otherwise it will not be possible to check it. To display on the page I can use an if block directly in index.html:

{% if nickname %} 
	Hello {{ nickname }}.
{% endif %}

Now a little welcome appears on the screen above the form.

form_welcome_user

There is no need to include the form again if the user has already entered a nickname. To hide, use the else part of an if statement:

{% if nickname %} 
	Hello {{ nickname }}.
{% else %}
	{% import "bootstrap/wtf.html" as wtf %}
	{{ wtf.quick_form(form) }}
{% endif %}

just_a_welcome

Redirect

Returning the same template as in the get causes a slight problem. Hitting the refresh button will cause the browser to ask the user whether they want to resend the form data.

If we instead use a redirect, we can send the user back to the get version of the page, then we avoid this issue. To do so we need to import redirect and url_for from flask and use them to build the correct redirect path.

pexels-photo-246124

The advice given is to never try to figure out the correct url yourself, that’s what url_for is intended to do. As a parameter, pass the name of the method that responds to the URL, using an alias for the blueprint that is being used. This allows for changing URLs without having to change the code in a large number of places.

Since I am still using the function name hello from when I built the first test application, this appears to be an appropriate place to rename the function to index, mirroring the template that is behind the request. So the path to redirect to can be derived as follows:

return redirect(url_for('web.index'))

Redirection introduces another problem, however, as we no longer can place the nickname into the response; a new response will be created from the new get. We can resolve this by storing the nickname in a session variable:

session['nickname'] = form.name.data

The get function needs to change to load the variable from the session variable. If not found it will use None.

return render_template('index.html', form=form, nickname=session.get('nickname'))

The data from this is stored in a session variable, visible in the session cookie as shown in this screen grab:

stored_in_cookie

Note this data is not secure. While not human readable it can be easily decoded into the following JSON block, which also shows the token used for CSRF protection. For a great explanation of this see this blog and video on the insecurity of session cookies

{
    "csrf_token": "a1f522a1706d7fd22e0675655210b60f3419d77a",
    "nickname": "Ronan"
}

Note that at the moment the system has no means to delete the nickname; that can only be done by manually removing the cookie using the browser’s functionality or by starting a new session. That’s good enough for now.

What’s Next?

Next we will add some code to show any groups created with the nickname and allow them to add a new group.