📜 ⬆️ ⬇️

Flask Mega-Tutorial, Part 3: Web Forms (Edition 2018)

blog.miguelgrinberg.com


Miguel grinberg




<<< previous next >>>


This article is a translation of the third part of the new edition of Miguel Greenberg’s textbook. The old translation has long lost its relevance.


In this third installment of the Flask Mega-Tutorial series, I’ll talk about how to work with forms.


For reference, below is a list of articles in this series.



Note 1: If you are looking for old versions of this course, this is here .


Note 2: If suddenly you would like to speak in support of my (Miguel) work on this blog, or simply do not have the patience to wait for a week of the article, I (Miguel Greenberg) offer a full version of this guide a packed e-book or video. For more information, visit learn.miguelgrinberg.com .


Brief repetition


In Chapter 2, I created a simple template for the application's home page and used false entities as placeholders for objects I don’t have yet, such as users or blog posts. In this chapter, I will discuss one of the many phenomena that I have in this application, in particular, how to receive input data from users via web forms.


Web forms are one of the most basic building blocks in any web application. I will use the form so that users can post blog posts as well as to enter the application.


Before continuing to read this chapter, make sure you have a microblog application. The work done in the previous chapter should allow it to run without any errors.


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


Introduction to Flask-WTF


To process the web forms in this application, I'm going to use the Flask-WTF extension, which is a wrapper for the WTForms package and perfectly integrates it with Flask. This is the first Flask extension that I introduce to you, but not the last. Extensions are a very important part of the Flask ecosystem, as they provide solutions to problems.


Flask extensions are regular Python packages that are installed along with pip . You need to install Flask-WTF in your virtual environment:


 (venv) $ pip install flask-wtf 

Configuration


So far, the application is very simple, and for this reason I did not need to worry about its configuration. But for any application other than the simplest, you will find that Flask (and possibly also the Flask extensions used) offer some freedom in how to do something, and you need to make some decisions that you pass as a list of configuration variables. .


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


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

Although the above syntax is enough to create configuration parameters for Flask, I like to apply the principle of problem sharing, so instead of setting my configuration in the same place where I create my application, I will use a slightly more complicated structure that allows me to save my configuration in a separate file.


The format I like a lot because it is extensible is to use a class to store configuration variables. So that everything is well organized, I'm going to create a configuration class in a separate Python module. Below you can see the new configuration class for this application, which is stored in the config.py module in the upper (!!!) level directory .



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

Pretty simple, isn't it? Configuration parameters are defined as class variables within the Config class. Because 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 configuration set, I can create subclasses of it. But don't worry about it for now.


The SECRET_KEY configuration SECRET_KEY , which I added as the only configuration item, is an important 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 against a nasty attack called Cross-Site Request Forgery or CSRF (pronounced "seasurf"). As its name implies, the secret key must be secret, since the strength of the tokens and signatures generated by it depends on the fact that no one outside the circle of trusted persons accompanying the application knows about it.


The value of the secret key is specified as an expression with two terms to which the OR operator is attached. The first term looks for the value of an environment variable, also called SECRET_KEY. The second term is simply a hard-coded string. This is a template that you will see that I often repeat for configuration variables. The idea is that the value that appears from the environment variable is preferable, but if the environment does not define a variable, then a hard-coded string is used instead. When developing this application, the security requirements are small, so you can simply ignore this parameter and allow the use of a hard-coded string. But when this application is deployed on a production server, I will set a unique and hard-to-guess value, so the server will have a secure key that nobody knows.


Now that I have a configuration file, I need Flask to read it and apply it. This can be done immediately after creating an instance of the Flask application using the app.config.from_object() method ( app\__init__.py ):


 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 Config class may seem confusing at first, but if you look at how the Flask class (uppercase F) is imported from the flask package (lowercase f), you will notice that I do the same with configuration. The string "config" is the name of the python module config.py, and obviously, the one that has the upper case "C" is the actual class.


As I mentioned above, configuration items can be accessed with the dictionary syntax from app.config. Here you can see a quick session with the Python interpreter, where I check what the secret key value 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 in mind the separation of problems, I'm going to use the new app / forms.py module to store the web form classes. First, let's define a user login form in which the user will be prompted to enter a username and password. The form will also include the "Remember me" checkbox and the Submit button:


 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 flask_ naming convention for top-level imports. In this case, Flask-WTF changes all its characters under flask_wtf. Here, the base class FlaskForm is imported from the top of app/forms.py .

The four classes that represent the field types I use for this form are imported directly from the WTForms package, because the Flask-WTF extension does not provide custom versions. For each field, an object is created as a class variable in the LoginForm class. Each field is assigned a description or label as the first argument.


The optional validators argument, which you see in some fields, is used to bind the verification behavior to fields. The DataRequired validator 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 visualized on the web page. The fields defined in the LoginForm class know how to visualize themselves as HTML, so this task is quite simple. Below you can see the login template, which I am going to store in the file app/templates/login.html :


app/templates/login.html : Login Form Template
(The code is corrected due to changes in the original article 2018-06-09. Thank you magax !)

 {% 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 reuse base.html one more time, as shown in Chapter 2 , through the template's extended inheritance instruction. In fact, I will do this with all the templates to provide a uniform layout that includes the top navigation bar across all pages of the application.


This template assumes that the form object created from the LoginForm class will be provided as an argument, which can be seen as a form link. This argument will be sent by the input viewer function, which I have not yet written.


The HTML <form> element is used as a container for the web form. The form's action attribute is used to tell the web browser the URL that should be used when sending information entered by the user into the form. If an empty string is specified for the action, the form is transferred to the URL address currently located in the address bar, that is, the URL that renders the form on the page. The method attribute specifies the HTTP request method to be used when sending the form to the server. By default, it is sent with a GET request, but in almost all cases the use of a POST request improves interaction with the user, because requests of this type can send form data to the request body, while GET requests add form fields to the URL address, cluttering the address browser string.


Added 2018-06-09 Thanks for the comments magax !
The novalidate attribute novalidate used to instruct the web browser not to apply validation to the fields in this form, which effectively leaves this task to the Flask application running on the server. Using novalidate is completely optional, but for this first form, it is important that you install it, because it will allow you to test the server side check later in this chapter.

The form.hidden_tag() template argument creates a hidden field containing a token used to protect the form from CSRF attacks. All you need to do to protect the form is to enable this hidden field and define the SECRET_KEY variable in the Flask configuration. If you take care of these two things, Flask-WTF does the rest for you.


If you have already written HTML Web Forms in the past, you may have thought it strange that there are no HTML fields in this template. This is because the fields from the Form object know how to visualize themselves as HTML. All I had to do was enable {{ form.<field_name>.label }} in the place where the field label is needed and {{ form.<field_name>() }} where the field itself is needed. For fields that require additional HTML attributes, they can be passed as arguments. The username and Password fields in this template take the size size as an argument, which will be added to the <input> HTML element as an attribute. In this way, you can also attach CSS classes or identifiers to form fields.


Form submission


The final step before you see this form in the browser is the code for the new viewing function in the application that displays the template from the previous section.


So let's write a new view function that is mapped to the /login URL, which will create the form, and pass it to the rendering template. This browsing feature can be found in the app/routes.py complementing the content:


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

Here I imported the LoginForm class from forms.py , created an instance of an object from it, and sent it to the template. The form = form syntax may look weird, but simply passes the form object created in the line above (and shown to the right) to the template with the form of the name (shown to the left). This is all that is required to display form fields.


To simplify access to the login form, the basic template should include a link to the navigation bar:


 <div> Microblog: <a href="/index">Home</a> <a href="/login">Login</a> </div> 

At this stage, you can run the application and see what happened with the web browser. When launching the application, enter http://localhost:5000/ in the address bar of the browser, and then click the Sign In link in the top navigation bar to see the new login form. Pretty cool, isn't it?



Getting form data


If you try to click the send button (Sign In), the browser displays the error "Method Not Allowed". This is due to the fact that the login function from the previous section performs half the task. It can display a form on a web page, but it has no logic to process the data submitted by the user. This is another area where Flask-WTF makes it easy. The following is an updated version of the view function that accepts and validates user submitted data:


 from flask import render_template, flash, redirect @app.route('/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('/index') return render_template('login.html', title='Sign In', form=form) 

The first novelty in this version is the methods argument in the route designer. This is a message to Flask that this view function accepts GET and POST requests, overriding the default value, which should only accept GET requests. The HTTP protocol indicates that GET requests are those that return information to the client (in this case, a web browser). All requests in the application are still of this type. POST requests are usually used when the browser sends form data to the server (in fact, GET requests can also be used for this purpose, but this is not recommended). The “Method Not Allowed” error that the browser showed you before appears because the browser tried to send a POST request and the application is not configured to accept it. By providing the methods argument, you tell Flask to accept the methods of the query.


The form.validate_on_submit() method performs all form processing. When the browser sends a GET request to get a web page with a form, this method returns False , so in this case the function skips the if statement and proceeds to display the template in the last line of the function.


When the browser sends a POST request as a result of the user clicking the submit button, form.validate_on_submit() collects all data, runs all validators attached to the fields, and if everything is OK, returns True , stating that the data is valid and can be processed by the application. But if at least one field does not confirm the check, the function returns False , and this will result in the form being returned to the user, for example, in the case of a GET request. Later, I'm going to add an error message when the check failed.


When form.validate_on_submit() returns True , the login function calls two new functions imported from Flask. The flash() function is a useful way to display a message to a user. Many applications use this technique to tell the user whether an action was successful or not. In this case, I will use this mechanism as a temporary solution, because I do not have all the infrastructure necessary for registering users in reality. The best thing I can do now is show a message confirming that the application has received credentials.


The second new function used in the login function is redirect() . This function instructs the client web browser to automatically switch to another page specified as an argument. This browsing function uses it to redirect the user to the /index page of the application.


When you call the flash() function, Flask saves the message, but magic messages will not appear on web pages. Application templates should display these minimized messages in a way that works for the site layout. I'm going to add these messages to the base template so that all templates inherit this functionality. This is the updated base template:


 <html> <head> {% if title %} <title>{{ title }} - microblog</title> {% else %} <title>microblog</title> {% endif %} </head> <body> <div> Microblog: <a href="/index">Home</a> <a href="/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 use the with construct to assign the result of the get_flashed_messages() call to the messages variable, all in the context of the template. The get_flashed_messages() function comes from Flask and returns a list of all messages that were previously registered with flash() . The condition that follows checks whether the message has some content, in which case the <ul> element is displayed with each message as a list item <li> . This style of rendering does not look very good, but the theme of styling a web application will appear later.


An interesting feature of these flash messages is that after they are requested once through the get_flashed_messages function get_flashed_messages they are removed from the list of messages, so they appear only once after the flash() function is called.


Time to try the application again and check how the form works. Make sure that you are trying to submit a form with username or password fields blank to see how the DataRequired validator stops the DataRequired process.


Improved field validation


Validators attached to form fields prevent invalid data from being passed to the application. The way the application responds to invalid form field input is to re-display the form to allow the user to make the necessary corrections.


If you tried to enter invalid data, I am sure that you noticed that, although the checking mechanisms work well, there are no messages to the user that something is wrong with the form, the user simply receives the form back. , , , .


, , , , — .


( .) :


 {% extends "base.html" %} {% block content %} <h1>Sign In</h1> <form action="" method="post"> {{ 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 %} 

, , username password , , . , , , , form.<field_name>.errors . , .


, .




, . , . , :


 <div> Microblog: <a href="/index">Home</a> <a href="/login">Login</a> </div> 

login , redirect() :


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

, , .


, Flask url_for() , URL-, URL- . , url_for('login') /login , url_for('index') '/index . url_for() , view.


, URL-. , URL- , , . , , , URL- , URL , . url_for() URL-.


url_for() , URL . :


 <div> Microblog: <a href="{{ url_for('index') }}">Home</a> <a href="{{ url_for('login') }}">Login</a> </div> 

login() :


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

<<< previous next >>>


')

Source: https://habr.com/ru/post/346342/


All Articles