Flask: Difference between revisions
Line 745: | Line 745: | ||
{# application content needs to be provided in the app_content block #} | {# application content needs to be provided in the app_content block #} | ||
{% block app_content %}{% endblock %} | {% block app_content %}{% endblock %} | ||
</div> | |||
{% endblock %} | |||
</syntaxhighlight> | |||
==Quick Forms== | |||
This does look like a code saver but I do wonder how many will be happy with the software rendering the controls compared to you. Having said that it did do a pretty good job for a simple render. | |||
<br> | |||
First the old way | |||
<syntaxhighlight lang="css+jinja"> | |||
{% extends "base.html" %} | |||
{% block content %} | |||
<h1>Register</h1> | |||
<form action="" method="post"> | |||
{{ 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.email.label }}<br> | |||
{{ form.email(size=64) }}<br> | |||
{% for error in form.email.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.password2.label }}<br> | |||
{{ form.password2(size=32) }}<br> | |||
{% for error in form.password2.errors %} | |||
<span style="color: red;">[{{ error }}]</span> | |||
{% endfor %} | |||
</p> | |||
<p>{{ form.submit() }}</p> | |||
</form> | |||
{% endblock %} | |||
</syntaxhighlight> | |||
Using quick forms | |||
<syntaxhighlight lang="css+jinja"> | |||
{% extends "base.html" %} | |||
{% import 'bootstrap/wtf.html' as wtf %} | |||
{% block app_content %} | |||
<h1>Register</h1> | |||
<div class="row"> | |||
<div class="col-md-4"> | |||
{{ wtf.quick_form(form) }} | |||
</div> | |||
</div> | </div> | ||
{% endblock %} | {% endblock %} | ||
</syntaxhighlight> | </syntaxhighlight> |
Revision as of 13:10, 26 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>
We can include subtemplates like this.
{% for post in posts %}
{% include '_post.html' %}
{% endfor %}
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)
Adding Data
Once the database problems were resolve I could add data with the flask shell command. E.g.
flask shell
db
u1 = User(username='john', email='john@example.com')
u2 = User(username='sum', email='sum@example.com')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
u1.follow(u2)
u2.followers
u2.followers.all()
Authentication
Log In
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('main.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('main.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.
Sign Up
This consisted of
- making a form
- making a template
- adding the route
All of the code was straight forward.
Error Handling
First thing is probably
export FLASK_DEBUG=1
flask run myapp
Error Handler Page
Here is the custom error page. You will need to register the blueprint and make an __init__.py
from flask import render_template
from app.errors import bp
from app import db
@bp.app_errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
Here is the 404
{% extends "base.html" %}
{% block content %}
<h1>File Not Found</h1>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}
And the 500
{% extends "base.html" %}
{% block content %}
<h1>An unexpected error has occurred</h1>
<p>The administrator has been notified. Sorry for the inconvenience!</p>
<p><a href="{{ url_for('main.index') }}">Back</a></p>
{% endblock %}
Emailing the Error
Quite liked the proactive approach from the beginning. Lets define the extract config
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['your-email@example.com']
And configure it at startup.
import logging
from logging.handlers import SMTPHandler
...
if not app.debug:
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
toaddrs=app.config['ADMINS'], subject='Microblog Failure',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
Run a fake python SMTP server
python -m smtpd -n -c DebuggingServer localhost:8025
Logging to File
Here is some sample code for logging. This comes with rotate.
file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Microblog startup')
And some sample output
2021-05-25 03:51:27,196 INFO: Microblog startup [in /workspaces/bcnu1701flask/microblog/app/__init__.py:44]
2021-05-25 03:51:29,723 INFO: Adding Mail Handler [in /workspaces/bcnu1701flask/microblog/app/__init__.py:60]
Unit Testing
Example
The tutorial has a database and the relationships are like twitter. A follows B, B follows A and C etc. Below is an example of the testing for the database.
import unittest
from datetime import datetime, timedelta
from app import create_app, db
from app.models import User, Post
from config import Config
class TestConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = 'sqlite://'
class UserModelCase(unittest.TestCase):
def setUp(self):
self.app = create_app(TestConfig)
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
def test_password_hashing(self):
u = User(username='susan')
u.set_password('cat')
self.assertFalse(u.check_password('dog'))
self.assertTrue(u.check_password('cat'))
def test_avatar(self):
u = User(username='john', email='john@example.com')
self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/'
'd4c74594d841139328695756648b6bd6'
'?d=identicon&s=128'))
def test_follow(self):
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
self.assertEqual(u1.followed.all(), [])
self.assertEqual(u1.followers.all(), [])
u1.follow(u2)
db.session.commit()
self.assertTrue(u1.is_following(u2))
self.assertEqual(u1.followed.count(), 1)
self.assertEqual(u1.followed.first().username, 'susan')
self.assertEqual(u2.followers.count(), 1)
self.assertEqual(u2.followers.first().username, 'john')
u1.unfollow(u2)
db.session.commit()
self.assertFalse(u1.is_following(u2))
self.assertEqual(u1.followed.count(), 0)
self.assertEqual(u2.followers.count(), 0)
def test_follow_posts(self):
# create four users
u1 = User(username='john', email='john@example.com')
u2 = User(username='susan', email='susan@example.com')
u3 = User(username='mary', email='mary@example.com')
u4 = User(username='david', email='david@example.com')
db.session.add_all([u1, u2, u3, u4])
# create four posts
now = datetime.utcnow()
p1 = Post(body="post from john", author=u1,
timestamp=now + timedelta(seconds=1))
p2 = Post(body="post from susan", author=u2,
timestamp=now + timedelta(seconds=4))
p3 = Post(body="post from mary", author=u3,
timestamp=now + timedelta(seconds=3))
p4 = Post(body="post from david", author=u4,
timestamp=now + timedelta(seconds=2))
db.session.add_all([p1, p2, p3, p4])
db.session.commit()
if __name__ == '__main__':
unittest.main()
Outputting Results
Not written many python tests but without the call to unittest.main() there is no output.
Pagination In Flask
No alarms and no surprises here.
- Page 1, implicit: http://localhost:5000/index
- Page 1, explicit: http://localhost:5000/index?page=1
- Page 3: http://localhost:5000/index?page=3
This is managed with Flask-SQLAlchemy with the paginate command
user.followed_posts().paginate(1, 20, False).items
Introduction
Flask provides two packages for Flask. With my recent work on JWt will be interested in how this works.
pip install flask-mail
pip install pyjwt
With Flask extension, like express or other approaches you need to create and instance and then attach it to the app
# ...
from flask_mail import Mail
app = Flask(__name__)
# ...
mail = Mail()
...
def create_app(config_class=Config):
app = Flask(__name__)
...
mail.init_app(app)
from flask import render_template
from flask_mail import Message
from app import mail
def send_email(subject, sender, recipients, text_body, html_body):
msg = Message(subject, sender=sender, recipients=recipients)
msg.body = text_body
msg.html = html_body
mail.send(msg)
def send_password_reset_email(user):
def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email('[Microblog] Reset Your Password',
sender=current_app.config['ADMINS'][0],
recipients=[user.email],
text_body=render_template('email/reset_password.txt',
user=user, token=token),
html_body=render_template('email/reset_password.html',
user=user, token=token))
Let's plug this into password reset.
Creating the Boiler Plate
As ever we need a
- link
- form
- html
- route
This is here a reminder of how to go and put any page into a flask app
Link
<p>
Forgot Your Password?
<a href="{{ url_for('main.reset_password_request') }}">Click to Reset It</a>
</p>
Form
class ResetPasswordRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
submit = SubmitField('Request Password Reset')
class ResetPasswordForm(FlaskForm):
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Request Password Reset')
Html
{% extends "base.html" %}
{% block content %}
<h1>Reset Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<h1>Reset Your Password</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<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.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Route
from app.forms import ResetPasswordRequestForm
from app.email import send_password_reset_email
@app.route('/reset_password_request', methods=['GET', 'POST'])
@bp.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
if current_user.is_authenticated:
return redirect(url_for('main.index'))
form = ResetPasswordRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user:
send_password_reset_email(user)
flash('Check your email for the instructions to reset your password')
return redirect(url_for('main.login'))
return render_template('reset_password_request.html',
title='Reset Password', form=form)
@bp.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
if current_user.is_authenticated:
return redirect(url_for('main.index'))
user = User.verify_reset_password_token(token)
if not user:
return redirect(url_for('index'))
form = ResetPasswordForm()
if form.validate_on_submit():
user.set_password(form.password.data)
db.session.commit()
flash('Your password has been reset.')
return redirect(url_for('main.login'))
return render_template('reset_password.html', form=form)
Managing Jwt
Here is an example of encode and decode for the Python implementation of Jwt. Few surprises here
import jwt
from app import app
class User(UserMixin, db.Model):
# ...
def get_reset_password_token(self, expires_in=600):
return jwt.encode(
{'reset_password': self.id, 'exp': time() + expires_in},
app.config['SECRET_KEY'], algorithm='HS256')
@staticmethod
def verify_reset_password_token(token):
try:
id = jwt.decode(token, app.config['SECRET_KEY'],
algorithms=['HS256'])['reset_password']
except:
return
return User.query.get(id)
UI
Before
It looks pretty awful the app as it stands. Let see what bootstrap can do. Here it is before.
Install
We need to install the wrapper. No shown but we need to set up the bootstrap in the app too.
pip install flask-bootstrap
Base Template
{% extends 'bootstrap/base.html' %}
{% block title %}
{% if title %}{{ title }} - Microblog{% else %}Welcome to Microblog{% endif %}
{% endblock %}
{% block navbar %}
<nav class="navbar navbar-default">
... navigation bar here (see complete code on GitHub) ...
</nav>
{% endblock %}
{% block content %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{# application content needs to be provided in the app_content block #}
{% block app_content %}{% endblock %}
</div>
{% endblock %}
Quick Forms
This does look like a code saver but I do wonder how many will be happy with the software rendering the controls compared to you. Having said that it did do a pretty good job for a simple render.
First the old way
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form action="" method="post">
{{ 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.email.label }}<br>
{{ form.email(size=64) }}<br>
{% for error in form.email.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.password2.label }}<br>
{{ form.password2(size=32) }}<br>
{% for error in form.password2.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Using quick forms
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Register</h1>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}