Flaskで再利用できそうなコードたち（サインアップ、ログイン）

# はじめに

Flaskでwebアプリを作っていて、再利用できそうなコードがありましたので共有いたします。ここでは、どのコードがどのような機能を提供するか、については述べていないので、各自で調べていただきますようよろしくお願いします。

# 前提

### インストール

今回使う拡張を事前にインストールしておきます。全て最新のものを用意するので、バージョンの指定は必要ありません。

```:requirements.txt
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では呼び出しだけを行うので、次のようになります。

```python: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への登録、およびあらゆる初期化を行っています。

```python:__init__.py
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`にテーブルの定義をしています。ユーザー名、メールアドレス、ハッシュ化したパスワードをおいています。

余談ですが、「パスワードを忘れましたか？」ボタンを押してもパスワードを教えてくれないのは、そもそもデータベースに残っていないため、教えようにも教えられないからです。

```python: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)
```

# サインアップ処理

一般ユーザーのサインアップを想定しています。ユーザーは、

- ユーザー名
- メールアドレス
- パスワード
- 確認用パスワード

を入力します。

まずはサインアップフォームです。

```python:forms.py
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')
```

次に内部処理です。ユーザー名、メールアドレスはそれぞれデータベース内で重なってはいけないため、データベース内で一致していた場合エラーを出し、再度入力を求めます。

サインアップに成功したらログインフォームへジャンプします。

```python:views.py
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)

```

# ログイン処理

一般ユーザーのログインを想定しています。

ユーザーはメールアドレスとパスワードを入力します。サインアップフォームと同じファイル内に記述しています。

```python:forms.py
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`関数を用いて、そのユーザーのパスワードハッシュ値と比較します。

ログインに成功したら、任意の場所へとびます。

ついでに、ログアウト処理も置いておきます。

```python:views.py

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()は次のようにして受け取ります。

```html:example.html

{% with messages = get_flashed_messages() %}
  {% if messages %}
    {% for message in messages %}

      {{ message }}

    {% endfor %}
  {% endif %}
{% endwith %}
```

### ログイン状態前後で表示を変える

ログイン状態前後で表示を変えたい場合があります。例えば、ログインしたらログアウトボタンを表示する、などという場合です。

この場合、`current_user.is_authenticated`を使って、条件分岐できます。

```html:example.html

{% 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`さえ記されていれば制限できますが、ログインした状態でログインが必要ない場所へはアクセスできてしまいます。これが可能だと、ログインしているのにもう一度ログインできる、というよくわからない状況になってしまいます。そのような場合には、

```python
render_template('home.html')
```
ではなく

```python

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
```

# おわりに

これまで書いてきたコードで再利用できそうなものをまとめさせていただきました。もっといい書き方がある場合にはご指摘の程よろしくお願いいたします。