Web apps with Python and Flask
Content
●
Introduction
●
Routing
●
Templates
●
Forms
What is Flask?
●
Framework for creating web-applications with Python
●
Contains multiple modules
●
Based on Jinja templates
Installing
There are two ways to install Flask – in virtual environment
and system-wide
System-Wide
Just execute pip with root privileges:
$ sudo pip install Flask
Recommended way: in virtual environment
Hello world
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
●
We import the module Flask. An instance of this class will
be our WSGI application.
●
We create instance of this class. The first expected
parameter is the name of the module or the packet name
of the module. If you use a single module, we should use
__name__ .
●
We use the route() decorator to tell Flask which URL
triggers our function
●
The function returns the page content that we want the
browser to show to the user
●
Save this in the file hello.py. Don't name your application
flask.py, because it will cause conflict with the framework
names.
●
To start the application use the command flask or the -m
python modifier. For that purpose we should tell the
terminal which application to work with and we do this by
exporting the shell variable FLASK_APP
$ export FLASK_APP=hello.py
$ flask run
* Running on http://127.0.0.1:5000/
OR
$ export FLASK_APP=hello.py
$ python -m flask run
* Running on http://127.0.0.1:5000/
●
Run like this the application is visible only from the local
computer. The reason for this is, that by default it is started
in debug mode and system user will be able to execute
Python code on the server.
●
If we disable the debugger or we trust the network users,
we can make the server public by add the parameter:
--host=0.0.0.0:
flask run --host=0.0.0.0
This tells the system to listen to all public IPs.
Debug mode
●
If the debug mode is active, the server reloads automatically every time
the code is changed and provides useful debug information if something
does not work
●
To activate the debug mode we just export the shell variable
FLASK_DEBUG before starting the server:
$ export FLASK_DEBUG=1
$ flask run
This makes the following:
●
Activates the debugger
●
Activates the automatic reloading
●
Activates the debug mode of the Flask application
Routing
●
Modern web applications have nice URLs, that are easily
remembered or bookmarked.
●
That is the reason to use the route() decorator – to make
our web application able to have different sub-URLs for
different functions
Routing
@app.route('/')
def index():
return 'Welcome to our new web-site'
@app.route('/about')
def about():
return 'We are young and dynamic company, specialized in
...'
Routing
To make part of the URL variable we need to mark the section
with <variable_name>. Then this part of the URL will be
passed as a function argument.
@app.route('/user/<username>')
def show_user_profile(username):
return 'User %s' % username
@app.route('/post/<int:post_id>')
def show_post(post_id):
return 'Post %d' % post_id
Routing
The following converters are available:
string Accepts every text without / by default
int Accept integer values
float Accepts float arguments
path The same as string, but accepts /
any Accepts any kind of value
uuid Accepts UUID strings
Routing
@app.route('/about')
def about():
return 'We are...'
@app.route('/contacts/')
def contacts():
return 'To contact us...'
Routing
●
The difference between the two routs is, that contacts
does not have slash after it, but about does not.
●
When we call about on URL 127.0.0.1:5000/about the
mechanism kicks in and we get the content, but we call
127.0.0.1:5000/about/, we will get an error message for
non-existing URL address.
Routing
●
The page contacts can be accessed with and without the
slash at the end
●
http://127.0.0.1:5000/contacts/ - this works
●
http://127.0.0.1:5000/contacts - this also works
HTTP methods
The HTTP protocol supports different methods to access the
URL.
By default one route responds only to GET requests, but this
can be changed by passing a method to the route() decorator
The other often-used method is POST.
The difference is how we pass values from the HTML forms
@app.route('/userdata', methods=['GET', 'POST'])
Static files
Web applications use static files, for example for :
●
CSS
●
Javascript
●
Images
●
others
The web server has to be configured to return them, but
during development time Flask can do it too.
Static files
We should create a sub-folder called static in the package
and its content will be available via route /static
To generate URLs for static files, we use static in the url_for
function:
url_for('static', filename='style.css')
In this case the file should be placed in static/style.css.
HTML templates
●
In Flask applications the html is not generated manually
●
Flask uses the Jinja2 template engine
●
To render a template we need to use the method
render_template().
●
It accepts as a first argument the template name and the
variables we want to pass to the template engine
HTML templates
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)
HTML templates
<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
HTML templates
●
The template use the Jinja2 syntax
●
Special characters are escaped by default
HTML templates
Control structures in templates
Jinja2 templates support control structures, written in blocks enclosed like this:
{%...%}
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>Welcome to microblog</title>
{% endif %}
</head>
...
</html>
HTML templates
Loops in templates
When we get our data from a database for example, we often
want to show multiple records of the sam type in the page –
pictures, products, profiles, posts, etc. To do so we use loops.
Loops allow us to define how a single element will look like an
then to iterate over all the elements to multiply the template
element.
HTML templates
def planedata():
…
return render_template('planes.html', data =
data[form.planename.data])
…
HTML templates
{% for datum in data %}
<div><p><b>{{ datum.name }}</b>: {{ datum.value
}}</p></div>
{% endfor %}
HTML templates
def index():
user = {'nickname': 'Miguel'} # fake user
posts = [ # fake array of posts
{
'author': {'nickname': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'nickname': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
return render_template("index.html", title='Home', user=user, posts=posts)
HTML templates
Template inheritance
Sometimes we want to have fragments that are used on
different places. Instead of repeating the code, we can take
advantage of the Jinja2 inheritance.
For example if we want to have a menu on every page, we
can inherit the base template that contains all the common
page elements.
HTML templates
<html>
<head>
<title>Fighter plane DB</title>
</head>
<body>
<a href="/planes">Planes</a>
<hr>
<a href="/about">About us</a>
{% block content %}{% endblock %}
</body>
</html>
HTML templates
●
We create a base template containing all the common
elements
●
It contains the main HTML document structure(head, body,
title)
●
We define a block, where the inherited templates will put
their content - {% block content %}{% endblock %}
HTML templates
After that in the inherited template we put only the content,
specific for that page
{% extends "base.html" %}
{% block content %}
{% for plane in planes %}
<div><p>{{ plane.data }} : {{ plane.value }}</p></div>
{% endfor %}
{% endblock %}
Forms
To handle forms in Flask we use the Flask-WTF module. It
integrates the WTForms with Flask. The functionality if offers
includes:
●
WTForms integration.
●
Secure forms with CSRF token.
●
Global CSRF protection.
●
ReCAPTCHA support.
●
File upload, based on Flask-Uploads.
●
Internationalization with Flask-Babel.
Forms
Configuration is done by assigning values to the
corresponding keys in the app.config dictionary
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['WTF_CSRF_ENABLED'] = True
app.config['WTF_CSRF_SECRET_KEY'] = 'you-will-never-
guess'
app.config['SECRET_KEY'] = 'very-dark-secret'
Forms
●
WTF_CSRF_ENABLED – ON by default in recent
versions. Means cross-site request forgery
●
WTF_CSRF_SECRET_KEY – Key to generate a hash if a
CSRF protection is active
●
SECRET_KEY – key to encrypt the whole application If it
is available, the previous one is needed only if we want to
be different from this one
Forms
Web forms Flask-WTF classes, derived from FlaskForm.
Such a class just defines the form fields as class attributes
class UserProfileForm(FlaskForm):
useryear = StringField('year', validators=[DataRequired()])
usermonth = StringField('month', validators=[DataRequired()])
userday = StringField('day', validators=[DataRequired()])
Forms
<form action="" method="post" name="login">
{{ form.hidden_tag() }}
<p>
Birth year:<br>
{{ form.useryear(size=8) }}<br>
{% for error in form.useryear.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}<br>
Month:<br>
{{ form.usermonth(size=40) }}<br>
Forms
{% for error in form.usermonth.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}<br>
Day:<br>
{{ form.userday(size=40) }}<br>
{% for error in form.userday.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}<br>
</p>
<p><input type="submit" value="Submit"></p>
</form>
Login
In order to manage sessions of registered users, we use the
LoginManager module
After we create the Flask instance
app = Flask(__name__)
We should create a LoginManager instance
lm = LoginManager()
lm.init_app(app)
Login
lm.login_view = 'login'
lm.session_protection = 'strong'
In the login_view attribute we write the name of the view,
responsible for the user login
Login
We will need a new form for that. We can put it in the main
file, but it will be to cluttered. For that reason we move all the
forms in a separate file.
In the main file we add
from forms import *
Login
We create new file forms.py with the following content:
from flask_wtf import FlaskForm
from wtforms import StringField, BooleanField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):# login
userid = StringField('userid', validators=[DataRequired()])
userpass = StringField('userpass', validators = [DataRequired()])
remember_me = BooleanField('remember_me', default = False)
And we move the NewPlaneForm, as well as the required
imports
Login
We will make the form for inserting new plane available only
to registered users by creating a static registration (unbound
to the database)
We add the following decorator to the form processing
function
@login_required
@app.route('/form', methods=['GET', 'POST'])
def planedata():
Login
We will have to add a new function, processing the login form:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
if authenticate_user(form.userid.data,
form.userpass.data):
Login
remember_me = False
session.permanent = True
if 'remember_me' in session:
remember_me = session['remember_me']
session.pop('remember_me', None)
user = User()
login_user(user, remember = remember_me)
next = request.args.get('next')
if not is_safe_url(next):
return flask.abort(400)
return redirect(next or url_for('index'))
return render_template('login.html', title='Sign In',form=form)
Login
The line
if authenticate_user(form.userid.data, form.userpass.data)
Calls a function that we need to implement
def authenticate_user(usr, passw):
if usr=='admin':
return passw=='123456'
Login
The line
user = User()
Creates a new instance of the created by us class User
class User(object):
id = "admin"
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
Login
@property
def is_anonymous(self):
return False
def get_id(self):
try:
return unicode(self.id) # python 2
except NameError:
return str(self.id) # python 3
def __repr__(self):
return '<User %r>' % (self.nickname)
Login
In a real Flask application the user will be a model, bound to the
database, most probably with the help of the sql-alchemy module
and will look like this:
id = db.Column(db.Integer, primary_key=True)
nickname = db.Column(db.String(64), index=True,
unique=True)
password = db.Column(db.String(256), index=True)
email = db.Column(db.String(120), index=True, unique=True)
posts = db.relationship('Post', backref='author', lazy='dynamic')
Login
On the line
login_user(user, remember = remember_me)
We call a function, defined in the flask_login module, in order
to allow the user in the system
Login
To redirect the user to the page only for registered users we
first need to validate the next parameter. We get it from the
request, available in the flask module with the same name.
We start with
from flask import request
And we extract the next parameter
next = request.args.get('next')
The validation is done by another function we have to
implement is_safe_url
if not is_safe_url(next):
return flask.abort(400)
Login
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ('http', 'https') and \
ref_url.netloc == test_url.netloc
The function urlparse and urljoin are defined the module
urlparse
from urlparse import urlparse, urljoin
Login
With the line:
return redirect(next or url_for('index'))
We redirect the user to the next page (that requres a login)or
to the first page
Login
When we use flask_login, we need to provide a user_loader
callback function. It is used to reload the user from the saved
in the session id. The function accepts as an argument the
unicode user ID and returns the corresponding user as an
object
@lm.user_loader
def load_user(id):
return User
Login template
<h1>Sign In</h1>
<form action="" method="post" name="login">
{{ form.hidden_tag() }}
<p>
Please enter your username:<br>
{{ form.userid(size=40) }}<br>
{% for error in form.userid.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}<br>
Login template
Please enter your password:<br>
{{ form.userpass(size=40) }}<br>
{% for error in form.userpass.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>
Links
●
https://blog.miguelgrinberg.com/post/the-flask-mega-
tutorial-part-i-hello-world
●
https://flask-wtf.readthedocs.io/en/stable/
●
http://flask.pocoo.org/docs/0.12/config/