Flask: Difference between revisions
Line 266: | Line 266: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
=Logins= | =Logins= | ||
==Introduction== | |||
The Flask-Login manages state of logins so I guess it is a bit like passport.<br> | The Flask-Login manages state of logins so I guess it is a bit like passport.<br> | ||
<br> | <br> | ||
Line 279: | Line 280: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
==Protecting Routes== | |||
We can protect routes by add the folllowing decorator. | |||
<syntaxhighlight lang="py"> | |||
from flask_login import login_required | |||
@bp.route('/') | |||
@bp.route('/index') | |||
@login_required | |||
</syntaxhighlight> | |||
==Next== | |||
There is some explanation of the use of next on the tutorial so maybe worth logging here as passport does redirecting. | |||
<syntaxhighlight lang="py"> | |||
if form.validate_on_submit(): | |||
user = User.query.filter_by(username=form.username.data).first() | |||
if user is None or not user.check_password(form.password.data): | |||
flash('Invalid username or password') | |||
return redirect(url_for('login')) | |||
login_user(user, remember=form.remember_me.data) | |||
next_page = request.args.get('next') | |||
if not next_page or url_parse(next_page).netloc != '': | |||
next_page = url_for('index') | |||
return redirect(next_page) | |||
</syntaxhighlight> | |||
*If the login URL does not have a next argument, then the user is redirected to the index page. | |||
*If the login URL includes a next argument that is set to a relative path (or in other words, a URL without the domain portion), then the user is redirected to that URL. | |||
*If the login URL includes a next argument that is set to a full URL that includes a domain name, then the user is redirected to the index page. | |||
<br> | |||
The third case is in place to make the application more secure. To determine if the URL is relative or absolute, I parse it with Werkzeug's url_parse() function and then check if the netloc component is set or not. |
Revision as of 04:01, 24 May 2021
Introduction
Quick tour of the python framework flask. Most of this has been taken from https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world
When I started the project flask we not found. I either started a new terminal to fix the probably or install flask with
python3 -m pip install flask --upgrade
Getting Started
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
return '''
<html>
<head>
<title>Home Page - Microblog</title>
</head>
<body>
<h1>Hello, ''' + user['username'] + '''!</h1>
</body>
</html>'''
Routing
Templates
So flask like others such pug or egs has templates. Very similar indeed. It supports inheritance for navigation and footers
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
}
]
return render_template('index.html', title='Home', user=user, posts=posts)
And the template
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<h1>Hi, {{ user.username }}!</h1>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
</body>
</html>
Forms
Introduction
Flask uses Flask-WTF for forms which is a wrapper for WTFForms. To configure this we need a secret key which is attached to requests to help prevent CSRF. This is stored in the root or the project e.g. config.py and loaded by the
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
And in __init__.py
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
Form Example
Don't want to cut and paste too must from the example but here is the basics for forms
- Define the form in python
- Define the template
- Render the form
Python Code
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')
Template
The novalidate attribute is used to tell the web browser to not apply validation to the fields in this form, which effectively leaves this task to the Flask application running in the server. Using novalidate is entirely optional.
The form.hidden_tag() template argument generates a hidden field that includes a token that is used to protect the form against CSRF attacks.
{% 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) }}<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 %}
Rendering
from flask import render_template
from app import app
from app.forms import LoginForm
# ...
@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)
Databases
Tools of the Trade
For Flask we use flask-sqlalchemy which is a wrapper for SQLAlchemy which is in turn a ORM (Object Relational Mapper). Like Room we can also use Flask-Migrate which is a wrapper for Alembic and database migration tool.
SQLite Config Example
First we add the connection string to the config
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
Then initialise in the __init__.py by adding the db and the migrate
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import routes, models
Table Definition Example
Nothing to see here except the __repr__ which is the python equivalent of toString(). It really could use some descent formatting. Almost unreadable.
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self):
return '<User {}>'.format(self.username)
Initialize
The flask command relies on FLASK_APP being set to work so make sure this is set.
If flask not found look at comment at the top of this page. Make sure the comments are for your table and not users.
flask db init
flask db migrate
# flask db upgrade
Database Problems
It looks to me that the Flask 2.0 does not have concept of update/downgrade in it's current form as it looks for a application factory to work with. I have to change the tutorial to use the following
microblog.py
The shell_context_processor sets up the flask shell.
from app import create_app, db
from app.models import User, Post
app = create_app()
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
init.py
Had to change the __init__.py to below. This mean creating a blueprint and adding it to the app. The routes and forms were moved to app/main
from flask import Flask, current_app
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from config import Config
db = SQLAlchemy()
migrate = Migrate()
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
db.init_app(app)
migrate.init_app(app, db)
from app.main import bp as main_bp
app.register_blueprint(main_bp)
return app
from app import models
main package
As said above the blueprint is created in init.py. Good way to learn the approach with these problems.
init.py
from flask import Blueprint
bp = Blueprint('main', __name__)
from app.main import routes
Forms
These are unchanged
Routes
The routes now point to the blueprint rather than the app.
from flask import render_template, flash, redirect
from app.main.forms import LoginForm
from app.main import bp
@bp.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)
Logins
Introduction
The Flask-Login manages state of logins so I guess it is a bit like passport.
In the user model we specify we want to use the default implementations. The load_user is required as Flask-Login does not know about databases. Good example is provided.
from flask_login import UserMixin
@login.user_loader
def load_user(id):
return User.query.get(int(id))
class User(UserMixin,db.Model):
Protecting Routes
We can protect routes by add the folllowing decorator.
from flask_login import login_required
@bp.route('/')
@bp.route('/index')
@login_required
Next
There is some explanation of the use of next on the tutorial so maybe worth logging here as passport does redirecting.
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
flash('Invalid username or password')
return redirect(url_for('login'))
login_user(user, remember=form.remember_me.data)
next_page = request.args.get('next')
if not next_page or url_parse(next_page).netloc != '':
next_page = url_for('index')
return redirect(next_page)
- If the login URL does not have a next argument, then the user is redirected to the index page.
- If the login URL includes a next argument that is set to a relative path (or in other words, a URL without the domain portion), then the user is redirected to that URL.
- If the login URL includes a next argument that is set to a full URL that includes a domain name, then the user is redirected to the index page.
The third case is in place to make the application more secure. To determine if the URL is relative or absolute, I parse it with Werkzeug's url_parse() function and then check if the netloc component is set or not.