First and foremost: this : https://flask-sqlalchemy.palletsprojects.com/en/2.x/
This is the old Flask website built in flask. Some patterns are outdated. But still pretty nice to gloss over with a cup of tea and some buiscuits. 🙂 https://github.com/pallets/flask-website/tree/master/flask_website
scoped_session(sessionmaker()) or plain sessionmaker() ?
Well, the recommended pattern is to use scoped_session as it is considered thread safe. You create a global session object that is thread safe and use it in all of your methods. Here is where flask-sqlalchemy bindings help us. They give us this scoped session out of the box.
So when you do:
from flask_sqlalchemy import SQLAlchemy
app = Flask(__name__)
db = SQLAlchemy(app)
#create some transient objects inside some method
db.session.add(some_db_object)
-> here the .session object is thread safe.
Some really nice stuff from the official docs here: https://flask-sqlalchemy.palletsprojects.com/en/2.x/quickstart/#road-to-enlightenment
Road to Enlightenment
The only things you need to know compared to plain SQLAlchemy are:
- SQLAlchemy gives you access to the following things:
- all the functions and classes from sqlalchemy and sqlalchemy.orm
- a preconfigured scoped session called session
- the metadata
- the engine
- a SQLAlchemy.create_all() and SQLAlchemy.drop_all() methods to create and drop tables according to the models.
- a Model baseclass that is a configured declarative base.
- The Model declarative base class behaves like a regular Python class but has a query attribute attached that can be used to query the model. (Model and BaseQuery)
- You have to commit the session, but you don’t have to remove it at the end of the request, Flask-SQLAlchemy does that for you.
Relationship,/h1>
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
image_file = db.Column(db.String(20), default='default.jpg', nullable=False)
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):
__tablename__ = 'post'
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)
content = db.Column(db.Text, nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author = db.relationship('User', backref='posts', lazy=True)
def __repr__(self):
return f'Post("{self.title}", "{self.date_posted}"'
Here, there will be no actual column created on user table with name ‘posts’. Instead doing user.posts will actually emit sql query that will fetch all the posts by that user.
Also, notice that in author = db.relationship('User', backref='posts', lazy=True)
, we have used the ‘User’ class. While in the user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
, we used ‘user’ or the User table in the database.
Similarly, there will be no actual column with name “author” on the post table. If we do post_instance.author, it will actually emot an sql query and fetch the user associated with the post via the foreign key relationship
Create database tables
After we have specified the models (see above section for User and Post models), we can create them in the database like so:
- source your env.sh file that has the database connection string
- enter python console
>>>from app.py import db
(you will do this in the python console.Assuming that app.py is the file where Flask app is instantiated and has the linedb = SQLAlchemy(app)
>>>db.create_all()
That’s it. All tables will be created.
You can query for all Users like so: User.query.all()
and for all posts like so Post.query.all()
Hashing Password
pip install flask-bcrypt
>>> from flask_bcrypt import Bcrypt >>> bcrypt = Bcrypt() >>> bcrypt.generate_password_hash('my_password') b'$2b$12$/1cfni8WiIVjTApYPSiSoOB2CJN7medvggPt1UGWZ2KuQpgyzfFeq' >>> # above, b' indicates that hash is in bytes >>> # to generate hash in string, use .decode('utf-8') >>> bcrypt.generate_password_hash('my_password').decode('utf-8') '$2b$12$1nShtvoPU3DumzEqDXGcxee7GoXj5XfZZ8O7uJoG1SVd1e/.vhxzy' >>> # the password hash is salted. So, each subsequent try of same >>> # password results in a different hash >>> bcrypt.generate_password_hash('my_password').decode('utf-8') '$2b$12$R7lH3OlOep4BQ7oSOQMPm.Pei/SAo16AH1E6Ke8SZflMy7abdyubK' >>> # here is how you check the password against the hash >>> # my_password is the correct password. >>> hashed_password = bcrypt.generate_password_hash('my_password').decode('utf-8') >>> bcrypt.check_password_hash(hashed_password,'my_password') True >>> bcrypt.check_password_hash(hashed_password,'not_my_password') False >>>
Flask-Login
This library only deals with user authentication. Almost exactly same as Django auth library. Like auth library mainly deals with the User model provided by Django, similarly, this library also mainly deals with User model in your app. The way to wire it in is to use UserMixin in your app’s User model.
pip install flask-login
Then wire it up to the app instance like so (generally done in __init__.py file);
from flask_login import LoginManager
login_manager = LoginManager(app)
login_manager.login_view = 'login'
login_manager.login_message_category = 'info'
Here ‘login’ is the method name associated with login route. This line will cause redirections from protected pages to login page, if an anonymous user tries to access them.
login_message_category is used to provide Bootstap css class to style the flash message.
Next, you will need to add default methods (4 methods.. is_authenticated, is_active, is_anonymous, get_id()) on your app’s User model.
Since, this is such a common thing, use UserMixin provided by flask_login in your User model:
class User(db.Model, UserMixin): __tablename__ = 'users'
That’s it. Now you can use the login_user, is_authenticated and current_user like so:
Note: current_user is a convenience method given by flask_login. It returns the curent logged user. Its very useful in redirecting logged-in users when the try to visit the login or registration pages.
Note: login_user() is like makes a session ID for the authenticated user and sets the is_authenticated flag to True. logout_user() does the opposite.
in routes.py
:
from flask_login import login_user, logout_user, current_user, login_required
and then:
@app.route('/register', methods=['GET', 'POST']) def register(): if current_user.is_authenticated: return redirect(url_for('site_home')) form = RegistrationForm() if form.validate_on_submit(): hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8') new_user = User( username = form.username.data, email = form.email.data, password = hashed_password ) db.session.add(new_user) db.session.commit() flash(f'Your account has been created. Please login.', category='success') return redirect(url_for('login')) return render_template('register.html', title='Register', form=form) @app.route('/login', methods=['GET', 'POST']) def login(): if current_user.is_authenticated: return redirect(url_for('site_home')) form = LoginForm() if form.validate_on_submit(): user = User.query.filter(User.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('site_home')) else: flash(f'Login unsuccessful{form.email.data}!', category='danger') return render_template('login.html', title='Login', form=form) @app.route('/logout') def logout(): logout_user() return redirect(url_for('site_home'))
post/redirect/get
pattern
@app.route('/account', methods=['GET', 'POST']) @login_required def account(): form = UpdateAccountForm() if form.validate_on_submit(): current_user.username = form.username.data current_user.email = form.email.data db.session.commit() flash('Your account has been updated', 'success') return redirect(url_for('account')) # Important! we redirected to account page above instead of letting it goto account url # through render_template. This forces the client to send a get request message to the server. # this is called post-redirect-get pattern # this way, the browser does not get a warning "you are about to resubmit.." which # happens when you reload a browser right after submitting a form return render_template('account.html', title='Account', form=form)
Above, we have some AccountUpdateForm with fields like email, username that a user can use to update their account information. What I want to point out are the
return redirect(url_for('account'))
and return render_template('account.html', title='Account', form=form)
lines.
If you see, they both load the account.html. But we implement a pattern called post/redirect/get pattern, so that reloading a form right after submission does not have side-effects. This is because if you run render_template, right after form submission, your browser will pop-up a warning message.. like you are about to reload form.. which is browser’s way of telling you that you are doing something that will lead to a post request to the server. Explicitly redirecting forces browser to send a get request and then you don’t get that warning.
Migrations
Step 1
pip install flask-migrate
then in the __init__.py of the main application package, add the flask_migrate extension to the app like so:
from flask import Flask from flask_sqlalchemy import SQLAlchemy from flask_bcrypt import Bcrypt from flask_login import LoginManager from flaskblog.config import Config from flask_migrate import Migrate db = SQLAlchemy() bcrypt = Bcrypt() migrate = Migrate() login_manager = LoginManager() login_manager.login_view = "users.login" login_manager.login_message_category = "info" # import routes here. Since this file is also imported into routes.py, this will avoid # circular dependency as app has been already been defined so when # from flaskblog import app statement is hit in routes.py, app is already resolved. def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(Config) db.init_app(app) migrate.init_app(app, db) bcrypt.init_app(app) login_manager.init_app(app) from flaskblog.users.routes import users from flaskblog.main.routes import main from flaskblog.posts.routes import posts from flaskblog.errors.handlers import errors app.register_blueprint(users) app.register_blueprint(posts) app.register_blueprint(main) app.register_blueprint(errors) return app
now you will be able to see flask db
commands
(i)$ flask db Usage: flask db [OPTIONS] COMMAND [ARGS]... Perform database migrations. Options: --help Show this message and exit. Commands: branches Show current branch points current Display the current revision for each database. downgrade Revert to a previous version edit Edit a revision file heads Show current available heads in the script directory history List changeset scripts in chronological order. init Creates a new migration repository. merge Merge two revisions together, creating a new revision file migrate Autogenerate a new revision file (Alias for 'revision... revision Create a new revision file. show Show the revision denoted by the given symbol. stamp 'stamp' the revision table with the given revision; don't run... upgrade Upgrade to a later version
Step 2
Now create a migration repository. This is done using a flask db init
command
(i)$ flask db init /home/helios/Python/projects/Flask/flask_tut/venv/lib/python3.7/site-packages/flask_sqlalchemy/__init__.py:835: FSADeprecationWarning: SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and will be disabled by default in the future. Set it to True or False to suppress this warning. 'SQLALCHEMY_TRACK_MODIFICATIONS adds significant overhead and ' Creating directory /home/helios/Python/projects/Flask/flask_tut/migrations ... done Creating directory /home/helios/Python/projects/Flask/flask_tut/migrations/versions ... done Generating /home/helios/Python/projects/Flask/flask_tut/migrations/script.py.mako ... done Generating /home/helios/Python/projects/Flask/flask_tut/migrations/env.py ... done Generating /home/helios/Python/projects/Flask/flask_tut/migrations/README ... done Generating /home/helios/Python/projects/Flask/flask_tut/migrations/alembic.ini ... done Please edit configuration/connection/logging settings in '/home/helios/Python/projects/Flask/flask_tut/migrations/alembic.ini' before proceeding.
As you see in the console output, running the above command creates a migrations
directory in your project and then puts some files inside it. Basically, this is an empty database migration repository. Each time we do a database migration, a migration script will be added to this migrations directory, specifically in the migrations/versions
directory.
Keep in mind that the entire migrations directory, with all of its contents needs to be committed to git. In other words, you should treat this directory and all of its contents as source code.
Step 3
Generate migrations script.
In this stage, we have two scenarios. New projects and Existing projects.
New Projects
flask db migrate
flask db upgrade
Existing Projects with existing database, that were never put under mogrations
Here running flask db migrate
will not generate any schema in the migrations/versions directory. This is because the database is up-to-date. The only way to generate the initial database migration in the migrations/versions directory is to change the SQLALCHEMY_DATABASE_URI variable in the config.py to point to a new and empty database. This way we trick flask-migrate into thinking that this is a from scratch migration. I generally don’t change the SQLALCHEMY_DATABASE_URI directly and instead keep my connection strings inside an env.sh file. So, I instead change the connection string in this file to point to an empty database. This step is only needed to generate this initial schema.
flask db migrate
Now you can change the connection string back to your database.
The next step according to the documentation is to run the upgrade
command, which executes the migration script and applies the changes in it to your database. Obviously this is also going to fail, because the database does not need updating. Instead you have to tell Flask-Migrate and Alembic that the database is up to date. You can do this with the stamp
command.
flask db stamp head
This command will add a alembic_version
table to your database, and will store the initial migration generated above as the current migration. The head
alias always points to the most recent migration, which in our case is the first and only one currently in the repository.
No Comments
You can leave the first : )