Flaskを用いたウェブアプリケーション作成④

# 16. 新規登録機能作成
以下では、ユーザーの新規登録機能を追加しました。
## 16-1. Userモデル定義
前回Reviewモデルを作成したように、Userのモデルも作成しました。

```python:./cafesite/models/users.py
from cafe_site import db
from datetime import datetime
import bcrypt

class User(db.Model):
# usersテーブル定義
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50))
    password = db.Column(db.String(50))
    salt = db.Column(db.String(50))
    created_at = db.Column(db.DateTime)

    def __init__(self, username=None, password=None):
        self.username = username
        self.salt = bcrypt.gensalt().decode()
        self.password = bcrypt.hashpw(password.encode(), self.salt.encode()).decode()
        self.created_at = datetime.utcnow()

    def __repr__(self):
        return '<Entry id:{} username:{}>'.format(self.id, self.username)
```
## 16-2. パスワード保護
パスワードを暗号化しデータベースへ保存します。

**users.py**では`self.salt = bcrypt.gensalt().decode()`でソルトを生成し,
`self.password = bcrypt.hashpw(password.encode(), self.salt.encode()).decode()`でパスワードをハッシュ化したものと生成されたソルトを結合することでセキュリティを強化しています。
## 16-3. Reviewモデル変更
どのユーザーが書いた記事なのか判断する為の処理をmodels/reviews.pyに付け加えます。

```python:./cafesite/models/reviews.py
from cafe_site import db
from datetime import datetime

class Review(db.Model):
    from cafe_site.models.users import User
    __tablename__ = 'Reviews'
    id = db.Column(db.Integer, primary_key=True)
    star = db.Column(db.Integer)
    title = db.Column(db.String(50), unique=True)
    text = db.Column(db.Text)
# usersテーブルのidを外部キーとして取得
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Reviewsテーブルのuserカラムにusersテーブルのuser_idカラムを関連付ける
    user = db.relationship('User', foreign_keys=user_id)
    created_at = db.Column(db.DateTime)

    def __init__(self, star=None, title=None, text=None, user_id=None):
        self.star = star
        self.title = title
        self.text = text
        self.user_id = user_id
        self.created_at = datetime.utcnow()

    def __repr__(self):
        return '<Review id:{} star:{} title:{} text:{} user_id:{}>'.format(self.id, self.star, self.title, self.text, self.user_id)
```
Userモデルで生成されたid属性を`user_id = db.Column(db.Integer, db.ForeignKey('users.id'))`で外部キーとして受け取り、`user = db.relationship('User', foreign_keys=user_id)`で外部キー用いてReviewモデルを操作出来るようにしました。
## 16-4. 新規投稿リンク追加
theme.htmlを編集してナビゲーションバーに新規登録するための「Signup」リンクを追加しました。

```html:./cafe_site/templates/theme.html
<ul class="main-nav">
    {% if not session.logged_in %}
     <li><a href="{{ url_for('user.new_user') }}">Signup</a></li>
    {% else %}

    {% endif %}
</ul>
```
新規登録リンクにアクセスした時、新規登録フォームを返すようusers.pyを作成し、new_reviewを追加します。

```python:./cafesite/views/users.py
@user.route('/users/new', methods=['GET'])
# 新規投稿フォーム
def new_user():
    return render_template('users/new.html', id='user')
```
## 16-5. 新規登録フォーム作成

```html:./cafe_site/templates/users/new.html
{% extends "theme.html" %}
{% block title %}Coffee House{% endblock %}
{% block head %}
  {{ super() }}
{% endblock %}
{% block content %}
<div class="wrapper">
    <h2 class="page-title">Signup</h2>
</div>
<div class="login-content wrapper">
  <form action="{{ url_for('user.add_user') }}" method=post>
    <div>
      <label for="name">ユーザ名</label>
      <input placeholder="username" type="text" name=username>
    </div>
    <div>
      <label for="Password">パスワード</label>
      <input placeholder="password" type="password" name=password>
    </div>
      <input type="submit" class="button" value="新規登録">
  </form>
</div>
{% endblock %}
{% block footer %}
  <footer>
      <div class="wrapper">
          <p><small>&copy; Coffee House</small></p>
      </div>
  </footer>
{% endblock %}
```
## 16-6. ユーザーデータ保存
これで新規登録フォームの作成は終わりました。、次に、登録したユーザーデータを先ほど作成したモデルインスタンスを用いてデータベースに保存するための機能を作成します。
## 16-7. add_user作成
users/new.htmlでは、フォームの投稿先は、
`action="{{ url_for('user.add_user') }}"`としています。**user.add_user**を作成し、投稿内容を受信してデータベースに保存する処理を追加します。views/users.pyに**add_user**を追加します。

```python:./cafesite/views/users.py
@user.route('/users', methods=['POST'])
def add_user():
    user = User(
            username=request.form['username'],
            password=request.form['password']
            )
    db.session.add(user)
    db.session.commit()
    flash('新規ユーザ登録が完了しました。ログインしてください。')
    return redirect(url_for('loging.login'))
```
Userモデルを使って、送られてきたユーザーネームとパスワードについてのモデルインスタンスを作成し、データベースに保存します。
ここで、views/users.pyの全体像を確認します。

```python:./cafesite/views/users.py
from flask import request, redirect, url_for, render_template, flash, session
from cafe_site import app
from cafe_site import db
from cafe_site.models.users import User
from cafe_site.views.loging import login_required
from flask import Blueprint


user = Blueprint('user', __name__)


@user.route('/users', methods=['POST'])
def add_user():
    user = User(
            username=request.form['username'],
            password=request.form['password']
            )
    db.session.add(user)
    db.session.commit()
    flash('新規ユーザ登録が完了しました。ログインしてください。')
    return redirect(url_for('loging.login'))


@user.route('/users/new', methods=['GET'])
def new_user():
    return render_template('users/new.html', id='user')
```
## 16-8. views/loging.py変更


```python:./cafesite/views/loging.py
from flask import request, redirect, url_for, render_template, flash, session
from cafe_site import app
from functools import wraps
from cafe_site.models.users import User
import bcrypt
from flask import Blueprint

loging = Blueprint('loging', __name__)

def login_required(loging):
    @wraps(loging)
    def inner(*args, **kwargs):
        if not session.get('logged_in'):
            return redirect(url_for('loging.login'))
        return loging(*args, **kwargs)
    return inner


@loging.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        try:
            user = User.query.filter_by(username=request.form['username']).first()
        except:
            flash('ユーザ名が異なります')
            return render_template('login.html')
        if request.form['username'] != user.username:
            flash('ユーザ名が異なります')
        elif bcrypt.hashpw(request.form['password'].encode(), user.salt.encode()).decode() != user.password:
            flash('パスワードが異なります')
        else:
            session['logged_in'] = True
            session['user_id'] = user.id
            flash('ログインしました')
            return redirect(url_for('review.show_reviews'))
    return render_template('login.html', id="login")

@loging.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash('ログアウトしました')
    return redirect(url_for('review.show_reviews'))

@loging.app_errorhandler(404)
def non_existant_route(error):
    return redirect(url_for('loging.login'))
```
Userモデルから送られたユーザーデータでログイン出来るよう処理し直しました。
## 16-9. cafe_site/__init__.py変更
最後にcafe_site/__init__.pyの変更を行います。

```python:./cafe_site/__init__.py
from cafe_site.views.users import user
app.register_blueprint(user)
```
これで、ウェブアプリケーションが完成しました。
> heroku を用いて今回作成したウェブアプリケーションをクラウド上で起動する方法については時間のある時に追記したいと思います^^;。