flask_sqlalchemyを使ってみる(MariaDBと接続）

#Flaskで練習アプリ作成の目次
docker-composeで作成したFlaskで色々試してみます。
１．[環境構築【docker-composeでNginx+Gunicorn+Flask+MariaDB+phpMyAdmin】](https://qiita.com/kk7679/items/9a3a1088f1b0b3ffc93a)
２．[Flask+RoboBrowserでスクレイピングした結果をbootstrap(honoka)で表示](https://qiita.com/kk7679/items/f12e2794b1bb0625d9fa)
**３．flask_sqlalchemyを使ってみる(MariaDBと接続）※本記事**
４．flask_loginを使ってみる（ログイン機能実装）

#内容
[前回記事](https://qiita.com/kk7679/items/f12e2794b1bb0625d9fa)で作成したものを利用します。
今回試したことです。
１．configファイルをinstanceディレクトリに作成して読み込むようにします。
２．データベース周りの設定を__init_.pyに追加します。**（メインテーマ）**
３．models.pyを作成し、データベースのモデル部分を記載します。**（メインテーマ）**
４．docker-composeでbuild時にinit.sqlでusersテーブルとrolesテーブルを作成します。
５．base.htmlにnavbarを作成します。（ランキング・ユーザー管理）
６．ユーザ一覧・ユーザ新規登録の機能を実装します。**（メインテーマ）**
　（Mariadbに保存されている情報取得と新規登録されたユーザ情報を保存します。）
７．auth.pyを作成し、パスワードをハッシュ化します。

#完成形
navbarの一覧表示を選択すると登録ユーザ一覧が表示されます。
![301.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188071/731063a4-ce81-bb2a-1bcf-c763f886ff8b.jpeg)
![302.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188071/9ba2e37c-8a0c-6ac3-f81e-8bfcf17657a6.jpeg)
navbarの新規登録を選択すると登録画面に遷移し、新規登録ができます。
![303.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188071/7d6b8ae6-7d70-b4d9-1b5b-df1b18bac35a.jpeg)

![304.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/188071/ecd7a410-ee0c-dc9d-3086-b3c5bac7c52b.jpeg)


#ソース
[https://github.com/kk7679/nginx-flask-maria-honoka2.git](https://github.com/kk7679/nginx-flask-maria-honoka2.git)

#ディレクトリ構成
アプリ部分は以下の6つに分割されています。
起動：startup.py
本体：\__init__.py
DBモデル：models.py
ページ：views.py
別機能：scrap.py
認証：auth.py

```
.
├── README.md
├── db
│   ├── Dockerfile
│   ├── backup
│   │   └── init.sql    ※DBの初期化用ファイル
│   └── conf.env    ※DBの設定ファイル
├── docker-compose.yml
├── nginx
│   ├── Dockerfile
│   ├── default.conf
│   ├── log
│   │   ├── access.log
│   │   └── error.log
│   └── nginx.conf
├── phpmyadmin
│   ├── Dockerfile
│   └── conf.env
└── web
    ├── Dockerfile
    ├── application
    │   ├── __init__.py    ※アプリケーション本体ファイル
    │   ├── auth.py    #ハッシュ作成等認証関連の機能を担当
    │   ├── models.py    #DBのモデル部分を担当
    │   ├── scrap.py    #はてなブックマークよりリストを取得するスクリプト
    │   ├── static    #honoka4.3.1よりアップロード
    │   │   ├── css
    │   │   │   ├── bootstrap.css
    │   │   │   └── bootstrap.min.css
    │   │   ├── js
    │   │   │   ├── bootstrap.bundle.js
    │   │   │   ├── bootstrap.bundle.min.js
    │   │   │   ├── bootstrap.js
    │   │   │   ├── bootstrap.min.js
    │   │   │   └── jquery-3.4.1.min.js
    │   │   │   └── pass_check.js    #ユーザ登録時のパスワード再入力チェック用のスクリプト
    │   │   └── scss
    │   │       ├── bootstrap.scss
    │   │       └── honoka
    │   │           ├── _honoka.scss
    │   │           ├── _mixins.scss
    │   │           ├── _override.scss
    │   │           └── _variables.scss
    │   ├── templates
    │   │   ├── base
    │   │   │   └── base.html    #共通部分を記載 navbar, javascript
    │   │   ├── error.html    #新規ユーザー登録時のエラーページ
    │   │   ├── index.html    #honokaのサンプルを利用して作成
    │   │   ├── list.html    #scrap.pyの結果を表示
    │   │   ├── new-user.html    #新規ユーザー登録ページ
    │   │   └── user-list.html    #ユーザ一覧を表示
    │   └── views.py    #ページの表示部分を担当
    ├── instance
    │   └── config.py    ※アプリの設定ファイル（DBの接続情報など）
    ├── requirements.txt
    └── startup.py    ※アプリケーション起動ファイル
```

#instanceディレクトリ
instanceディレクトリを作成し、config.pyを保存します。
このファイル内にデータベースの接続情報を記載します。
instanceフォルダは慣例としてgithub管理外の設定ファイルをおいておくということになっているみたいですが、
あくまで慣例なだけなのでちゃんとgitignoreする必要があるそうです。

```config.py
import os

DB_USER = 'adminuser'  # docker-composeのconf.envに記載
DB_PASS = 'zaq1xsw2'   # docker-composeのconf.envに記載
DB_HOST = 'db'
DB_NAME = 'sample01'
db_uri = "mysql+pymysql://{0}:{1}@{2}/{3}?charset=utf8".format(DB_USER, DB_PASS, DB_HOST, DB_NAME)

SQLALCHEMY_DATABASE_URI = db_uri
DEBUG = True
SECRET_KEY = os.urandom(24)
```

#\_\_init\_\_.py
app = Flask(\__name__, instance_relative_config=<font color="Red">True</font>)とすることでinstanceディレクトリを
デフォルトのコンフィグディレクトリとして認識してくれます。
参考：[僕の考えるFlaskの設定まわりのベストプラクティス](https://www.smartbowwow.com/2018/07/flask.html)

上記コンフィグファイルを読み込んだ後にdb = SQLAlchemy(app) を記載することでdbという名前で
データベースを扱うことができるようになります。

```__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__, instance_relative_config=True) 
app.config.from_pyfile('application.cfg', silent=False) #コンフィグファイルの読み込み
#silent=False ファイルが見つからない場合はエラー発生させる
db = SQLAlchemy(app) 　#データベースを扱えるようにする

import application.views  #ルーティング関連を読み込み
import application.models  #データベースのモデル部分を読み込み
```

#models.py
データベースのモデル定義部分をmodels.pyにまとめます。
参考：[本田崇智 ゼロからFlaskがよくわかる本: Pythonで作るWebアプリケーション開発入門 - モデル を 作る](https://www.amazon.co.jp/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89Flask%E3%81%8C%E3%82%88%E3%81%8F%E3%82%8F%E3%81%8B%E3%82%8B%E6%9C%AC-%E6%9C%AC%E7%94%B0%E5%B4%87%E6%99%BA-ebook/dp/B07F2X9GRQ)


```models.py
from datetime import datetime

from application import db


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))
    password = db.Column(db.String(150))
    email = db.Column(db.String(50))
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    salt = db.Column(db.String(100))
    created_at = db.Column(db.Text)

    #ユーザ新規登録時に使用
    def __init__(self, name=None, password=None, email=None, role_id=None, salt=None):
        self.name = name
        self.password = password
        self.email = email
        self.role_id = role_id
        self.salt = salt
        self. created_at = datetime.utcnow()

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.Text)
    User = db.relationship('User', backref=db.backref('roles', lazy=True))
```

#init.sql
sampleユーザーのパスワードはデフォルトのソルトから手動作成しました。

- データベースsample01を作成
- テーブルusers, rolesを作成
- sampleデータ5件をインサート

#auth.py
パスワードのハッシュ化を担います。
２つの関数を作成しました。

1.ソルトを作成
2.パスワードとソルトを渡し、ハッシュ化されたパスワードを作成
decodeを行っているのはデータベースに保存する際にb'xxxxxx'の文字列として保存されると
パスワードの認証時にうまくいかなかったので事前に処理して保存しています。



参考1：​[パスワードをDBに保存する時の基礎の基礎的なこと](https://qiita.com/ms2sato/items/6005eea50def287090d0)
参考2：[【Udemy】現役シリコンバレーエンジニアが教えるPython 3 入門 + 応用（暗号化 hashlibのハッシュ）](https://www.udemy.com/course/python-beginner/)


```auth.py
import base64
import binascii
import hashlib
import os 

def create_salt():
    salt = base64.b64encode(os.urandom(64)).decode()
    return salt

def create_hash(password, salt):
    temp_password = hashlib.pbkdf2_hmac("sha512", password.encode('utf-8'), salt.encode('utf-8'), 100000)
    hash_password = binascii.hexlify(temp_password).decode()
    return hash_password


#テスト用です。
if __name__ == "__main__":
    salt = create_salt()
    # salt = "koR54sfR472wsdr0ol5TYdGhEfcm"　#init.sqlに設定したソルト
    password = '0o9i8u7y6t'  #テスト用パスワード
    hash_password = create_hash(password, salt)
    print('----salt-----')
    print(salt)
    print('-----hash_password-----')
    print(hash_password)
```

#views.py
ユーザーの新規登録、登録完了、エラーページへのルーティングを追加します。

```views.py
from flask import flash, render_template, request, url_for 

from application import app
from application import auth
from application import db
from application import scrap
from application.models import User, Role  #models.pyで作成したモデルをimport

### 途中省略  ###

@app.route('/new-user')
def new_user():
    title = '新規登録'
    roles = Role.query.all()    #役割の選択肢をデータベースより取得
    return render_template('new-user.html', title = title, roles = roles)

@app.route('/create-user', methods=['POST'])
def create_user():
    title = '登録完了'
    password =  request.form['password']
    salt = auth.create_salt()    #ソルトを作成
    hash_password = auth.create_hash(password, salt)  #作成したソルトと入力されたパスワードでハッシュ作成 
    email = request.form['email']

    # models.pyで作成したUserクラスをインスタンス化
    user = User(
        name = request.form['name'],
        password = hash_password,
        email = email,
        role_id = request.form['role'],
        salt = salt
    )
    
    # emailがDBに登録されていないか確認 
    email_exist = User.query.filter_by(email=email).first()
    
    # emailが既に存在しない場合はDBへ登録する
    if email_exist is None:
        db.session.add(user)   #インスタンス化したuserを書き込む準備
        db.session.commit()   #DBへ書き込み
        flash('登録を完了しました。')
        users = User.query.all()
        return render_template('user-list.html', title = title, users = users)

    # emailが既に存在する場合は登録しない
    else:
        title = '登録エラー'
        msg = 'すでに登録済みです。'
        return render_template('error.html', title = title, message = msg)

```

#base.html
ベースとなるHTMLテンプレートを作成し、すべてのHTMLテンプレートで読み込むようにします。
下記URLを参考にbase.htmlにnavbar部分を作成しました。
参考1：[Bootstrap4移行ガイド](https://cccabinet.jpn.org/bootstrap4/components/buttons)
参考2：[【Bootstrap】Navbarの使い方・カスタマイズ方法を徹底解説](https://www.sejuku.net/blog/75948)

新たにアップロードしたpass_check.jsの読み込みを追記しています。
参考3：[JavaScriptで同値チェックを実装してHTML5と同じデザインのエラーを出す方法](https://xn--web-s70e101eh74aq95bj6d.xn--vcki1fxh883oon2c.com/%E8%A8%98%E4%BA%8B/40/)

```base.html
    <!-- navbar部分省略 -->
    <!-- JQuery BootStrap javascript-->
    <script src="{{ url_for('static', filename='js/jquery-3.4.1.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
    <script src="{{ url_for('static', filename='js/pass_check.js') }}" type="text/javascript"></script>
</body>
</html>
```
#new-user.html
ユーザーの新規登録画面です。
レイアウトや見た目の調整に若干苦戦しました。
\<div class="mt-5 pt-3 ml-5">などのマージンやパディングをうまく調整して好みのレイアウトにしてください。
参考：[Bootstrap4でコンテンツのマージンやパディングを指定する](http://info-i.net/bootstrap-spacing)


```new-user.html
{% extends "base/base.html" %}

{% block content %}

<div class="mt-5 pt-3 ml-5">
    <hr>
    <h5 class="mb-3 ml-3">新規登録</h5>
    <hr>
    <form action="{{ url_for('create_user') }}" method="POST">
        <div class="form-group">
            <label for='name'>名前</label>
            <div class="row">
                <div class="col-sm-4">
                    <input class='form-control type='text' name='name' value='' pattern='{2,20}' autofocus required>
                </div>
            </div>
        </div>
        <div class="form-group mt-3">
            <label for="role">役割</label>
            <div class="row">
                <div class="col-sm-3">
                    <select class="form-control" name="role">
                        {% for role in roles%}
                            <option value="{{role.id}}">{{role.name}}</option>
                        {% endfor%}
                    </select>
                </div>
            </div>
        </div>
        <div class="form-group mt-3">
            <label for='email'>メールアドレス</label><br>
            <div class="row">
                <div class="col-sm-4">
                    <input class="form-control" type='email' name='email' placeholder='xxxxx@xxx.com' required>
                </div>
            </div>
        </div>
        <div class="form-group mt-3">
            <label for='password'>パスワード（半角英数６文字以上20字以下）</label><br>
            <div class="row">
                <div class="col-sm-4">
                    <!-- patternで数字アルファベットを含む6文字以上20文字以下でバリデーション -->
                    <input class="form-control" type='password' name='password' id="password" pattern='(?=.*[a-zA-Z])(?=.*\d).{6,20}' required> 
                </div>
            </div>
        </div>
        <div class="form-group mt-3">
            <label for='password'>パスワード確認</label><br>
            <div class="row">
                <div class="col-sm-4">
                    <!-- oninputでpass_check.jsの関数を呼び出しています。-->
                 <input type="password" class="form-control" name="confirm" oninput="CheckPassword(this)" required>
                </div>
            </div>
        </div>
        <div class="pt-4">
            <button class='btn btn-outline-dark mr-2 w-15' type='submit' onclick="OnButtonClick();">登録</button>
            <a href='/' class='btn btn-outline-dark'>キャンセル</a>
        </div>
    </form>
</div>


{% endblock %}  
```
#動作確認

```
$ git clone https://github.com/kk7679/nginx-flask-maria-honoka2.git
$ cd nginx-flask-maria-honoka2.git
$ docker-compose up -d --build
```