Running through The Flask Mega-Tutorial Again

This post is part 1 of the "The Flask Mega Tutorial" series:

  1. Running through The Flask Mega-Tutorial Again
  2. The Flask Mega-Tutorial Rereading - Part 2

Table Of Contents

This tutorial1 is really amazing and worth my second learning.

Chapter 1: Hello, World!

This chapter talks about setting up virtualenv for the flask app and the structure is looking like this:

microblog/
  venv/
  app/
    __init__.py
    routes.py
  microblog.py

Then I need to set environment variable:

$ FLASK_APP=microblog.py

Then run in the working dir:

$ flask run

That's quite organized but I found it's not mandatory.

All I need is this:

microblog/
  venv/
  microblog.py

Then put things I need together in microblog.py along with extra lines to run the app2:

from flask import Flask

app = Flask(__name__)

@app.route('/')
@app.route('/index')
def index():
    return "Hello world!"

if __name__ == "__main__":
    app.run()

Then I could execute it through atom or terminal with a simple command:

$ python /home/sim/python-projects/microblog/app.py

Or set environment var and do that way.

However, that structure in the tutorial is more modular and makes everything clearer so I followed the structure.

To remember the env var automatically each time flask is executed:

$ pip install python-dotenv

Then inside .flaskenv:

FLASK_APP=microblog.py

Involved module:

from flask import Flask

Chapter 2: Templates

It's things about jinja2. I've been tangoing with it3 these days. So okay to me as well.
Involved module:

from flask import render_template

Chapter 3: Web Forms

  • app.config['SECRET_KEY'] from os.environ.get.
  • A login form with 'POST' and 'GET' method allowed.
    Package: flask-wtf
    Involved modules:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
  • Data response. Involved modules:
from flask import flash, redirect

The flashed messages can be obtained through get_flashed_messages. * url_for is also introduced.

from flask import url_for

Chapter 4: Database

Package:

  • flask-sqlalchemy (Flask-friendly wrapper to the popular SQLAlchemy package)
  • flask-migrate (This extension is a Flask wrapper for Alembic, a database migration framework for SQLAlchemy.)
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

Then Configure SQLALCHEMY_DATABASE_URI and SQLALCHEMY_TRACK_MODIFICATIONS.

  • Create models
  • Creating The Migration Repository
  • The First Database Migration
  • Database Upgrade and Downgrade Workflow
  • Database Relationships

Additionally sometimes when developing in sqlite I need to drop a column I need to edit the recent migration script in migrations by changing the upgrade function4:

from olembic import op #They already have this
...
def upgrade():
    with op.batch_alter_table('table_name') as batch_op:
        batch_op.drop_column('the_column_to_remove')
...

Chapter 5: User Logins

  • Password Hashing
  • Introduction to Flask-Login
  • Preparing The User Model for Flask-Login
  • User Loader Function
  • Logging Users In
  • Logging Users Out
  • Requiring Users To Login
  • Showing The Logged In User in Templates
  • User Registration

Import:

Password Hashing

from werkzeug.security import generate_password_hash, check_password_hash
# generate_password_hash is a function receiving a string parameter and hashing it to an encrypted string. Different returned hash even for the same password.
# check_password_hash is a function receiving two parameters, first being a hash, second being a string parameter as a password to see if the original password from the hash is the same as the password.

Introduction to Flask-Login

New package needed: flask-login

from flask_login import LoginManager
# LoginManager is a function receiving Flask app object as a parameter
# LoginManager(app=None, add_context_processor=True)[source]

Preparing The User Model for Flask-Login

# ...
from flask_login import UserMixin
# UserMixin provides generic implementations that are appropriate for most user model classes
class User(UserMixin, db.Model):

User Loader FUnction

from app import login # Here login is an object created from LoginManager function
# ...

@login.user_loader
def load_user(id):
    return User.query.get(id)

Logging Users In

# ...
from flask_login import current_user, login_user
from app.models import User

# ...
# User Class: is_authenticated, is_active, is_anonymous, get_id()
# login_user(user, remember=False, duration=None, force=False, fresh=True)
# current_user: A proxy for the current user.

Logging Users Out

# ...
from flask_login import logout_user

# ...
# logout_user(): Log the current user out

Requiring Users To Login

from flask_login import login_required
# login_required(func) does the trick
from flask import request
from werkzeug.urls import url_parse
...
next_page = request.args.get('next') # This get the argument next, useful in this case
if not next_page or url_parse(next_page).netloc != '':
    next_page = url_for('index')
...
#Usage above

Showing The Logged In User in Templates

Jinja2 things

User Registration

A form similar to login form with

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from app.models import User

# ...

class RegistrationForm(FlaskForm):
    username = StringField('Username', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Register')

    def validate_username(self, username):
        user = User.query.filter_by(username=username.data).first()
        if user is not None:
            raise ValidationError('Please use a different username.')

    def validate_email(self, email):
        user = User.query.filter_by(email=email.data).first()
        if user is not None:
            raise ValidationError('Please use a different email address.')

THen jinja2 things.

P.S. Hmm, to work in atom, install linter-pylint, linter-pycodestyle

This pylintrc is helpful:

[MASTER]

load-plugins=pylint_flask, pylint_sqlalchemy
ignored-modules=flask_sqlalchemy

Chapter 6: Profile Pages

  1. Create a route with @login_required function passing queried user and posts to the returned template
  2. Edit the template to include the vars
  3. Include profile link in the nav

Avatars

  • Hash the email and use it in the url
>>> from hashlib import md5
>>> 'https://www.gravatar.com/avatar/' + md5(b'john@example.com').hexdigest()
'https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6'

Available argument in the url:

d: default avatar, can be a url-encoded url.
s: size

  • Automatize the process in the module as a function to the User model.

Using Jinja2 Sub-Templates

  1. A sub-template _post
  2. reference it in the profile template

More Interesting Profiles

  1. Add two columns about_me, last_seen to the User model.
  2. Insert them in the profile template

Recording The Last Visit Time For a User

@app.before_request function to generate time and save it the column last_seen of the given user.

Profile Editor

  1. A FlaskForm object
  2. A template for it.
  3. @app.route with methods=['GET', 'POST']