はじめに
Flaskでwebアプリを作っていて、再利用できそうなコードがありましたので共有いたします。ここでは、どのコードがどのような機能を提供するか、については述べていないので、各自で調べていただきますようよろしくお願いします。
前提
インストール
今回使う拡張を事前にインストールしておきます。全て最新のものを用意するので、バージョンの指定は必要ありません。
Flask
Flask-Login
Flask-Migrate
Flask-SQLAlchemy
Flask-WTF
SQLAlchemy
Werkzeug
WTForms
Jinja2
email-validator
pip install -r requirements.txt
ファイル構成
Flaskでは、Blueprint
というものを使って機能を分割しています。
├───main.py # main app.py file to be called to start server for web app
├───requirements.txt # File of pip install statements for your app
├───migrations # folder created for migrations by calling
├───myproject # main project folder, sub-components will be in separate folders
│ │ data.sqlite
│ │ models.py
│ │ __init__.py
│ │
│ │───auth
│ │ │ forms.py
│ │ │ views.py
│ │ │
│ │ │───templates
│ │ │ └───auth
│ │ │ login.html
│ │ │ signup.html
│ │
│ ├───static # Where you store your CSS, JS, Images, Fonts, etc...
│ ├───templates
│ base.html
│ home.html
この構成では、__init__.py
にconfig
を含んでいますが、config.py
として分けてもよいです。
base.html
に共通のレイアウトを用意しておいて、jinja2
というテンプレートエンジンを使って、{{% extends "base.html" %}}
として再利用しています(layout.hmtl
という名前も良く使われる)。
SQLにはSQLite
を使っていますが、Heroku
にはデプロイできず、スケールにも適していないため、SQLを使う場合は、実際にはMySQL
やPostgreSQL
などを使うとよいと思います。データベースの扱いにはSQLAlchemy
というORM
を使っているので、SQLが違っていても同じコードが利用できます(セットアップは違う)。data.sqlite
migrations
は勝手に作られるファイル/フォルダーです。
myproject\auth\templates\auth
のような冗長な構造をしているのは、Blueprint
の要請です。
auth\forms.py
にはサインアップ及びログインのフォームを、auth\views.py
ではフォームから入力されたデータを受け取って処理をし、レンダリングしています。
main.py
と__init__.py
main.pyでは呼び出しだけを行うので、次のようになります。
from flask import render_template
from myproject import app
@app.route('/')
def index():
return render_template('home.html')
if __name__ == '__main__':
app.run(debug=True)
このファイルでは、configとblueprintへの登録、およびあらゆる初期化を行っています。
import os
from flask import Flask, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
login_manager = LoginManager()
app = Flask(__name__, static_folder='static', template_folder='templates')
basedir = os.path.abspath(os.path.dirname(__file__))
# Heroku Postgresアドオンを追加した場合、環境変数DATABASE_URLに
# PostgreSQLデータベースの接続先URLがセットされるので、この値が
# セットされているとき(Heroku上で動作するとき)はそれを使い、
# セットされていないとき(ローカルでのデバッグなど)はローカルのSQLiteデータベースを使うようにする。
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URI') or 'sqlite:///' + os.path.join(basedir, 'data.sqlite')
app.config['SECRET_KEY'] = 'mysecretkey'
app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
Migrate(app, db)
# ここのimportは必ずdbの定義を終えてから
from myproject.auth.views import auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/authenticate')
login_manager.init_app(app)
login_manager.login_view = "auth.login"
最後の行のlogin_manager.login_view = "auth.login"
は@login_required
がつけられているのにも関わらず、そこへアクセスしようとしたときにリダイレクトされるログイン画面を指しています。
例えば、ログインしていな状態で\logout
へとぼうとすると、views.py
で定義したログイン関数が呼び出されます(関数のreturnでログイン画面をレンダリングしているので、その画面に行きます)。
ユーザーのデータベース
models.py
にテーブルの定義をしています。ユーザー名、メールアドレス、ハッシュ化したパスワードをおいています。
余談ですが、「パスワードを忘れましたか?」ボタンを押してもパスワードを教えてくれないのは、そもそもデータベースに残っていないため、教えようにも教えられないからです。
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from myproject import db, login_manager
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
def __init__(self, email, username, password):
self.email = email
self.username = username
self.password_hash = generate_password_hash(password)
def check_password(self,password):
return check_password_hash(self.password_hash, password)
サインアップ処理
一般ユーザーのサインアップを想定しています。ユーザーは、
- ユーザー名
- メールアドレス
- パスワード
- 確認用パスワード
を入力します。
まずはサインアップフォームです。
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired,Email,EqualTo
class SignupForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(),Email()])
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), EqualTo('pass_confirm', message='Passwords Must Match!')])
pass_confirm = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Sign Up')
次に内部処理です。ユーザー名、メールアドレスはそれぞれデータベース内で重なってはいけないため、データベース内で一致していた場合エラーを出し、再度入力を求めます。
サインアップに成功したらログインフォームへジャンプします。
from flask import (Blueprint, render_template,
redirect, url_for, request,
flash, abort)
from flask_login import login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from myproject import db, app
from myproject.models import User
from myproject.auth.forms import LoginForm, SignupForm
auth_blueprint = Blueprint('auth',
__name__,
template_folder='templates/auth')
@auth_blueprint.route('/signup', methods=['GET', 'POST'])
def signup():
form = SignupForm()
if form.validate_on_submit():
if User.query.filter_by(email=form.email.data).first():
flash('Email has been registered already!')
redirect(url_for('auth.signup'))
elif User.query.filter_by(username=form.username.data).first():
flash('Username has been registered already!')
redirect(url_for('auth.signup'))
else:
user = User(email=form.email.data,
username=form.username.data,
password=form.password.data)
db.session.add(user)
db.session.commit()
flash('Now You can login!')
return redirect(url_for('auth.login'))
return render_template('signup.html', form=form)
ログイン処理
一般ユーザーのログインを想定しています。
ユーザーはメールアドレスとパスワードを入力します。サインアップフォームと同じファイル内に記述しています。
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired,Email,EqualTo
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Log In')
class SignupForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(),Email()])
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired(), EqualTo('pass_confirm', message='Passwords Must Match!')])
pass_confirm = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Sign Up')
次に内部処理です。メールアドレスでユーザーを特定します。パスワードはmodels.py
で再生したcheck_password
関数を用いて、そのユーザーのパスワードハッシュ値と比較します。
ログインに成功したら、任意の場所へとびます。
ついでに、ログアウト処理も置いておきます。
from flask import (Blueprint, render_template,
redirect, url_for, request,
flash, abort)
from flask_login import login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
from myproject import db, app
from myproject.models import User
from myproject.auth.forms import LoginForm, SignupForm
auth_blueprint = Blueprint('auth',
__name__,
template_folder='templates/auth')
@auth_blueprint.route('/logout')
@login_required
def logout():
logout_user()
flash('You logged out!')
return redirect(url_for('index'))
@auth_blueprint.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
flash('something wrong!')
redirect(url_for('auth.login'))
elif user.check_password(form.password.data):
login_user(user)
flash('Logged in successfully.')
# login => somewhere
return redirect(url_for('somewhere'))
else:
pass
return render_template('login.html', form=form)
@auth_blueprint.route('/signup', methods=['GET', 'POST'])
def signup():
form = SignupForm()
if form.validate_on_submit():
if User.query.filter_by(email=form.email.data).first():
flash('Email has been registered already!')
redirect(url_for('auth.signup'))
elif User.query.filter_by(username=form.username.data).first():
flash('Username has been registered already!')
redirect(url_for('auth.signup'))
else:
user = User(email=form.email.data,
username=form.username.data,
password=form.password.data)
db.session.add(user)
db.session.commit()
flash('Now You can login!')
return redirect(url_for('auth.login'))
return render_template('signup.html', form=form)
flash()メッセージの表示
flash()は次のようにして受け取ります。
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% endwith %}
ログイン状態前後で表示を変える
ログイン状態前後で表示を変えたい場合があります。例えば、ログインしたらログアウトボタンを表示する、などという場合です。
この場合、current_user.is_authenticated
を使って、条件分岐できます。
{% if current_user.is_authenticated %}
<a class="nav-item nav-link" href="#">{{ current_user.username }}</a>
<a class="nav-item nav-link" href="{{ url_for('task.scheduletoday') }}">今日のスケジュール</a>
<a class="nav-item nav-link" href="{{ url_for('task.alltasks')}}">タスク一覧</a>
<a class="nav-item nav-link" href="{{ url_for('auth.logout') }}">ログアウト</a>
{% else %}
<a class="nav-item nav-link" href="{{ url_for('auth.login') }}">ログイン</a>
{% endif %}
のようにすればよいです。
ログインした状態でのアクセス制限
ログインしていない状態でログインが必要な場所へアクセスしようとした場合、@login_required
さえ記されていれば制限できますが、ログインした状態でログインが必要ない場所へはアクセスできてしまいます。これが可能だと、ログインしているのにもう一度ログインできる、というよくわからない状況になってしまいます。そのような場合には、
render_template('home.html')
ではなく
return redirect(url_for('somewhere')) if current_user.is_authenticated else render_template('home.html')
のようにすればよいです。
データベースの更新
テーブルを新たに定義したら、反映させるために、更新する必要があります。
---------------------------------------
MACOS/Linux
$ export FLASK_APP=main.py
Windows
$ set FLASK_APP=main.py
flask db init
# ここまでは最初だけ
----------------------------------------
flask db migrate -m "message"
flask db upgrade
のようにすればよいです。
ローカル環境で試す
python main.py
おわりに
これまで書いてきたコードで再利用できそうなものをまとめさせていただきました。もっといい書き方がある場合にはご指摘の程よろしくお願いいたします。