【python】勉強メモ　速習Flask⑥



## はじめに
今回は、ログイン機能を実装します。

以下の記事の続きとなります。
[【python】勉強メモ 速習Flask①](https://qiita.com/qiitataro/items/d9b4b44255ae5082838a)
[【python】勉強メモ 速習Flask②](https://qiita.com/qiitataro/items/682faa9749d596cb3496)
[【python】勉強メモ 速習Flask③](https://qiita.com/qiitataro/items/5f12300c4b238d2b6170)
[【python】勉強メモ 速習Flask④](https://qiita.com/qiitataro/items/ae5eca426fbf7d1bea83)
[【python】勉強メモ 速習Flask⑤](https://qiita.com/qiitataro/items/8d7027f97e20a6e07cbc)

## 前回までの環境
#### ディレクトリ構造
```
pydir/
 ┣myproject/ ->仮想環境有効化のコマンドを実行＆flask runコマンドもここで実行
   ┣venv/
   ┣instance/
    ┣blog.db
   ┣hello.py
   ┣app.py
   ┣templates/
    ┣hello.html
    ┣index.html ->編集有り
    ┣base.html
    ┣create.html
    ┣singup.html ->今回作成
    ┣login.html ->今回作成
```

#### コード

前回まで作成した「新規登録」「更新」「削除」を行うコードを掲載しておきます。
```app.py
from flask import Flask
from flask import render_template
from flask import request
from flask import redirect
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import pytz

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"
db = SQLAlchemy(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title= db.Column(db.String(50), nullable=True)
    body = db.Column(db.String(300), nullable=True)
    created_at = db.Column(db.DateTime, nullable=False,default=datetime.now(pytz.timezone('Asia/Tokyo')))

# GETとPOSTを受け取れる様にする。
@app.route("/", methods=['GET', 'POST'])
def index():
    # ページにアクセスしたら（HTTP GETでアクセスしたら）
    if request.method == 'GET':
        # DBに登録されたデーターを全てPostで取得する。取得したデーターは、postsに、リスト形式で格納される。
        posts = Post.query.all()
        # index.htmlに表示するため、posts=postsで、取得したデーターを渡す。
        return render_template('index.html', posts=posts)


@app.route("/create", methods=['GET', 'POST']) 
def create():
    if request.method == 'POST':
        title = request.form.get('title')
        body = request.form.get('body')

        post = Post(title=title, body=body)

        db.session.add(post)
        # 変更を保存
        db.session.commit()

        return redirect('/')
    else:
        return render_template('create.html')

# update処理を作成
@app.route("/<int:id>/update", methods=['GET', 'POST']) 
def update(id):
    post = Post.query.get(id)
    
    if request.method == 'GET':
        return render_template('update.html', post=post)
    
    # 更新ボタンが押されると、POSTが送信されるめ、eles移行が実行される
    else:
        post.title = request.form.get('title') # 書き換え
        post.body = request.form.get('body') # 書き換え
        
        # 不要なのでコメントアウト
        # post = Post(title=title, body=body)
        
        # db.session.add(post) 更新なので、addする必要はないため削除
        # commitだけすれば良い
        db.session.commit()

        return redirect('/')
        # return render_template('create.html')

# delete処理作成
# deleteは、POSTが流れることはないため、GETのみにする
@app.route("/<int:id>/delete", methods=['GET'])

def delete(id):
    post = Post.query.get(id)
    
    # delete処理
    db.session.delete(post)
    db.session.commit()
    
    return redirect('/')    
```

```index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>

<a href="/create" role="button">新規作成画面</a>
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <a href="/{{post.id}}/update" role="button">編集</a>
    <!-- 削除ボタン作成 -->
    <a href="/{{post.id}}/delete" role="button">削除</a>
    <p>作成日時： {{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

```base.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- ここに、HTMLの共通化が埋め込まれる -->
    {% block content %}
    {% endblock %}
</body>
</html>
```

```create.html
{% extends "base.html" %}
{% block content %}
<h1>新規登録</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <input type="text" name="title">
    <label for="body">内容</label>
    <input type="text" name="body">
    <input type="submit" value="新規登録">
</form>
{% endblock %}
```

```update.html
{% extends "base.html" %}
{% block content %}
<h1>編集</h1>
<form method="POST">
    <label for="title">タイトル</label>
    <!-- value="{{ post.title }}" は、renderから情報を取得してくる-->
    <input type="text" name="title" value="{{ post.title }}">
    <label for="body">内容</label>
    <!-- value="{{ post.body }}" も、renderから情報を取得してくる-->
    <input type="text" name="body" value="{{ post.body }}">
    <input type="submit" value="更新">
</form>
{% endblock %}
```

## 事前準備

```
pip install flask-login
```

app.pyを以下の様に編集
```app.py
from flask import Flask
from flask import render_template
from flask import request
from flask import redirect
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin, LoginManager # 今回追加
import os # 今回追加 セッションを暗号化するために必要
from datetime import datetime
import pytz

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"

#今回追加
#セッション状態を暗号化するための設定
# os.urandom(24)でランダムな値を生成し、SECRET_KEYという環境変数にセットする
app.config['SECRET_KEY'] = os.urandom(24)
db = SQLAlchemy(app)

# 今回追加
# LoginManagerクラスのインスタンス化
login_manager = LoginManager()

# 今回追加
# loginManagerと今回作成しているappの紐づけを行う
login_manager.init_app(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title= db.Column(db.String(50), nullable=True)
    body = db.Column(db.String(300), nullable=True)
    created_at = db.Column(db.DateTime, nullable=False,default=datetime.now(pytz.timezone('Asia/Tokyo')))

# 今回追加
# UserMixinを継承することで、flaskのloginで必要な機能を持たせたClassを作成できる
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username= db.Column(db.String(30), unique=True)
    password = db.Column(db.String(12))


@app.route("/", methods=['GET', 'POST'])
def hello():
    
    if request.method == 'GET':
        posts = Post.query.all()
        return render_template('index.html', posts=posts)


@app.route("/create", methods=['GET', 'POST']) 
def create():
    if request.method == 'POST':
        title = request.form.get('title')
        body = request.form.get('body')

        post = Post(title=title, body=body)

        db.session.add(post)
        db.session.commit()

        return redirect('/')
    else:
        return render_template('create.html')

@app.route("/<int:id>/update", methods=['GET', 'POST']) 
def update(id):
    post = Post.query.get(id)
    
    if request.method == 'GET':
        return render_template('update.html', post=post)
    
    else:
        post.title = request.form.get('title')
        post.body = request.form.get('body')
        
        db.session.commit()

        return redirect('/')

@app.route("/<int:id>/delete", methods=['GET'])

def delete(id):
    post = Post.query.get(id)
    
    # delete処理
    db.session.delete(post)
    db.session.commit()
    
    return redirect('/')

```

対話モード
userというテーブルを作成したため、再度DBを作成（例のやつ）DB作成が完了したら
以下のファイルを作成

ユーザー登録を行う、signup.htmlの作成
```
{% extends "base.html" %}
{% block content %}
<h1>ユーザー登録</h1>
<form method="POST">
    <label for="">ユーザー名</label>
    <input type="text" name="username">
    <label for="password">パスワード</label>
    <input type="password" name="password">
    <input type="submit" value="新規登録">
</form>
{% endblock %}
```

login.htmlの作成
```login.html
{% extends "base.html" %}
{% block content %}
<h1>ログイン画面</h1>
<form method="POST">
    <label for="title">ユーザー名</label>
    <input type="text" name="username">
    <label for="body">パスワード</label>
    <input type="password" name="password">
    <input type="submit" value="ログイン">
</form>
{% endblock %}
```
---
app.pyにsignup、login、logout機能実装

```app.py
from flask import Flask
from flask import render_template
from flask import request
from flask import redirect
from flask_sqlalchemy import SQLAlchemy
# 今回追加
from flask_login import UserMixin, LoginManager, login_user, logout_user, login_required
# 今回追加 pip install必要
from werkzeug.security import generate_password_hash, check_password_hash
# 今回追加 セッションを暗号化するために必要
import os
from datetime import datetime
import pytz

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///blog.db"

#今回追加
#セッション状態を暗号化するための設定
# os.urandom(24)でランダムな値を生成し、SECRET_KEYという環境変数にセットする
app.config['SECRET_KEY'] = os.urandom(24)
db = SQLAlchemy(app)

# 今回追加
# LoginManagerクラスのインスタンス化
login_manager = LoginManager()

# 今回追加
# loginManagerと今回作成しているappの紐づけを行う
login_manager.init_app(app)

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title= db.Column(db.String(50), nullable=True)
    body = db.Column(db.String(300), nullable=True)
    created_at = db.Column(db.DateTime, nullable=False,default=datetime.now(pytz.timezone('Asia/Tokyo')))

# 今回追加
# UserMixinを継承することで、flaskのloginで必要な機能を持たせたClassを作成できる
class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username= db.Column(db.String(30), unique=True)
    password = db.Column(db.String(12))

# 今回追加
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(init(user_id))


@app.route("/", methods=['GET', 'POST'])
# 以下のデコレーターは、ログインしているユーザーだけに適用される
@login_required # 今回追加
def index():
    if request.method == 'GET':
        posts = Post.query.all()
        return render_template('index.html', posts=posts)


# 今回作成
@app.route("/signup", methods=['GET', 'POST']) 
def signup():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        # Userクラスに引数の情報を入れ、インスタンス化を行う
        user = User(username=username, password=generate_password_hash(password, method='sha256'))
        # インスタンス化した user をDBに追加
        db.session.add(user)
        # 変更を保存
        db.session.commit()

        # signup後は、ログインページへリダイレクト
        return redirect('/login')
    else:
        return render_template('signup.html')


# 今回作成
@app.route("/login", methods=['GET', 'POST']) 
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')

        # Userの中から、ユーザーを探す
        user = User.query.filter_by(username=username).first()
        
        # ユーザーが入力したパスワードが正しいか、ハッシュ値で確認を行う
        if check_password_hash(user.password, password):
            # パスワードが合っていたらログイン
            login_user(user)
            # ログインできたら、トップページにリダイレクト
            return redirect('/')

    else:
        return render_template('login.html')

# 今回作成
@app.route("/logout")

@login_required
def logout():
    logout_user()
    return redirect('login')


@app.route("/create", methods=['GET', 'POST']) 
@login_required
def create():
    if request.method == 'POST':
        title = request.form.get('title')
        body = request.form.get('body')

        post = Post(title=title, body=body)

        db.session.add(post)
        # 変更を保存
        db.session.commit()

        return redirect('/')
    else:
        return render_template('create.html')

# update処理を作成
@app.route("/<int:id>/update", methods=['GET', 'POST'])
@login_required
def update(id):
    post = Post.query.get(id)
    
    if request.method == 'GET':
        return render_template('update.html', post=post)
    
    # 更新ボタンが押されると、POSTが送信されるめ、eles移行が実行される
    else:
        post.title = request.form.get('title') # 書き換え
        post.body = request.form.get('body') # 書き換え
        
        # 不要なのでコメントアウト
        # post = Post(title=title, body=body)
        
        # db.session.add(post) 更新なので、addする必要はないため削除
        # commitだけすれば良い
        db.session.commit()

        return redirect('/')

# delete処理作成
# deleteは、POSTが流れることはないため、GETのみにする
@app.route("/<int:id>/delete", methods=['GET'])
@login_required
def delete(id):
    post = Post.query.get(id)
    
    # delete処理
    db.session.delete(post)
    db.session.commit()
    
    return redirect('/')

```

```signup.html
{% extends "base.html" %}
{% block content %}
<h1>ユーザー登録</h1>
<form method="POST">
    <label for="">ユーザー名</label>
    <input type="text" name="username">
    <label for="password">パスワード</label>
    <input type="password" name="password">
    <input type="submit" value="新規登録">
</form>
{% endblock %}
```

```login.html
{% extends "base.html" %}
{% block content %}
<h1>ログイン画面</h1>
<form method="POST">
    <label for="title">ユーザー名</label>
    <input type="text" name="username">
    <label for="body">パスワード</label>
    <input type="password" name="password">
    <input type="submit" value="ログイン">
</form>
{% endblock %}
```

```index.html
{% extends "base.html" %}
{% block content %}
<h1>ブログアプリケーション</h1>

<a href="/create" role="button">新規作成画面</a>
<a href="/logout" role="button">ログアウト</a>
{% for post in posts %}
<article>
    <h2>{{ post.title }}</h2>
    <a href="/{{post.id}}/update" role="button">編集</a>
    <!-- 削除ボタン作成 -->
    <a href="/{{post.id}}/delete" role="button">削除</a>
    <p>作成日時： {{ post.created_at }}</p>
    <p>{{ post.body }}</p>
</article>
{% endfor %}
{% endblock %}
```

`127.0.0.1:5000`にアクセスすると、`@login_required`でユーザー認証を行いログインしていない
ため、以下の様に**Unauthorized**となる。
![Flask-1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3014502/3cb5a111-2f31-5c12-cbe3-e83ccbd4c351.png)

`http://127.0.0.1:5000/signup`へアクセスし、ユーザーとパスワード登録を行う。
ユーザー名：testuser
パスワード：1234
![Flask-2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3014502/8eb8d8dd-81ce-c127-f404-184d378546d5.png)

新規登録を行うと、ログイン画面に戻るので、登録したユーザーとパスワードを入力する。
![Flask-3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3014502/82b2341e-5038-bf9d-f6bd-f0d368709199.png)

ログインすると、以下の様にトップページが表示される。
![Flask-4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3014502/3df9849a-6f7b-ae49-330e-3c07ef1fb3d8.png)