Python Flask Tutorial I: Full-Featured Web App from Corey Schafer

Tell you how to work with flask to create a website


Github code

The GitHub links for this tutorial are Browse

If name equals main explanation

Part 1 - Getting Started

$ pip install flask

$ python3 >>> import flask >>> exit() $ mkdir flask_blog

Cd to flask_blog then create flaskblog.py.

Copy this code to flaskblog.py

from flask import Flask # Flask => Name of the class app = Flask(__name__) # __name__ => Special variables in python, just the name of the module, when run that, __name__ = __main__, will see this later. Basically flask knows where to look for your templates and static files... @app.route("/") # a route = what we type in the browser to go to different pages. Route decorators. def hello(): return "Hello World!"

$ export FLASK_APP=flaskblog.py
$ flask run

* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Can change http://127.0.0.1:5000/ to http://localhost:5000/

$ export FLASK_DEBUG=1

or add this code into the main *.py file

if __name__ == '__main__': app.run(debug=True)

Can run by
$ python3 flaskblog.py

This run the script directly with python (if you don't want to keep setting those environment variables again whenerver shut down the terminal).

Add more route

@app.route("/about") def about(): return "<h1>About Page</h1>"

Part 2 - Templates

Create new folder 'templates'
Create inside 'templates' home.html & about.html

#home.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Home page</title> </head> <body> <h1>Home Page</h1> </body> </html>

Code inside flaskblog.py

from flask import Flask, render_template ... @app.route("/") @app.route("/home") def hello(): return render_template('home.html')

Same code to about.html

Fake data for posts

# flaskblog.py posts = [ { 'author': 'author no.1', 'title': 'title no.1', 'content': 'blog no.1 content', 'date_posted': 'May 01 2019' }, { 'author': 'author no.2', 'title': 'title no.2', 'content': 'blog no.2 content', 'date_posted': 'May 02 2019' } ] # change route 'home' return render_template('home.html', posts= posts)

Code inside home.html and about.html

<!-- <head> --> {% if title%} <title>Flask Blog | {{ title }}</title> {% else %} <title>Flask Blog</title> {% endif %}

More blog content to home page

<!-- <body> --> {% for post in posts %} <h1>{{ post.title }}</h1> <p>Posted by {{ post.author }} on {{post.date_posted }}</p> <p>{{ post.content }}</p> {% endfor %}

Templates Inheritance

Create layout.html inside templates folder

<!-- layout.html --> <!DOCTYPE html> <html lang="en"> <head> {% if title%} <title>Flask Blog | {{ title }}</title> {% else %} <title>Flask Blog</title> {% endif %} </head> <body> {% block content %} {% endblock %} </body> </html>
<!-- home.html --> {% extends "layout.html" %} {% block content %} {% for post in posts %} <h1>{{ post.title }}</h1> <p>Posted by {{ post.author }} on {{ post.date_posted }}</p> <p>{{ post.content }}</p> {% endfor %} {% endblock content %} <!-- should have "name" inside with endblock {% endblock content %} in case we have many block to manage, so name it like the open block tag for easier later -->
<!-- about.html --> {% extends "layout.html" %} {% block content %} <h1>About Page</h1> {% endblock content %}

Option + Command + U to check source page

Now, check the powerful of this inheritance method by using Bootstrap for entire pages of the website.

<!-- layout.html / <body> --> <div class="container"> {% block content %} {% endblock %} </div>

Reload the page hard refresh by Command + Shift + R (clear the cache)

Add code from this github link

  • create static folder
  • add main.css file
  • add main.css content from main.css gihub
  • add navigation.html & main.html to layout.html
  • add the css link to layout
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}"

Article layout in home

<article class="media content-section"> <div class="media-body"> <div class="article-metadata"> <a class="mr-2" href="#">{{ post.author }}</a> <small class="text-muted">{{ post.date_posted }}</small> </div> <h2><a class="article-title" href="#">{{ post.title }}</a></h2> <p class="article-content">{{ post.content }}</p> </div> </article>

Result

Part 3 - Forms and User Input

$ pip install flask-wtf

forms.py

from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField, BooleanField from wtforms.validators import DataRequired, Length, Email, EqualTo class RegisrationForm(FlaskForm): username = StringField('Username', validators=[DataRequired(), Length(min=2, max=20)]) email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')]) submit = SubmitField('Sign Up') class LoginForm(FlaskForm): email = StringField('Email', validators=[DataRequired(), Email()]) password = PasswordField('Password', validators=[DataRequired()]) remember = BooleanField('Remember Me') submit = SubmitField('Login')

Generate random key for SECRET_KEY

$ python3
>>> import secrets
>>> secrets.token_hex(16)
>>> '2833f00b7d64c20651131b5fa9f65287'

flaskblog.py

from forms import RegistrationForm, LoginForm app.config = ['SECRET_KEY'] = '2833f00b7d64c20651131b5fa9f65287'

register.html

{% extends "layout.html" %} {% block content %} <div class='content-section'> <form method="POST" action=""> {{ form.hidden_tag() }} <fieldset class="form-group"> <legend class="border-bottom mb-4">Join Today</legend> <div class="form-group"> {{ form.username.label(class="form-control-label")}} {{ form.username(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.email.label(class="form-control-label")}} {{ form.email(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.password.label(class="form-control-label")}} {{ form.password(class="form-control form-control-lg")}} </div> <div class="form-group"> {{ form.confirm_password.label(class="form-control-label")}} {{ form.confirm_password(class="form-control form-control-lg")}} </div> </fieldset> <div class="form-group"> {{ form.submit(class="btn btn-outline-info")}} </div> </form> </div> <div class="border-top pt-3"> <small class="text-muted"> Already Have An Account? <a class="ml-2" href="{{ url_for('login') }}">Sign In</a> </small> </div> {% endblock content %}

layout.html

<main role="main" class="container"> <div class="row"> <div class="col-md-8"> {% with messages = get_flashed_messages(with_categories=true) %} {% if messages %} {% for category, message in messages %} <div class="alert alert-{{ category }}"> {{ message }} </div> {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %} </div> ...

Validate the form's fields

register.html

...fieldset ...legend <div class="form-group"> {{ form.username.label(class="form-control-label")}} {% if form.username.errors %} {{ form.username(class="form-control form-control-lg is-invalid")}} <div class="invalid-feedback"> {% for error in form.username.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.username(class="form-control form-control-lg")}} {% endif %} <div class="form-group"> {{ form.email.label(class="form-control-label")}} {% if form.email.errors %} {{ form.email(class="form-control form-control-lg is-invalid")}} <div class="invalid-feedback"> {% for error in form.email.errors %} <span>{{ error }}</span> {% endfor %} </div> {% else %} {{ form.email(class="form-control form-control-lg")}} {% endif %} </div> <!-- add the same to password and confirm_password -->

Result 1

Copy to login.html (remove username and confirm_password)

Add methods and condition to route login

flaskblog.py

@app.route("/login", methods=['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): if form.email.data == "admin@blog.com" and form.password.data == "password": flash('You have been logged in!', 'success') return redirect(url_for('home')) else: flash("Login Unsuccessful. Please check email and password", 'danger') return render_template('login.html', title='login', form=form)

Result 2

Part 4 - Database with Flask-SQLAlchemy

May 03 2019

$ pip install flask-sqlalchemy

flaskblog.py

from flask_sqlalchemy import SQLAlchemy ... app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' db = SQLAlchemy(app) ... class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(20), unique=True, nullable=False) email = db.Column(db.String(120), unique=True, nullable=False) image_file = db.Column(db.String(20), nullable=False, default='default.jpg') password = db.Column(db.String(60), nullable=False) def __repr__(self): return f"User('{self.username}', '{self.email}', '{self.image_file}')" class Post(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), nullable=False) date_posted = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) ... from datetime import datetime

Author? One-to-many Relationship between Author(Post) & Author(User)

flaskblog.py

class User(db.Model): ... posts = db.relationship('Post', backref="author", lazy=True) class Post(db.Model): ... user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
>>> python3
>>> from flaskblog import db
>>> db.create_all()

>>> from flaskblog import User, Post
>>> user_1 = User(username="luke", email="luke@demo.com", password="password1")
>>> db.session.add(user_1)
>>> user_2 = User(username="luca", email="luca@demo.com", password="password2")
>>> db.session.add(user_2)
>>> db.session.commit()

>>> User.query.all()
[User('luke', 'luke@demo.com', 'default.jpg'), User('luca', 'luca@demo.com', 'default.jpg')]

>>> User.query.first()
User('luke', 'luke@demo.com', 'default.jpg')

>>> User.query.filter_by(username="luke").all()
[User('luke', 'luke@demo.com', 'default.jpg')]

>>> User.query.filter_by(username="luke").first()
User('luke', 'luke@demo.com', 'default.jpg')

>>> user = User.query.filter_by(username="luke").first()
>>> user
User('luke', 'luke@demo.com', 'default.jpg')

>>> user.id
1

>>> user = User.query.get(1)
>>> user
User('luke', 'luke@demo.com', 'default.jpg')

>>> user.posts
[]
>>> user.id
1
>>> post_1 = Post(title="Blog 1", content="First post content", user_id=user.id)
>>> post_2 = Post(title="Blog 2", content="Second post content", user_id=user.id)
>>> db.session.add(post_1)
>>> db.session.add(post_2) 
>>> db.session.commit()
>>> user.posts
[User('Blog 1', '2019-05-03 17:16:09.993101'), User('Blog 2', '2019-05-03 17:16:09.995732')]

>>> for post in user.posts:
...    print(post.title)
Blog 1
Blog 2

>>> post = Post.query.first()
>>> post
User('Blog 1', '2019-05-03 17:16:09.993101')

>>> post.user_id
1
>>> post.author
User('luke', 'luke@demo.com', 'default.jpg')

# Delete all database

>>> db.drop_all()

# Then create the new fresh one
>>> db.create_all()

# check if it ok
>>> User.query.all()
[]

Part 5 - Package Structure

May 05 2019

Create the models.py, move this code to the models

from flaskblog.py move to models.py

from flaskblog import db from datetime import datetime class User(db.Model): ... class Post(db.Model): ...

Create folder name flaskblog, inside create __init__.py

Move these files into "package" flaskblog

__init__.py

from flask import Flask from flask_sqlalchemy import SQLAlchemy app = Flask(__name__) app.config['SECRET_KEY'] = '5791628bb0b13ce0c676dfde280ba245' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' db = SQLAlchemy(app) from flaskblog import routes

Create routes.py

routes.py

from flask import render_template, url_for, flash, redirect from flaskblog import app from flaskblog.models import User, Post from flaskblog.forms import RegistrationForm, LoginForm ...

flaskblog.py only left

from flaskblog import app if __name__ == '__main__': app.run(debug=True)

Rename flaskblog.py = run.py

Part 6 - User Authentication

May 04 2019

$ pip install flask-bcrypt

>>> from flask_bcrypt import Bcrypt
>>> bcrypt = Bcrypt()
>>> bcrypt.generate_password_hash('testing')
b'$2b$12$e9mzug5NoJCwYUlH/aNp2.Q9zIodxiKMGrLAHO0x.BO4NeHzm2HZW'
>>> bcrypt.generate_password_hash('testing').decode('utf-8')
'$2b$12$5oH0c023RIKm0QroBwKNcu6Ndh/gflgBJt6A0EzUKUpkV2xVnZnMO'
>>> hashed_pw = bcrypt.generate_password_hash('testing').decode('utf-8')
>>> bcrypt.check_password_hash(hashed_pw, 'password')
False
>>> bcrypt.check_password_hash(hashed_pw, 'testing')
True

__init__.py

... from flask_bcrypt import Bcrypt ... bcrypt = Bcrypt(app) from flaskblog import routes

routes.py

... from flaskblog import app, db, bcrypt @app.route("/register", methods=['GET', 'POST']) def register(): form = RegistrationForm() if form.validate_on_submit(): hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') user = User(username=form.username.data, email=form.email.data, password=hashed_password) db.session.add(user) db.session.commit() flash('Your account has been created! You are now able to login', 'success') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form)

>>> from flaskblog import db
>>> from flaskblog .models import User
>>> user = User.query.first()
>>> user
User('lululu', 'lululu@gmail.com', 'default.jpg')
>>> user.password
'$2b$12$f/av35Sg02H8UopgGbt0VO6pBke9kXlTNV8rvLInyH.jO3iPOdpmy'

Check if the same user information has already be used

forms.py

def validate_field(self, field): if True: raise ValidationError('Validation message') ===== def validate_username(self, username): user = User.query.filter_by(username=username.data).first() if user: raise ValidationError('That username is taken, please choose another one') from flaskblog.models import User

Result for this step

Create the login system

After creating account, user can login and log out

$ pip install flask-login

__init__.py

... from flask_login import LoginManager ... login_manager = LoginManager(app)

models.py

... from flask_login import Usermixin @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) ... class User(db.Model, UserMixin): ...

routes.py

from flask_login import login_user ... def login(): form = LoginForm() if form.validate_on_submit(): user = User.query.filter_by(email=form.email.data).first() if user and bcrypt.check_password_hash(user.password, form.password.data): login_user(user, remember=form.remember.data) return redirect(url_for('home')) else: flash('Login Unsuccessful. Please check email and password', 'danger') return render_template('login.html', title='Login', form=form)

Result for this step

Logged-in user dont need to login again, when they press on Login button (Navigation bar)

routes.py

from flask_login import login_user, current_user ... def register(): if current_user.is_authenticated: return redirect(url_for('home')) ... def login(): if current_user.is_authenticated: return redirect(url_for('home')) ...

Create Log out button to replace Log In

routes.py

from flask_login import login_user, current_user, logout_user ... def register(): if current_user.is_authenticated: return redirect(url_for('home')) ... def login(): if current_user.is_authenticated: return redirect(url_for('home')) ...

layout.html

<div class="navbar-nav"> {% if current_user.is_authenticated %} <a class="nav-item nav-link" href="{{ url_for('logout') }}">Logout</a> {% else %} <a class="nav-item nav-link" href="{{ url_for('login') }}">Login</a> <a class="nav-item nav-link" href="{{ url_for('register') }}">Register</a> {% endif %}

Create account routes beside log out button

routes.py

@app.route("/account") def account(): return render_template('account.html', title='Account')

account.html

{% extends "layout.html" %} {% block content %} <h1>{{ current_user.username }}</h1> {% endblock content %}

layout.html

<div class="navbar-nav"> {% if current_user.is_authenticated %} <a class="nav-item nav-link" href="{{ url_for('account') }}">Account</a> <a class="nav-item nav-link" href="{{ url_for('logout') }}">Logout</a>

Check in required not show account.html page

routes.py

from flask_login import ... login_required ... @app.route("/account") @login_required def account(): ...

__init__.py

... login_manager.login_view ='login'

http://127.0.0.1:5000/account => http://127.0.0.1:5000/login?next=%2Faccount

from flask import ... redirect, request ... def login(): ... next_page = request.args.get('next') return redirect(next_page) if next_page else redirect(url_for('home'))
tags: Python Corey Schafer Flask Full Web Application I
Select a repo