An API-centric project to learn how to bring Python to the Web through Flask, a micro webdevelopment framework. This is based on the Lynda.com course taught by Lalith Polepeddi (@polepeddi).
This project is based on the Lynda.com course of the same name: Learning Flask.
This project relies on virtualenv to safely install and run Flask without compromising other libraries on my machine.
To create a virtualenv environment, type in the root folder:
$ virtualenv venv
To activate it, navigate to the containing folder and type:
source venv/bin/activate
To stop working in the virtual environment, type:
$ deactivate
A few things need to happen to deploy the app to Heroku:
Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork worker model. The Gunicorn server is broadly compatible with various web frameworks, simply implemented, light on server resources, and fairly speedy.
$ pip install gunicorn
Create a file named requirements.txt that will list all of the Python libraries we have installed.
$ pip freeze > requirements.txt
This file will tell Heroku to run Flask using Gunicorn.
$ touch Procfile
Note the capital P.
Add this line to Procfile:
web: gunicorn routes:app
Note that there is no space between routes:
and app
. I had one and it broke the Heroku app!
Instructions can be found here: https://devcenter.heroku.com/articles/heroku-cli. I ended up using the macOS installer—I don't have brew on my work machine.
Type:
heroku create
Heroku will ask for credentials and return a custom URL.
Changes can be pushed directly to the Heroku app through:
git push heroku master
I've used Postgres.app, a full-featured PostgreSQL installation packaged as a standard Mac app.
To launch postgres in the CLI, type:
psql postgres
The command to create a new database is:
user=# create database learningflask;
Connect to the database:
user=# \c learningflask
Postgres should return:
You are now connected to database "learningflask" as user "user".
CREATE TABLE users (
uid serial PRIMARY KEY,
firstname VARCHAR(100) not null,
lastname VARCHAR(100) not null,
email VARCHAR(120) not null unique,
pwdhash VARCHAR(100) not null
);
See that the table is created and empty:
SELECT * from users;
INSERT INTO users (firstname,lastname,email,pwdhash) VALUES ('Thomas','Deneuville','thomas@thomasdeneuville.com','learning-flask');
The app uses the generate_password_hash
and check_password_hash
functions from werkzeug
. See this snippet page for more info.
We will need to install SQLAlchemy, a Flask extension for that.
pip install Flask-Migrate
Add a line of code in routes.py
:
app.config['SQLALCHEMY_DATABASE_URI'] = 'postgresql://localhost/learningflask'
The command is:
\q
Install Flask-WTF by getting back into the venv, then use:
$ pip install flask-wtf
We first insert a hidden tag to protect ourselves from CSRF attacks. In the signup.html
template, in the body of the <form>
:
{{ form.hidden_tag() }}
For each form group (field), we add the field label and the field box:
<div class="form-group">
{{ form.first_name.label }}
{{ form.first_name }}
</div>
The signup.html
page can see two methods: GET when the page is accessed and POST when the form is submitted. We need to account for that logic or we'll otherwise get a method error upon submit:
@app.route("/signup", methods=['GET', 'POST'])
def signup():
form = SignupForm()
if request.method == 'POST':
return "Success!"
elif request.method == 'GET':
return render_template("signup.html", form=form)
{% if form.first_name.errors %}
{% for error in form.first_name.errors %}
<p class="error-message">{{ error }}</p>
{% endfor %}
{% endif %}
{{ form.first_name }}
from wtforms.validators import DataRequired
to forms.py
to import the DataRequired
WTForms validator. Read more details on built-in validators.
We then make sure that these validators are declared for each field. It is possible to add a custom message, too. For example, on the email field:
email = StringField('Email', validators=[DataRequired("Please enter your email address."),Email("Please enter a valid email address.")])
Note that for using the Email
validator, I had to import it, too:
from wtforms.validators import DataRequired, Email
We'll also need to work on the template to loop through the errors that the validators return and display them. For each field in signup.html
:
<div class="form-group">
{{ form.first_name.label }}
{% if form.first_name.errors %}
{% for error in form.first_name.errors %}
<p class="error-message">{{ error }}</p>
{% endfor %}
{% endif %}
{{ form.first_name }}
</div>
Import the user
class from models
in routes.py
. We already import db
:
from models import db, User
Still in routes.py
, under the success case for the POST method, create a new user and pass the data we get from the form, field by field, using the .data
method:
newuser = User(form.first_name.data,form.last_name.data,form.email.data,form.password.data)
Next, we'll add the info to the database using db.session.add
and db.session.commit
:
db.session.add(newuser)
db.session.commit()
In routes.py
import: session, redirect,
and url_for
from Flask.
When a new user signs up (request.method == 'POST'
and form.validate() == True
), create a new session passing the email address:
session['email'] = newuser.email
And redirect to the homepage:
return redirect(url_for('home'))
- Create a new class called login form in
forms.py
- Create a new URL mapping for /login in
routes.py
- Create a web tempate in templates
Don't forget to import the LoginForm we created in forms.py
in routes.py
so we can use it in the new class we're creating.
Logging a user == creating a new session Logging a user out == deleting that session
We delete a session by using the session.pop()
method. In routes.py
:
@app.route("/logout")
def logout():
session.pop('email', None)
return redirect(url_for('index'))
We don't need to create a template for /logout
. When the page is requested, it simply deletes the cookie and redirects to the home page.
If a user is logged in, they shouldn't see the sign up or the log in form. They should be redirected to the home page.
If a user is logged out, they shouldn't have access to the home page (≠ from index). They should be redirected to the login page.
Let's add some logic in home()
:
if 'email' not in session: return redirect(url_for('login')) else: return render_template("home.html)
Under signup()
:
if 'email' in session: return redirect(url_for('home))
Add the same code under login()
.
Flask-Login does a really good job at that!
In forms.py
:
class AddressForm(Form):
address = StringField('Address', validators=[DataRequired("Please enter an address.")])
submit = SubmitField('Search')
Don't forget to import it to routes.py
! The import statement now looks like:
from forms import SignupForm, LoginForm, AddressForm
Remember: A form has two states: a POST and a GET.
Make sure that the route for home
has the two methods declared:
@app.route("/home", methods=['GET', 'POST'])
Since the page has a form.
We started by talking about Heroku, and thankfully, the author has published a wiki explaining in detail how to deploy the app on Heroku!